面試資料-作業系統

PURE2PURE發表於2020-12-30

作業系統:

作業系統:

  1. 作業系統(Operating System,簡稱 OS)是管理計算機硬體與軟體資源的程式,是計算機系統的核心與基石;
  2. 作業系統本質上是執行在計算機上的軟體程式 ;
  3. 作業系統為使用者提供一個與系統互動的操作介面 ;
  4. 作業系統分核心與外殼(我們可以把外殼理解成圍繞著核心的應用程式,而核心就是能操作硬體的程式)。
    【關於核心多插一嘴:核心負責管理系統的程式、記憶體、裝置驅動程式、檔案和網路系統等等,決定著系統的效能和穩定性。是連線應用程式和硬體的橋樑。 核心就是作業系統背後黑盒的核心。】
    在這裡插入圖片描述

CPU及其工作狀態
特權指令:僅供OS核心程式使用的指令。(如:啟動外設、清空記憶體、載入PSW、載入PC等敏感操作)
普通指令:除特權指令以外的指令。
管態:
可執行指令全集、訪問全部記憶體和所有系統資源。
【系統態(kernel mode):
可以簡單的理解系統態執行的程式或程式幾乎可以訪問計算機的任何資源,不受限制。】
目態:
只能執行規定的指令、訪問指定暫存器和指定儲存區域。
【使用者態(user mode) : 使用者態執行的程式或可以直接讀取使用者程式的資料。】

使用者態,核心態:
從巨集觀上來看,Linux作業系統的體系架構分為使用者態和核心態(或者使用者空間和核心)。核心從本質上看是一種軟體——控制計算機的硬體資源,並提供上層應用程式執行的環境。使用者態即上層應用程式的活動空間,應用程式的執行必須依託於核心提供的資源,包括CPU資源、儲存資源、I/O資源等。為了使上層應用能夠訪問到這些資源,核心必須為上層應用提供訪問的介面:即系統呼叫。
當一個程式在執行使用者自己的程式碼時處於使用者執行態(使用者態),此時特權級最低,為3級,是普通的使用者程式執行的特權級,大部分使用者直接面對的程式都是執行在使用者態。Ring3狀態不能訪問Ring0的地址空間,包括程式碼和資料;當一個程式因為系統呼叫陷入核心程式碼中執行時處於核心執行態(核心態),此時特權級最高,為0級。執行的核心程式碼會使用當前程式的核心棧,每個程式都有自己的核心棧。
總結一下,使用者態的應用程式可以通過三種方式來訪問核心態的資源(切換到核心態):
1)系統呼叫
2)庫函式
3)Shell指令碼
系統呼叫是作業系統的最小功能單位,這些系統呼叫根據不同的應用場景可以進行擴充套件和裁剪,現在各種版本的Unix實現都提供了不同數量的系統呼叫,如Linux的不同版本提供了240-260個系統呼叫,FreeBSD大約提供了320個(reference:UNIX環境高階程式設計)。我們可以把系統呼叫看成是一種不能再化簡的操作(類似於原子操作,但是不同概念),有人把它比作一個漢字的一個“筆畫”,而一個“漢字”就代表一個上層應用,我覺得這個比喻非常貼切。因此,有時候如果要實現一個完整的漢字(給某個變數分配記憶體空間),就必須呼叫很多的系統呼叫。如果從實現者(程式設計師)的角度來看,這勢必會加重程式設計師的負擔,良好的程式設計方法是:重視上層的業務邏輯操作,而儘可能避免底層複雜的實現細節。庫函式正是為了將程式設計師從複雜的細節中解脫出來而提出的一種有效方法。它實現對系統呼叫的封裝,將簡單的業務邏輯介面呈現給使用者,方便使用者呼叫,從這個角度上看,庫函式就像是組成漢字的“偏旁”。這樣的一種組成方式極大增強了程式設計的靈活性,對於簡單的操作,我們可以直接呼叫系統呼叫來訪問資源,如“人”,對於複雜操作,我們藉助於庫函式來實現,如“仁”。顯然,這樣的庫函式依據不同的標準也可以有不同的實現版本,如ISO C 標準庫,POSIX標準庫等。
Shell是一個特殊的應用程式,俗稱命令列,本質上是一個命令直譯器,它下通系統呼叫,上通各種應用,通常充當著一種“膠水”的角色,來連線各個小功能程式,讓不同程式能夠以一個清晰的介面協同工作,從而增強各個程式的功能。同時,Shell是可程式設計的,它可以執行符合Shell語法的文字,這樣的文字稱為Shell指令碼,通常短短的幾行Shell指令碼就可以實現一個非常大的功能,原因就是這些Shell語句通常都對系統呼叫做了一層封裝。為了方便使用者和系統互動,一般,一個Shell對應一個終端,終端是一個硬體裝置,呈現給使用者的是一個圖形化視窗。我們可以通過這個視窗輸入或者輸出文字。這個文字直接傳遞給shell進行分析解釋,然後執行。

系統呼叫
我們執行的程式基本都是執行在使用者態,如果我們呼叫作業系統提供的系統態級別的子功能咋辦呢?那就需要系統呼叫了!
也就是說在我們執行的使用者程式中,凡是與系統態級別的資源有關的操作(如檔案管理、程式控制、記憶體管理等),都必須通過系統呼叫方式向作業系統提出服務請求,並由作業系統代為完成。
這些系統呼叫按功能大致可分為如下幾類:
 裝置管理。完成裝置的請求或釋放,以及裝置啟動等功能。
 檔案管理。完成檔案的讀、寫、建立及刪除等功能。
 程式控制。完成程式的建立、撤銷、阻塞及喚醒等功能。
 程式通訊。完成程式之間的訊息傳遞或訊號傳遞等功能。
 記憶體管理。完成記憶體的分配、回收以及獲取作業佔用記憶體區大小及地址等功能。
使用者程式中對作業系統的呼叫稱為系統呼叫。使使用者程式通過簡單的呼叫來實現一些硬體相關,應用無關的工作,從而簡化了使用者程式。
獨立程式:不需要作業系統幫助的程式;
非獨立程式:需要。

處理過程:
(1)將處理機狀態由使用者態轉為系統態;之後,由硬體和核心程式進行系統呼叫的一般性處理,即首先保護被中斷程式的CPU環境,將處理機狀態字PSW、程式計數器PC、系統呼叫號、使用者找指標以及通用暫存器內容等壓入堆疊;然後,將使用者定義的引數傳送到指定的地方儲存起來。
  (2)分析系統呼叫型別,轉入相應的系統呼叫處理子程式。為使不同的系統呼叫能方便地轉向相應的系統呼叫處理子程式,在系統中配置了一張系統呼叫入口表。表中的每個表目都對應一條系統呼叫,其中包含該系統呼叫自帶引數的數目、系統呼叫處理子程式的入口地址等。核心可利用系統呼叫號去查詢該表,即可找到相應處理子程式的入口地址而轉去執行它。
  (3)在系統呼叫處理子程式執行完後,恢復被中斷的或設定新程式的CPU現場,然後返冋被中斷程式或新程式,繼續往下執行。

系統呼叫是動態連線的。
靜態連線:程式在編譯時,將被呼叫的程式嵌入到自身中。如:庫函式呼叫。
動態連線:程式在執行過程中,執行到呼叫指令時,才連線到被呼叫的程式進行執行。如:動態連線庫,系統呼叫等。

中斷:
是指計算機在執行程式過程中,當遇到需要立即處理的事件時,暫停當前執行的程式,轉去執行有關服務程式,處理完後自動返回原程式。
發生中斷的原因:系統呼叫,程式異常,IO事件完成,時間片結束,等等。
可以歸結為兩大方面,一任務間切換的時候發生中斷,二由使用者態進入系統態時發生中斷。

中斷的執行過程:儲存現場,將PSW等現場資訊放入堆疊中,然後轉去相應的屮斷處理程式。中斷結束返回時,恢復現場,從堆疊屮取出PSW等現場資訊。繼續執行原程式。

中斷請求,中斷響應,中斷點(暫停當前任務並儲存現場),中斷處理例程,中斷返回(恢復中斷點的現場並繼續原有任務)

作業系統最為重要的硬體基礎是硬體的中斷機構:

  1. 當作業系統無事可做時以“閒逛”的形式等待事件的發生。各種事件以各種中斷源發向CPU,經過中斷機構響應後,進入對作業系統某些功能的呼叫,OS從而被驅動。
  2. 因為作業系統的所有功能都是由中斷驅動的,所以只有藉助中斷,OS才能獲得系統監控權。(所以,中斷是驅動和啟用OS唯一的手段)
  3. 作業系統核心程式碼執行在系統態(也叫管態、核心態)。
  4. 使用者程式程式碼執行在使用者態(也叫目態、常態)。
  5. 從使用者態進入核心態的唯一途徑是中斷。
    在這裡插入圖片描述

CPU,快取,記憶體,磁碟:

CPU
CPU是中央處理器的簡稱,它可以從記憶體和快取中讀取指令,放入指令暫存器,並能夠發出控制指令來完成一條指令的執行。但是CPU並不能直接從硬碟中讀取程式或資料。
記憶體
記憶體作為與CPU直接進行溝通的部件,所有的程式都是在記憶體中執行的。其作用是暫時存放CPU的運算資料,以及與硬碟交換的資料。也是相當於CPU與硬碟溝通的橋樑。只要計算機在執行,CPU就會把需要運算的資料調到記憶體中進行運算,運算完成後CPU再將結果傳出來。
快取
快取是CPU的一部分,存在於CPU裡。由於CPU的存取速度很快,而記憶體的速度很慢,為了不讓CPU每次都在執行相對緩慢的記憶體中操作,快取就作為一箇中間者出現了。有些常用的資料或是地址,就直接存在快取中,這樣,下一次呼叫的時候就不需要再去記憶體中去找了。因此,CPU每次回先到自己的快取中尋找想要的東西(一般80%的東西都可以找到),找不到的時候再去記憶體中獲取。
最初的快取生產成本很高,價格昂貴,所以為了儲存更多的資料,又不希望成本過高,就出現了二級快取的概念,他們採用的並不是一級快取的SRAM(靜態RAM),而是採用了效能比SRAM稍差一些,但是比記憶體更快的DRAM(動態RAM)
硬碟
我們都知道記憶體是掉電之後資料就消失的部件,所以,長期的資料儲存更多的還是依靠硬碟這種本地磁碟作為儲存工具。
簡單的概括:
CPU主要用於計算,執行時首先會去自身的快取中尋找資料,如果沒有再去記憶體中找。
硬碟中的資料會先寫入記憶體才能被CPU使用。
快取會記錄一些常用的資料等資訊,以免每次都要到記憶體中,節省了時間,提高了效率。
記憶體+快取 -> 記憶體儲空間
硬碟 -> 外儲存空間
讀寫速度:
CPU快取速度>記憶體速度>硬碟速度

CPU三級快取:
1、 一級快取,是CPU的第一層快取記憶體,主要分為資料快取和指令快取,這是對CPU效能影響最大的一層;
一級快取基本上都是內建在cpu的內部和cpu一個速度進行執行,能有效的提升cpu的工作效率。一級快取越多,cpu的工作效率就會越來越高,是cpu的內部結構限制了一級快取的容量大小,使一級快取的容量都是很小的。
2、 二級快取主要作用是協調一級快取和記憶體之間的工作效率。cpu首先用的是一級記憶體,當cpu的速度慢慢提升之後,一級快取就不夠cpu的使用量了,這就需要用到二級記憶體。
二級快取,是CPU的第二層快取記憶體,分內部和外部兩種晶片,內部晶片速度基本上與CPU主頻相同,而外部晶片只有主頻的一半。
3、 三級快取和一級快取與二級快取的關係差不多,是為了在讀取二級快取不夠用的時候而設計的一種快取手段,在有三級快取cpu之中,只有大約百分之五的資料需要在記憶體中調取使用,這能提升cpu不少的效率,從而cpu能夠高速的工作。
三級快取,離CPU較遠,讀取速度沒一級二級快,但一般三級快取容量比前面兩級大很多。
4、 二級快取Intel的CPU是很重要,Intel的CPU的二級快取越大效能提升非常明顯,而AMD的CPU雖然二級快取也很重要,但是二級快取大小對AMD的CPU的效能提升不是很明顯。

CPU,核,執行緒
單核cpu和多核cpu
• 都是一個cpu,不同的是每個cpu上的核心數。
• 一個CPU可以有單核,也可以多核。
• 多核cpu是多個單核cpu的替代方案,多核cpu減小了體積,同時也減少了功耗。
• 一個核心只能同時執行一個執行緒。

序列,併發與並行
序列
多個任務,執行時一個執行完再執行另一個。
比喻:吃完飯再看球賽。
併發
多個執行緒在單個核心執行,同一時間一個執行緒執行,系統不停切換執行緒,看起來像同時執行,實際上是執行緒不停切換。
比喻: 一會跑去食廳吃飯,一會跑去客廳看球賽。
並行
每個執行緒分配給獨立的核心,執行緒同時執行。
比喻:一邊吃飯一邊看球賽。

執行緒,程式,協程:

程式的引入:在多道程式系統下,記憶體中可以裝入多個程式(程式是一組有序指令的集合,並存放在某種介質上,是靜態的),它們共享系統資源、併發執行。但這樣一來程式就會失去了其封閉性,並具有間斷性,以及其執行結果不可再現的特徵,那這樣的話程式的執行就失去了意義。
在批處理系統中:但是問題也來了,當程式在運算的時候,會一直佔用著CPU資源,有可能某個時間在寫磁碟資料、讀取網路裝置資料,一直霸佔著CPU會造成資源的浪費,其實CPU空閒著也是浪費,這時候完全可以把CPU的計算資源讓給其他程式,直到資料讀寫準備就緒後再切換回來。
因此,為了使程式能夠併發進行,並且可以對併發執行的程式加以描述和控制,人們引入了“程式”的概念。
怎麼控制和管理多個程式間的計算機資源呢?劃分資源管理的最小單元啊:程式,也就是上面說的記憶體中載入指令的最小單位。有了程式,作業系統才好在記憶體中載入程式的執行指令,方便排程程式在有限資源情況下更有效的利用資源,提高資源利用率。
程式實體:由三部分構成:程式段、相關的資料段、PCB(程式控制塊),所謂建立/撤銷程式,實際上就是建立/撤銷程式實體中的PCB。
程式是指在系統中正在執行的一個應用程式,每個程式都有自己獨立的記憶體空間,比如使用者點選桌面的IE瀏覽器,就啟動了一個程式,作業系統就會為該程式分配獨立的地址空間。資源分配的最小單位。
在這裡插入圖片描述
執行緒的引入: 在引入了程式的概念後,此後長達20年的時間裡,在多道程式OS中一直把程式作為能擁有資源和獨立排程執行的基本單位,但是後來漸漸發現這樣併發執行後造成的時空開銷非常大,因為系統要進行併發執行需要做一系列的操作:建立程式、撤銷程式、程式切換。這樣一來就限制了系統中所設定的程式的數目,於是人們開始追尋新的方法來改善這一點。即使劃分了資源管理的最小單元,但是一個程式在執行的過程中,也不可能一直佔據著CPU進行邏輯運算,執行過程中很可能在進行磁碟I/O或者網路I/O,資源還是有些浪費。為了更加充分利用CPU運算資源,提高資源利用率,有人設計了執行緒的概念。
為何非要設計出執行緒呢?將程式再劃分小一點(爭取在程式空閒時劃分為小的程式),開多個程式也可以提升CPU的利用率,但是開多個程式的話,程式間通訊又是個麻煩的事情,畢竟程式之間地址空間是獨立的,沒法像執行緒那樣做到資料的共享,需要通過其他的手段來解決,比如管道等。
一般作業系統都會分為核心態和使用者態,使用者態執行緒之間的地址空間是隔離的,而在核心態,所有執行緒都共享同一核心地址空間。不管是使用者執行緒還是核心執行緒,都和程式一樣,均由作業系統的排程器來統一排程(至少在Linux中是這樣子)。
因此,為了使多個程式能夠更好地併發執行,又能減少程式在併發執行時所付出的時空開銷,人們引入了“執行緒”的概念。
在這裡插入圖片描述
一個程式可以擁有多個執行緒–>這就是我們常說的多執行緒程式設計。
執行緒是由程式建立的(寄生在程式)。
執行緒是程式中的一個實體,是CPU排程和分派的基本單位,執行緒自己不擁有系統資源,只擁有一點在執行中必不可少的資源,但它可與同屬一個程式的其它執行緒共享程式所擁有的全部資源。一個執行緒可以建立和撤消另一個執行緒,同一程式中的多個執行緒之間可以併發執行。
執行緒沒有獨立的地址空間(記憶體空間);
一個執行緒只能屬於一個程式,而一個程式可以有多個執行緒,但至少有一個執行緒。
區別:
程式有自己的獨立地址空間,執行緒沒有
程式是資源分配的最小單位,執行緒是CPU排程和分配的最小單位

程式和執行緒通訊方式不同(執行緒之間的通訊比較方便。同一程式下的執行緒共享資料(比如全域性變數,靜態變數),通過這些資料來通訊不僅快捷而且方便,當然如何處理好這些訪問的同步與互斥正是編寫多執行緒程式的難點。而程式之間的通訊只能通過程式通訊的方式進行。
程式上下文切換開銷大,執行緒開銷小
一個程式掛掉了不會影響其他程式,而執行緒掛掉了會影響其他執行緒
對程式程式操作一般開銷都比較大,對執行緒開銷就小了
協程的引入
在多核場景下,如果是I/O密集型場景,就算開多個執行緒來處理,也未必能提升CPU的利用率,反而會增加執行緒切換的開銷。另外,多執行緒之間假如存在臨界區或者共享資料,那麼同步的開銷也是不可忽視的。協程恰恰就是用來解決該問題的。協程是輕量級執行緒,在一個使用者執行緒上可以跑多個協程,這樣可以提升單核的利用率。
為了提升使用者執行緒的最大利用效率,又提出了協程的概念,可以充分提高單核的CPU利用率,降低排程的開銷(協程因不受作業系統資源管理的自動排程,如果需要可以手工或寫程式碼排程)。
在實際場景下,假如CPU有N個核,就只要開N+1個執行緒,然後在這些執行緒上面跑協程就行了。
協程不像程式或執行緒那樣可以讓系統負責相關的排程工作,協程處於一個執行緒當中,系統是無感知的,如果需要在該執行緒中阻塞某個協程的話,需要手工進行排程,如圖所示。
要在使用者執行緒上實現協程是一件很難受的事情,原理類似於排程器根據條件的改變不停地呼叫各個協程的callback機制,但前提是大家都在一個使用者執行緒下。要注意,一旦有一個協程阻塞,其他協程也都不能執行了。
協程
協程(Coroutines)是一種比執行緒更加輕量級的存在,正如一個程式可以擁有多個執行緒一樣,一個執行緒可以擁有多個協程。
協程不是被作業系統核心所管理的,而是完全由程式所控制,也就是在使用者態執行。這樣帶來的好處是效能大幅度的提升,因為不會像執行緒切換那樣消耗資源。
協程不是程式也不是執行緒,而是一個特殊的函式,這個函式可以在某個地方掛起,並且可以重新在掛起處外繼續執行。所以說,協程與程式、執行緒相比並不是一個維度的概念。
比如生產者消費者裡面,讓消費者等待,可以用協程。讓協程暫停,和執行緒的阻塞是有本質區別的。協程的暫停完全由程式控制,執行緒的阻塞狀態是由作業系統核心來進行切換。協程的開銷遠遠小於執行緒的開銷。
一個程式可以包含多個執行緒,一個執行緒也可以包含多個協程。簡單來說,一個執行緒內可以由多個這樣的特殊函式在執行,但是有一點必須明確的是,一個執行緒的多個協程的執行是序列的。如果是多核CPU,多個程式或一個程式內的多個執行緒是可以並行執行的,但是一個執行緒內協程卻絕對是序列的,無論CPU有多少個核。畢竟協程雖然是一個特殊的函式,但仍然是一個函式。一個執行緒內可以執行多個函式,但這些函式都是序列執行的。當一個協程執行時,其它協程必須掛起。
程式、執行緒、協程的對比
• 協程既不是程式也不是執行緒,協程僅僅是一個特殊的函式,協程它程式和程式不是一個維度的。
• 一個程式可以包含多個執行緒,一個執行緒可以包含多個協程。
• 一個執行緒內的多個協程雖然可以切換,但是多個協程是序列執行的,只能在一個執行緒內執行,沒法利用CPU多核能力。
• 協程與程式一樣,切換是存在上下文切換問題的。
總結:
多程式的出現是為了提升CPU的利用率,特別是I/O密集型運算,不管是多核還是單核,開多個程式必然能有效提升CPU的利用率。但程式間依然有資源利用優化空間,以及程式間通訊的麻煩問題。
多執行緒則可以共享同一程式地址空間上的資源,能在資源的空閒時刻更好的利用,且不存在程式間通訊的麻煩。但執行緒的建立和銷燬會造成資源的浪費。
為了降低執行緒建立和銷燬的開銷,又出現了執行緒池的概念,在一開始就建立批量的執行緒。雖然減少了部分建立和銷燬執行緒所消耗的資源,但排程的開銷依然存在。
為了提升使用者執行緒的最大利用效率,又提出了協程的概念,可以充分提高單核的CPU利用率,降低排程的開銷(協程因不受作業系統資源管理的自動排程,如果需要可以手工或寫程式碼排程)。

執行緒的實現(3):
I. 核心級執行緒:
核心支援執行緒,是在核心的支援下執行的,即無論是使用者程式中的執行緒,還是系統程式中的執行緒,他們的建立、撤消和切換等,是依靠核心實現的。
在核心空間中為每一個核心支援執行緒設定了一個執行緒控制塊TCB, 核心是根據該控制塊而感知某執行緒的存在的,並對其加以控制。所有執行緒管理由核心完成,雖沒有執行緒庫,但核心提供API。
優點:
在多處理器系統中,核心能夠同時排程同一程式中多個執行緒並行執行;
如果程式中的一個執行緒被阻塞了,核心可以排程該程式中的其它執行緒佔有處理器執行,也可以執行其它程式中的執行緒;
核心支援執行緒具有很小的資料結構和堆疊,執行緒的切換比較快,切換開銷小;
核心本身也可以採用多執行緒技術,可以提高系統的執行速度和效率。
缺點:
對於執行緒切換而言,其模式切換的開銷較大,在同一個程式中,從一個執行緒切換到另一個執行緒時,需要從使用者態轉到核心態進行,這是因為使用者程式的執行緒在使用者態執行,而執行緒排程和管理是在核心實現的,系統開銷較大。
核心級執行緒一對一實現方式:
由核心來完成執行緒切換,核心通過操縱排程器(Scheduler)對執行緒進行排程,並負責將執行緒的任務對映到各個處理器上。每個核心執行緒可以視為核心的一個分身,這樣作業系統就有能力同時處理多件事情,支援多執行緒的核心就叫做多執行緒核心。
程式一般不會直接去使用核心執行緒,而是去使用核心執行緒的一種高階介面——輕量級程式(我們通常意義上所講的執行緒)
在這裡插入圖片描述
(schedule排程器,LWP輕量級執行緒,KLT核心級執行緒,P程式)
由於核心執行緒的支援,每個輕量級程式都成為一個獨立的排程單元,即使有一個輕量級程式在系統呼叫中阻塞了,也不會影響整個程式繼續工作,但是輕量級程式具有它的侷限性:
首先,由於是基於核心執行緒實現的,所以各種執行緒操作,如建立、析構及同步,都需要進行系統呼叫。而系統呼叫的代價相對較高,需要在使用者態(User Mode)和核心態(Kernel Mode)中來回切換。
其次,每個輕量級程式都需要有一個核心執行緒的支援,因此輕量級程式要消耗一定的核心資源(如核心執行緒的棧空間),因此一個系統支援輕量級程式的數量是有限的。

II. 使用者級執行緒:
使用者級執行緒僅存在於使用者空間中。對於這種執行緒的建立、撤消、執行緒之間的同步與通訊等功能,都無須核心來實現。
由應用程式完成所有執行緒的管理;
執行緒庫(使用者空間):通過一組管理執行緒的函式庫來提供一個執行緒執行管理系統(執行系統);
執行緒切換不需要核心態特權,因而使執行緒的切換速度特別快;
使用者執行緒指的是完全建立在使用者空間的執行緒庫上,系統核心不能感知執行緒存在的實現。使用者執行緒的建立、同步、銷燬和排程完全在使用者態中完成,不需要核心的幫助。如果程式實現得當,這種執行緒不需要切換到核心態(在程式執行中沒有這個必要),因此操作可以是非常快速且低消耗的,也可以支援規模更大的執行緒數量,部分高效能資料庫中的多執行緒就是由使用者執行緒實現的。這種程式與使用者執行緒之間1:N的關係稱為一對多的執行緒模型。
UT使用者級執行緒
UT使用者級執行緒
使用使用者執行緒的優勢在於不需要系統核心支援,劣勢也在於沒有系統核心的支援,所有的執行緒操作都需要使用者程式自己處理。執行緒的建立、切換和排程都是需要考慮的問題,而且由於作業系統只把處理器資源分配到程式,那諸如“阻塞如何處理”、“多處理器系統中如何將執行緒對映到其他處理器上”這類問題解決起來將會異常困難,甚至不可能完成。

優點:
執行緒切換不呼叫核心;
排程是應用程式特定的:可以選擇最好的演算法;
可執行在任何作業系統上(只需要執行緒庫),可以在一個不支援執行緒的OS上實現。
缺點:
大多數系統呼叫會引起程式阻塞,並且是程式中所有執行緒將被阻塞;
核心只將處理器分配給程式,同一程式中的兩個執行緒不能同時執行於兩個處理器上;
但是注意:對於設定了使用者級執行緒的系統,其排程仍是以程式為單位進行。在採用時間片輪轉排程演算法時,各程式間是公平的,但各程式的執行緒間是不公平的。

使用者級執行緒是在使用者空間實現的。所有使用者級執行緒都具有相同的資料結構,它們都執行在一箇中間系統上。當前有兩種方式實現的中間系統:
1執行時系統
是用於管理和控制執行緒的函式的集合,又稱為執行緒庫。包括建立、撤消執行緒函式、執行緒同步和通訊函式、執行緒排程函式等。使用者級執行緒不能直接利用系統呼叫,必須通過執行時系統間接利用系統呼叫。
2核心控制執行緒
這種執行緒又稱為輕型程式LWP(Light Weight Process)。每個程式都可擁有多個LWP,每個LWP都有自己的TCB,其中包括執行緒識別符號、優先順序、狀態、棧和區域性儲存區等。如下圖所示:
在這裡插入圖片描述
III. 組合方式:
有些OS把核心支援執行緒和使用者級執行緒兩種方式進行組合,在這種組合方式執行緒系統中,核心支援多個核心支援執行緒的建立、排程和管理,同時,也允許使用者應用程式建立、排程和管理使用者級執行緒。
同一個程式內的多個執行緒可以同時在多處理器上並行執行,而且在阻塞一個執行緒時並不需要將整個程式阻塞。有三種不同的模型:多對一模型、一對一模型和多對多模型。
使用者執行緒還是完全建立在使用者空間中,因此使用者執行緒的建立、切換、析構等操作依然廉價,並且可以支援大規模的使用者執行緒併發。而作業系統提供支援的輕量級程式則作為使用者執行緒和核心執行緒之間的橋樑,這樣可以使用核心提供的執行緒排程功能及處理器對映,並且使用者執行緒的系統呼叫要通過輕量級執行緒來完成,大大降低了整個程式被完全阻塞的風險。
在這裡插入圖片描述
Java執行緒的實現 與 排程模型
對於Sun JDK來說,它的Windows版與Linux版都是使用一對一的執行緒模型實現的,一條Java執行緒就對映到一條輕量級程式之中,因為Windows和Linux系統提供的執行緒模型就是一對一的。
Java執行緒排程
執行緒排程是指系統為執行緒分配處理器CPU使用權的過程,主要排程方式有兩種,分別是協同式執行緒排程(Cooperative Threads-Scheduling)和搶佔式執行緒排程(Preemptive Threads-Scheduling)。JAVA中是搶佔式排程。
協同式排程
如果使用協同式排程的多執行緒系統,執行緒的執行時間由執行緒本身來控制,執行緒把自己的工作執行完了之後,要主動通知系統切換到另外一個執行緒上。協同式多執行緒的最大好處是實現簡單,而且由於執行緒要把自己的事情幹完後才會進行執行緒切換,切換操作對執行緒自己是可知的,所以沒有什麼執行緒同步的問題。Lua語言中的“協同例程”就是這類實現。它的壞處也很明顯:執行緒執行時間不可控制,甚至如果一個執行緒編寫有問題,一直不告知系統進行執行緒切換,那麼程式就會一直阻塞在那裡。很久以前的Windows 3.x系統就是使用協同式來實現多程式多工的,相當不穩定,一個程式堅持不讓出CPU執行時間就可能會導致整個系統崩潰。
搶佔式排程
如果使用搶佔式排程的多執行緒系統,那麼每個執行緒將由系統來分配執行時間,執行緒的切換不由執行緒本身來決定(在Java中,Thread.yield()可以讓出執行時間,但是要獲取執行時間的話,執行緒本身是沒有什麼辦法的)。在這種實現執行緒排程的方式下,執行緒的執行時間是系統可控的,也不會有一個執行緒導致整個程式阻塞的問題,Java使用的執行緒排程方式就是搶佔式排程。在JDK後續版本中有可能會提供協程(Coroutines)方式來進行多工處理。與前面所說的Windows 3.x的例子相對,在Windows 9x/NT核心中就是使用搶佔式來實現多程式的,當一個程式出了問題,我們還可以使用工作管理員把這個程式“殺掉”,而不至於導致系統崩潰。
執行緒優先順序
雖然Java執行緒排程是系統自動完成的,但是我們還是可以“建議”系統給某些執行緒多分配一點執行時間,另外的一些執行緒則可以少分配一點——這項操作可以通過設定執行緒優先順序來完成。Java語言一共設定了10個級別的執行緒優先順序(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在兩個執行緒同時處於Ready狀態時,優先順序越高的執行緒越容易被系統選擇執行。不過,執行緒優先順序並不是太靠譜,原因是Java的執行緒是通過對映到系統的原生執行緒上來實現的,所以執行緒排程最終還是取決於作業系統,雖然現在很多作業系統都提供執行緒優先順序的概念,但是並不見得能與Java執行緒的優先順序一一對應,如Solaris中有2147483648(232)種優先順序,但Windows中就只有7種,比Java執行緒優先順序多的系統還好說,中間留下一點空位就可以了,但比Java執行緒優先順序少的系統,就不得不出現幾個優先順序相同的情況了,表12-1顯示了Java執行緒優先順序與Windows執行緒優先順序之間的對應關係,Windows平臺的JDK中使用了除THREAD_PRIORITY_IDLE之外的其餘6種執行緒優先順序。

Linux實現程式執行緒
我們知道系統呼叫fork()可以新建一個子程式,函式pthread()可以新建一個執行緒。
但無論執行緒還是程式,都是用task_struct結構表示的,唯一的區別就是共享的資料區域不同。
一般作業系統都會分為核心態和使用者態,使用者態執行緒之間的地址空間是隔離的,而在核心態,所有執行緒都共享同一核心地址空間。不管是使用者執行緒還是核心執行緒,都和程式一樣,均由作業系統的排程器來統一排程(至少在Linux中是這樣子)。
fork()
在Linux中fork函式是非常重要的函式,它的作用是從已經存在的程式中建立一個子程式,而原程式稱為父程式。
在這裡插入圖片描述
呼叫fork(),當控制轉移到核心中的fork程式碼後,核心開始做:
1.分配新的記憶體塊和核心資料結構給子程式。
2.將父程式部分資料結構內容拷貝至子程式。
3.將子程式新增到系統程式列表。
4.fork返回開始排程器,排程。
•呼叫一次,返回兩次
fork函式被父程式呼叫一次,但是卻返回兩次;一次是返回到父程式,一次是返回到新建立的子程式。
•併發執行
子程式和父程式是併發執行的獨立程式。核心能夠以任意的方式交替執行他們的邏輯控制流中的指令。在我們的系統上執行這個程式時,父程式先執行它的printf語句,然後是子程式。
•相同但是獨立的地址空間
因為父程式和子程式是獨立的程式,他們都有自己私有的地址空間,當父程式或者子程式單獨改變時,不會影響到彼此,類似於c++的寫實拷貝的形式自建一個副本。
•fork的返回值
1.fork的子程式返回為0;
2.父程式返回的是子程式的pid。
•fork的常規用法
1.一個父程式希望複製自己,使得子程式同時執行不同的程式碼段,例如:父程式等待客戶端請求,生成一個子程式來等待請求處理。
2.一個程式要執行一個不同的程式。
•fokr呼叫失敗的原因
1.系統中有太多程式
2.實際使用者的程式數超過限制

在當前bash下執行一個程式發生了什麼?
首先什麼是bash?
對於一個作業系統來說,shell相當於核心kernel外的一層外殼,作為使用者介面。
一般來說,作業系統的介面分為兩類:
CLI:command line interface命令列介面
常見的有:sh csh ksh zsh bash tcsh
GUI:graphical user interface 圖形化使用者介面
常見的有:Gnome KDE Xfce
bash可理解為一種shell的版本(linux預設版本)
bash及其特性:
1、bash實質上是一個可執行程式,一個使用者的工作環境。
2、在每一個shell下可以再開啟一個shell,新開啟的shell可以稱為子shell,每一個shell之間是相互獨立的。
3、可以使用pstree命令檢視當前shell下的子shell個數。
bash也是一個程式,是作業系統和使用者互動的一個程式。
寫在bash這個大程式裡的小程式叫做內部程式
和bash 互相獨立的程式叫做外部程式
所以bash也有自己的環境變數
環境變數又分為自定義變數和環境變數,自定義變數只是適用於當前程式,環境變數是子程式可以繼承的環境變數。
當我們執行一個程式時,這個程式是在bash中執行的。其實是bash用fork函式建立了一個子程式,子程式複製了bash的映像,然後在子程式的映像中呼叫了exec系列的函式,將我們寫的程式的可執行檔案(映像)替換為子程式從父程式那裡複製來的映像。然後執行我們寫的程式的可執行檔案。
fork之後bash下建立了一個子程式,開始這個子程式和bash是共用一個PCB的,當子程式發生改變時,子程式複製了父程式的PCB並呼叫了exec系列函式,將你要執行的程式的程式的PCB替換掉你從父程式那裡複製來的PCB。然後執行你的程式。

如何保證服務程式只有一個例項在執行:

  1. 通過已知的程式名,來查詢是否有同名的程式正在執行。
    可以利用proc,也可以讀取ps的輸出等;
  2. 利用pid檔案,這也是linux各種服務常見的實現方式:
    服務程式啟動的時候,首先在指定目錄下,一般為/var/run/,查詢是否已經存在對應該程式的pid檔案。
    如果已經存在,表明有同樣的程式在執行。但是也許該程式意外崩潰,所以需要進一步檢查。讀取該pid檔案,獲得pid。
    然後再利用確定該pid的程式是否存在。如存在,是否為同名程式。

作業系統中前臺程式與後臺程式(適用於Linux)
後臺程式也叫守護程式(Daemon),是執行在後臺的一種特殊程式。它獨立於控制終端並且週期性地執行某種任務或等待處理某些發生的事件。
一般用作系統服務,可以用crontab提交,編輯或者刪除相應得作業。
守護的意思就是不受終端控制。Linux的大多數伺服器就是用守護程式實現的。比如,Internet伺服器inetd,Web伺服器httpd等。同時,守護程式完成許多系統任務。比如,作業規劃程式crond,列印程式lpd等。
前臺程式就是使用者使用的有控制終端的程式。

兩種程式的主要區別:
1.前臺程式使用者可以操作,和使用者互動,需要較高的響應速度,優先順序別稍微高一點;
後臺程式使用者不能操作(除了把它關閉),基本上不和使用者互動,優先順序別稍微低一點。
2.前臺程式不全是由計算機自動控制,後臺程式全都是由計算機自動控制.
3.後臺程式一般用作系統服務,可以用crontab提交(Linux下),編輯或者刪除相應得作業.

主要特徵:
1.前臺程式可以以視窗,對話匡的形式在系統中顯示.後臺程式不行.
2.在工作列中點亮的程式都可以稱為前臺程式.沒點亮的為後臺程式.
3.前臺程式和後臺程式有時候可以互相轉換.(linux終端中執行的命令後加上&表示後臺命令)
4.一般把較為費時間的命令轉到後臺執行.

程式的幾種狀態:
 建立狀態(new) :程式正在被建立,尚未到就緒狀態。
 就緒狀態(ready) :程式已處於準備執行狀態,即程式獲得了除了處理器之外的一切所需資源,一旦得到處理器資源(處理器分配的時間片)即可執行。
 執行狀態(running) :程式正在處理器上上執行(單核 CPU 下任意時刻只有一個程式處於執行狀態)。
 阻塞狀態(waiting) :又稱為等待狀態,程式正在等待某一事件而暫停執行如等待某資源為可用或等待 IO 操作完成。即使處理器空閒,該程式也不能執行。
 結束狀態(terminated) :程式正在從系統中消失。可能是程式正常結束或其他原因中斷退出執行。

執行緒間的通訊方式:
執行緒間的同步方式
1臨界區
2互斥量
互斥與臨界區很相似,但是使用時相對複雜一些(互斥量為核心物件),不僅可以在同一應用程式的執行緒間實現同步,還可以在不同的程式間實現同步,從而實現資源的安全共享。
3訊號量
訊號量的用法和互斥的用法很相似,不同的是它可以同一時刻允許多個執行緒訪問同一個資源,PV操作
4事件
事件分為手動置位事件和自動置位事件。事件Event內部它包含一個使用計數(所有核心物件都有),一個布林值表示是手動置位事件還是自動置位事件,另一個布林值用來表示事件有無觸發。由SetEvent()來觸發,由ResetEvent()來設成未觸發。

Java中的執行緒通訊的方式有如下:
  1.volatile關鍵字 實現共享變數
基於 volatile 關鍵字來實現執行緒間相互通訊是使用共享記憶體的思想,大致意思就是多個執行緒同時監聽一個變數,當這個變數發生變化的時候 ,執行緒能夠感知並執行相應的業務。這也是最簡單的一種實現方式
  2.Object類的wait() notify()notifyAll()方法
Object類提供了執行緒間通訊的方法:wait()、notify()、notifyaAl(),它們是多執行緒通訊的基礎,而這種實現方式的思想自然是執行緒間通訊。
注意: wait和 notify必須配合synchronized使用,wait方法釋放鎖,notify方法不釋放鎖
  3.CountDownLatch 併發元件 中的wait() 和down()方法
jdk1.5之後在java.util.concurrent包下提供了很多併發程式設計相關的工具類,簡化了我們的併發程式設計程式碼的書寫,CountDownLatch基於AQS框架,相當於也是維護了一個執行緒間共享變數state。
countDownLatch這個類使一個執行緒等待其他執行緒各自執行完畢後再執行。是通過一個計數器來實現的,計數器的初始值是執行緒的數量。只不過這個計數器的操作是原子操作,同時只能有一個執行緒去操作這個計數器,也就是同時只能有一個執行緒去減這個計數器裡面的值。每當一個執行緒執行完畢後,計數器的值就-1,當計數器的值為0時,表示所有執行緒都執行完畢,然後在閉鎖上等待的執行緒就可以恢復工作了。可以向CountDownLatch物件設定一個初始的數字作為計數值,任何呼叫這個物件上的await()方法都會阻塞,直到這個計數器的計數值被其他的執行緒減為0為止。
CountDownLatch的一個非常典型的應用場景是:有一個任務想要往下執行,但必須要等到其他的任務執行完畢後才可以繼續往下執行。假如我們這個想要繼續往下執行的任務呼叫一個CountDownLatch物件的await()方法,其他的任務執行完自己的任務後呼叫同一個CountDownLatch物件上的countDown()方法,這個呼叫await()方法的任務將一直阻塞等待,直到這個CountDownLatch物件的計數值減到0為止。
  4.ReentrantLock和Condition 結合使用
condition.signal();喚醒指導的執行緒
condition.await();對指定的執行緒要求等待中
  5.LockSupport 類中的park()和unpark()方法
LockSupport 是一種非常靈活的實現執行緒間阻塞和喚醒的工具,使用它不用關注是等待執行緒先進行還是喚醒執行緒先執行,但是得知道執行緒的名字。

程式間通訊方式
1.管道( pipe )
通常指無名管道,是 UNIX 系統IPC最古老的形式。
無名管道只能用於具有親緣關係的程式之間,這是它與有名管道的最大區別。有名管道叫named pipe或者FIFO(先進先出),可以用函式mkfifo()建立。
特點:
它是半雙工的(即資料只能在一個方向上流動),具有固定的讀端和寫端。
它只能用於具有親緣關係的程式之間的通訊(也是父子程式或者兄弟程式之間)。
它可以看成是一種特殊的檔案,對於它的讀寫也可以使用普通的read、write 等函式。但是它不是普通的檔案,並不屬於其他任何檔案系統,並且只存在於記憶體中。
單個程式中的管道幾乎沒有任何用處。所以,通常呼叫 pipe 的程式接著呼叫 fork,這樣就建立了父程式與子程式之間的 IPC 通道。
若要資料流從父程式流向子程式,則關閉父程式的讀端(fd[0])與子程式的寫端(fd[1]);反之,則可以使資料流從子程式流向父程式。
pipe()函式建立了管道,並返回了兩個描述符:fd[0]用來從管道讀資料,fd[1]用來向管道寫資料。
管道的結構
在Linux 中,管道的實現並沒有使用專門的資料結構,而是藉助了檔案系統的file結構和VFS的索引節點inode。
將兩個file 結構指向同一個臨時的VFS 索引節點,而這個VFS引節點又指向一個物理頁面而實現的。
管道實際上就是一個file結構和一個VFS的索引。也就是說管道是一個虛擬的檔案。那在Java中就可以通過直接讀寫這個檔案,從而實現PIPE通訊的效果。
File pipe1 = new File(namedPipe1);
BufferedReader reader = new BufferedReader(new FileReader(pipe2));

2.有名管道 (named pipe)
FIFO,也稱為命名管道,它是一種檔案型別。
特點

  1. FIFO可以在無關的程式之間交換資料,與無名管道不同。
  2. FIFO有路徑名與之相關聯,它以一種特殊裝置檔案形式存在於檔案系統中。
    FIFO的通訊方式類似於在程式中使用檔案來傳輸資料,只不過FIFO型別檔案同時具有管道的特性。在資料讀出時,FIFO管道中同時清除資料,並且“先進先出”。
    3.訊號量( semophore )
    訊號量(semaphore)與已經介紹過的 IPC 結構不同,它是一個計數器。訊號量用於實現程式間的互斥與同步,而不是用於儲存程式間通訊資料。
    特點
  3. 訊號量用於程式間同步,若要在程式間傳遞資料需要結合共享記憶體。
  4. 訊號量基於作業系統的 PV 操作,程式對訊號量的操作都是原子操作。
  5. 每次對訊號量的 PV 操作不僅限於對訊號量值加 1 或減 1,而且可以加減任意正整數。
  6. 支援訊號量組。

4.訊息佇列( message queue ) :
訊息佇列是由訊息的連結串列,存放在核心中並由訊息佇列識別符號(即佇列ID)標識。
訊息佇列克服了訊號傳遞資訊少、管道只能承載無格式位元組流以及緩衝區大小受限等缺點。
特點

  1. 訊息佇列是面向記錄的,其中的訊息具有特定的格式以及特定的優先順序。
  2. 訊息佇列獨立於傳送與接收程式。程式終止時,訊息佇列及其內容並不會被刪除。
  3. 訊息佇列可以實現訊息的隨機查詢,訊息不一定要以先進先出的次序讀取,也可以按訊息的型別讀取。
    容量受到系統限制,且要注意第一次讀的時候,要考慮上一次沒有讀完資料的問題。

5.訊號 ( signal ) :
訊號是一種比較複雜的通訊方式,用於通知接收程式某個事件已經發生。

6.[共享記憶體( shared memory )] :下面有介紹,最快的方式
共享記憶體就是對映一段能被其他程式所訪問的記憶體,這段共享記憶體由一個程式建立,但多個程式都可以訪問。共享記憶體是最快的 IPC 方式,它是針對其他程式間通訊方式執行效率低而專門設計的。
共享記憶體並未提供同步機制,也就是說,在第一個程式結束對共享記憶體的寫操作之前,並無自動機制可以阻止第二個程式開始對它進行讀取,所以我們通常需要用其他的機制來同步對共享記憶體的訪問,例如訊號量。它往往與其他通訊機制,如訊號量,配合使用,來實現程式間的同步和通訊。
共享記憶體,指兩個或多個程式共享一個給定的儲存區。
特點:

  1. 共享記憶體是最快的一種 IPC,因為程式是直接對記憶體進行存取。
  2. 因為多個程式可以同時操作,所以需要進行同步。
  3. 訊號量+共享記憶體通常結合在一起使用,訊號量用來同步對共享記憶體的訪問。
    能夠很容易控制容量,速度快,但要保持同步,比如一個程式在寫的時候,另一個程式要注意讀寫的問題,相當於執行緒中的執行緒安全,當然,共享記憶體區同樣可以用作執行緒間通訊,不過沒這個必要,執行緒間本來就已經共享了同一程式內的一塊記憶體。

7.套接字( socket ) :
一種程式間通訊機制,與其他通訊機制不同的是,它可用於不同機器間的程式通訊。
套接字是一種通訊機制,憑藉這種機制,客戶/伺服器(即要進行通訊的程式)系統的開發工作既可以在本地單機上進行,也可以跨網路進行。也就是說它可以讓不在同一臺計算機但通過網路連線計算機上的程式進行通訊。也因為這樣,套接字明確地將客戶端和伺服器區分開來。
套接字通訊的方式非常多,有Unix域套接字、TCP套接字、UDP套接字、鏈路層套接字等等。但最常用的肯定是TCP套接字。
Unix域套接字
Unix域套接字是套接字的一種,用於本機程式間通訊,一般用來實現雙向通訊的管道。Unix域套接字是比網路套接字輕量級且高效的多,因為它不涉及網路通訊,不需要監聽連線,不需要繫結地址,不需要關心協議型別,等等。
建立Unix域套接字後返回兩個檔案描述符,這兩個檔案描述符均對套接字可讀、可寫,從而實現全雙工的雙向通訊。
同樣的,為了避免使用單個檔案描述符同時讀、寫造成的資料錯亂,Unix域套接字也有兩個buffer空間。

共享記憶體
為什麼實現共享記憶體?
採用共享記憶體通訊的一個顯而易見的好處是效率高,因為程式可以直接讀寫記憶體,而不需要任何資料的拷貝。
是最快的一種程式間通訊的方式。
對於像管道和訊息佇列等通訊方式,則需要在核心和使用者空間進行四次的資料拷貝,
而共享記憶體則只拷貝兩次資料:一次從輸入檔案到共享記憶體區,另一次從共享記憶體區到輸出檔案。實際上,程式之間在共享記憶體時,並不總是讀寫少量資料後就解除對映,有新的通訊時,再重新建立共享記憶體區域。而是保持共享區域,直到通訊完畢為止,這樣,資料內容一直儲存在共享記憶體中,並沒有寫回檔案。共享記憶體中的內容往往是在解除對映時才寫回檔案的。
記憶體共享:
兩個不同程式A、B共享記憶體的意思是,同一塊實體記憶體被對映到程式A、B各自的程式地址空間。程式A可以即時看到程式B對共享記憶體中資料的更新,反之亦然。由於多個程式共享同一塊記憶體區域,必然需要某種同步機制,互斥鎖和訊號量都可以。
共享記憶體實現機制
共享記憶體是通過把同一塊記憶體分別對映到不同的程式空間中實現程式間通訊。而共享記憶體本身不帶任何互斥與同步機制,但當多個程式同時對同一記憶體進行讀寫操作時會破壞該記憶體的內容,所以,在實際中,同步與互斥機制需要使用者來完成。
共享記憶體的特點:
(1)共享記憶體就是允許兩個不想關的程式訪問同一個記憶體
(2)共享記憶體是兩個正在執行的程式之間共享和傳遞資料的最有效的方式
(3)不同程式之間共享的記憶體通常安排為同一段實體記憶體
(4)共享記憶體不提供任何互斥和同步機制,一般用訊號量對臨界資源進行保護。
(5)介面簡單
linux實現共享記憶體同步的四種方法:
為了在多個程式間交換資訊,核心專門留出了一塊記憶體區,可以由需要訪問的程式將其對映到自己的私有地址空間。程式就可以直接讀寫這一記憶體區而不需要進行資料的拷貝,從而大大提高的效率。
同步(synchronization)指的是多個任務(執行緒)按照約定的順序相互配合完成一件事情。由於多個程式共享一段記憶體,因此也需要依靠某種同步機制,如互斥鎖和訊號量等 。
訊號燈(semaphore),也叫訊號量。它是不同程式間或一個給定程式內部不同執行緒間同步的機制。訊號燈包括posix有名訊號燈、 posix基於記憶體的訊號燈(無名訊號燈)和System V訊號燈(IPC物件)
方法一、利用POSIX有名訊號燈實現共享記憶體的同步
有名訊號量既可用於執行緒間的同步,又可用於程式間的同步。
兩個程式,對同一個共享記憶體讀寫,可利用有名訊號量來進行同步。
一個程式寫,另一個程式讀,利用兩個有名訊號量semr, semw。semr訊號量控制能否讀,初始化為0。 semw訊號量控制能否寫,初始為1。
方法二、利用POSIX無名訊號燈實現共享記憶體的同步
POSIX無名訊號量是基於記憶體的訊號量,可以用於執行緒間同步也可以用於程式間同步。若實現程式間同步,需要在共享記憶體中來建立無名訊號量。
方法三、利用System V的訊號燈實現共享記憶體的同步
System V的訊號燈是一個或者多個訊號燈的一個集合。其中的每一個都是單獨的計數訊號燈。而Posix訊號燈指的是單個計數訊號燈
System V 訊號燈由核心維護,主要函式semget,semop,semctl 。
一個程式寫,另一個程式讀,訊號燈集中有兩個訊號燈,下標0代表能否讀,初始化為0。 下標1代表能否寫,初始為1。
方法四、利用訊號實現共享記憶體的同步
訊號是在軟體層次上對中斷機制的一種模擬,是一種非同步通訊方式。利用訊號也可以實現共享記憶體的同步。
思路:
reader和writer通過訊號通訊必須獲取對方的程式號,可利用共享記憶體儲存雙方的程式號。
reader和writer執行的順序不確定,可約定先執行的程式建立共享記憶體並初始化。
利用pause, kill, signal等函式可以實現該程式(流程和前邊類似)。

程式排程
處理機排程的層次:
I. 在多道程式系統中,一個作業從提交到執行要經歷多級排程:
作業(JOB):是使用者在一次算題過程中或一次事務處理中,要求計算機系統所做的工作的集合。作業是比程式更廣泛的概念,不僅包含了通常的程式和資料,而且還配有一份作業說明書,系統根據作業說明書對程式執行進行控制。在批處理系統中,以作業為單位從外存調入記憶體。作業提交給系統進入後備狀態後,系統將為每個作業建立一個作業控制塊JCB。JCB在作業的整個執行過程中始終存在,並且其內容與作業的狀態同步地動態變化。只有當作業完成並退出系統時,JCB才被撤消。可以說,JCB是一個作業在系統中存在的唯一標誌,系統根據JCB才感知到作業的存在。

高階排程:作業排程。排程物件是作業,將外存上處於後備佇列的作業調入記憶體,將它們放入就緒佇列中。
低階排程:程式排程。排程物件是程式,讓某些就緒佇列中的程式獲得處理機。
中級排程:記憶體排程。對換功能,將暫時不能執行的程式,調至外存等待,此時程式的狀態為就緒駐外存狀態(或掛起狀態)。當它們已具備執行條件且記憶體有空間時,就把它從外存重新調入記憶體,並修改其狀態為就緒狀態,掛在就緒佇列上等待。
在這裡插入圖片描述
引起程式排程的因素可歸結為:
正在執行的程式執行完畢,或因發生某事件而不能再繼續執行(包括:當前執行程式被中斷、時間片用完了、掛起自己、退出等);
執行中的程式因提出I/O請求而暫停執行;
在程式通訊或同步過程中執行了某種原語操作,如P、V操作原語,Block原語, Wakeup原語等。

程式排程演算法
1) 先來先服務排程演算法:
按照作業/程式進入系統的先後次序進行排程,先進入系統者先排程;即啟動等待時間最長的作業/程式。
2) 時間片輪轉排程法
系統將所有就緒程式按先來先服務原則,排成一個佇列,每次排程時,把CPU分配給隊首程式,並令其執行一個時間片。當時間片用完時,由一個計時器發出時鐘中斷請求,排程程式便根據此訊號來停止該程式的執行,並將它送往就緒佇列的末尾;然後,再把處理機分配給就緒佇列中新的隊首程式,同時也讓它執行一個時間片。保證就緒佇列中的所有程式,在一給定的時間內,均能獲得一個時間片的處理機執行時間,換言之,系統能在給定的時間內,響應所有使用者的請求。
3) 短作業(SJF)優先排程演算法
以要求執行時間長短進行排程,即啟動要求執行時間最短的作業。可以分別用於作業排程和程式排程。
短作業優先(SJF)的排程演算法:是從後備佇列中選擇一個或若干個估計執行時間最短的作業,將它們調入記憶體執行;
短程式優先(SPF)排程演算法:是從就緒佇列中選出一個估計執行時間最短的程式,將處理機分配給它,使它立即執行並一直執行到完成,或發生某事件而被阻塞放棄處理機時,再重新排程。
4)最短剩餘時間優先(搶佔)

5)高響應比優先排程演算法:
R=(w+s)/s (R為響應比,w為等待處理的時間,s為預計的服務時間)
HRRN是一種動態優先權機制,即:隨程式的推進或隨其等待時間的增加而改變,以獲得更好的排程效能。
6)優先順序排程演算法

7)多級反饋佇列排程演算法的實現思想如下:
應設定多個就緒佇列,併為各個佇列賦予不同的優先順序,第1級佇列的優先順序最高,第2級佇列次之,其餘佇列的優先順序逐次降低。
賦予各個佇列中程式執行時間片的大小也各不相同,在優先順序越高的佇列中,每個程式的執行時間片就越小。例如,第2級佇列的時間片要比第1級佇列的時間片長一倍, ……第i+1級佇列的時間片要比第i級佇列的時間片長一倍。
當一個新程式進入記憶體後,首先將它放入第1級佇列的末尾,按FCFS原則排隊等待排程。當輪到該程式執行時,如它能在該時間片內完成,便可準備撤離系統;如果它在一個時間片結束時尚未完成,排程程式便將該程式轉入第2級佇列的末尾,再同樣地按FCFS 原則等待排程執行;如果它在第2級佇列中執行一個時間片後仍未完成,再以同樣的方法放入第3級佇列……如此下去,當一個長程式從第1級佇列依次降到第 n 級佇列後,在第 n 級佇列中便釆用時間片輪轉的方式執行。
僅當第1級佇列為空時,排程程式才排程第2級佇列中的程式執行;僅當第1 ~ (i-1)級佇列均為空時,才會排程第i級佇列中的程式執行。如果處理機正在執行第i級佇列中的某程式時,又有新程式進入優先順序較高的佇列(第 1 ~ (i-1)中的任何一個佇列),則此時新程式將搶佔正在執行程式的處理機,即由排程程式把正在執行的程式放回到第i級佇列的末尾,把處理機分配給新到的更高優先順序的程式。
多級反饋佇列的優勢有:
終端型作業使用者:短作業優先。
短批處理作業使用者:週轉時間較短。
長批處理作業使用者:經過前面幾個佇列得到部分執行,不會長期得不到處理。

孤兒程式
一個父程式退出,而它的一個或多個子程式還在執行,那麼那些子程式將成為孤兒程式。孤兒程式將被init程式(程式號為1)所收養,並由init程式對它們完成狀態收集工作。
殭屍狀態
是一個比較特殊的狀態,當程式退出父程式(使用wait()系統呼叫)沒有讀取到子程式退出的返回程式碼時就會產生殭屍程式。殭屍程式會在以終止狀態保持在程式表中,並且會一直等待父程式讀取退出狀態程式碼。
殭屍程式與孤兒程式的區別:
孤兒程式是子程式還在執行,而父程式掛了,子程式被init程式收養。
殭屍程式是父程式還在執行但是子程式掛了,但是父程式卻沒有使用wait來清理子程式的程式資訊,導致子程式雖然執行實體已經消失,但是仍然在核心的程式表中佔據一條記錄,這樣長期下去對於系統資源是一個浪費。殭屍程式將會導致資源浪費,而孤兒則不會。

殭屍程式
子程式結束了,父程式沒有對其進行回收,這時它就是殭屍程式。
如果子程式先結束而父程式後結束,即子程式結束後,父程式還在繼續執行但是並未呼叫wait/waitpid那子程式就會成為殭屍程式。
但如果子程式後結束,即父程式先結束了,但沒有呼叫wait/waitpid來等待子程式的結束,此時子程式還在執行,父程式已經結束。那麼並不會產生殭屍程式。因為每個程式結束時,系統都會掃描當前系統中執行的所有程式,看看有沒有哪個程式時剛剛結束的這個程式的子程式,如果有,就有init來接管它,成為它的父程式。
同樣的在產生殭屍程式的那種情況下,即子程式結束了但父程式還在繼續執行(並未呼叫wait/waitpid)這段期間,假如父程式異常終止了,那麼該子程式就會自動被init接管。那麼它就不再是殭屍程式了。應為intit會發現並釋放它所佔有的資源。

這樣就導致了一個問題,如果沒有呼叫wait/waitpid的話,那麼保留的資訊就不會釋放。比如程式號就會被一直佔用了。但系統所能使用的程式號的有限的,如果產生大量的殭屍程式,將導致系統沒有可用的程式號而導致系統不能建立程式。所以我們應該避免殭屍程式。
殭屍程式的避免:
⒈如果父程式並不是很繁忙我們就可以通過直接呼叫wait/waitpid來等待子程式的結束。當然這會導致父程式被掛起。比如:子程式先結束,父程式呼叫wait等待子程式,父程式迴圈結束後並不會結束,而是被掛起等待子程式的結束。

⒉ 如果父程式很忙,那麼可以用signal函式為SIGCHLD安裝handler,因為子程式結束後, 父程式會收到該訊號,可以在handler中呼叫wait回收。
使用訊號函式sigaction為SIGCHLD設定wait處理函式。這樣子程式結束後,父程式就會收到子程式結束的訊號並呼叫wait回收子程式的資源

⒊ 如果父程式不關心子程式什麼時候結束,那麼可以用signal(SIGCHLD,SIG_IGN) 通知核心,自己對子程式的結束不感興趣,那麼子程式結束後,核心會回收, 並不再給父程式傳送訊號。

⒋ 還有一些技巧,就是fork兩次,父程式fork一個子程式,然後繼續工作,子程式fork一 個孫程式後退出,那麼孫程式被init接管,孫程式結束後,init會回收。不過子程式的回收 還要自己做。
原理是將子程式成為孤兒程式,從而其的父程式變為init程式,通過init程式可以處理殭屍程式

死鎖

如果一組程式中的每一個程式都在等待僅由該組程式中的其他程式才能引發的事件,那麼該組程式就是死鎖的。或者在兩個或多個併發程式中,如果每個程式持有某種資源而又都等待別的程式釋放它或它們現在保持著的資源,在未改變這種狀態之前都不能向前推進,稱這一組程式產生了死鎖。通俗地講,就是兩個或多個程式被無限期地阻塞、相互等待的一種狀態。
產生死鎖的原因
竟爭資源
程式間推進順序不當
產生死鎖的必要條件(4)

  • 互斥:資源不能被共享,只能由一個程式使用。
  • 請求與保持:已經得到資源的程式可以再次申請新的資源。
  • 非搶佔:已經分配的資源不能從相應的程式中被強制地剝奪。
  • 迴圈等待:系統中若干程式組成環路,該環路中每個程式都在等待相鄰程式正佔用的資源。

如何避免死鎖
由於在避免死鎖的策略中,允許程式動態地申請資源。因而,系統在進行資源分配之前預先計算資源分配的安全性。若此次分配不會導致系統進入不安全的狀態,則將資源分配給程式;否則,程式等待。其中最具有代表性的避免死鎖演算法是銀行家演算法。
銀行家演算法:首先需要定義狀態和安全狀態的概念。系統的狀態是當前給程式分配的資源情況。因此,狀態包含兩個向量Resource(系統中每種資源的總量)和Available(未分配給程式的每種資源的總量)及兩個矩陣Claim(表示程式對資源的需求)和Allocation(表示當前分配給程式的資源)。安全狀態是指至少有一個資源分配序列不會導致死鎖。當程式請求一組資源時,假設同意該請求,從而改變了系統的狀態,然後確定其結果是否還處於安全狀態。如果是,同意這個請求;如果不是,阻塞該程式知道同意該請求後系統狀態仍然是安全的。

臨界資源
對於某些資源來說,其在同一時間只能被一個程式所佔用。這些一次只能被一個程式所佔用的資源就是所謂的臨界資源。對於臨界資源的訪問,必須是互斥進行。
臨界區
程式內訪問臨界資源的程式碼被成為臨界區。

PV
P是通過(-1),V是釋放(+1)
PV操作是一種實現程式互斥與同步的有效方法。

管程
管程在功能上和訊號量及PV操作類似,屬於一種程式同步互斥工具,但是具有與訊號量及PV操作不同的屬性。
管程,指的是管理共享變數以及對其操作過程,讓它們支援併發訪問。
它是將共享的變數和對於這些共享變數的操作封裝起來,形成一個具有一定介面的功能模組,程式可以呼叫管程來實現程式級別的併發控制。
程式只能互斥得使用管程,即當一個程式使用管程時,另一個程式必須等待。當一個程式使用完管程後,它必須釋放管程並喚醒等待管程的某一個程式。
在管程入口處的等待佇列稱為入口等待佇列,由於程式會執行喚醒操作,因此可能有多個等待使用管程的佇列,這樣的佇列稱為緊急佇列,它的優先順序高於等待佇列。

執行緒安全
如果你的程式碼所在的程式中有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。如果每次執行結果和單執行緒執行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。或者說:一個類或者程式所提供的介面對於執行緒來說是原子操作或者多個執行緒之間的切換不會導致該介面的執行結果存在二義性,也就是說我們不用考慮同步的問題。

執行緒安全問題都是由全域性變數及靜態變數引起的。
若每個執行緒中對全域性變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全域性變數是執行緒安全的;若有多個執行緒同時執行寫操作,一般都需要考慮執行緒同步,否則的話就可能影響執行緒安全。

記憶體儲存管理

記憶體分配:
為了能將使用者程式裝入記憶體,必須為它分配一定大小的記憶體空間。

記憶體管理有哪幾種方式?
儲存器分配方式有
連續分配儲存管理方式 和 離散分配儲存管理方式 兩種。

連續分配
是指為一個使用者程式分配連續的記憶體空間。
連續分配有單一連續儲存管理和分割槽式儲管理兩種方式。
分割槽式儲存管理:
為了支援多道程式系統和分時系統,支援多個程式併發執行,引入了分割槽式儲存管理。分割槽式儲存管理是把記憶體分為一些大小相等或不等的分割槽,作業系統佔用其中一個分割槽,其餘的分割槽由應用程式使用,每個應用程式佔用一個或幾個分割槽。分割槽式儲存管理雖然可以支援併發,但難以進行記憶體分割槽的共享。
分割槽式儲存管理引人了兩個新的問題:內碎片和外碎片
內部碎片是已經被分配出去的的記憶體空間大於請求所需的記憶體空間。
外部碎片是指還沒有分配出去,但是由於大小太小而無法分配給申請空間的新程式的記憶體空間空閒塊。

離散分配:
在前面的幾種儲存管理方法中,為程式分配的空間是連續的,使用的地址都是實體地址。如果允許將一個程式分散到許多不連續的空間,就可以避免記憶體緊縮,減少碎片。基於這一思想,通過引入程式的邏輯地址,把程式地址空間與實際儲存空間分離,增加儲存管理的靈活性。
為了解決碎片問題,又要進行緊湊等高開銷的活動,人們引入了離散分配方式,即:程式在記憶體中不一定連續存放。這是一種基於離散分配方式的分頁和分段機制的虛擬記憶體機制。
根據離散分配的基本單位不同,分為:分頁儲存管理、分段儲存管理、段頁式儲存管理。

地址空間:將源程式經過編譯後得到的目標程式,存在於它所限定的地址範圍內,這個範圍稱為地址空間。地址空間是邏輯地址的集合。
儲存空間:指主存中一系列儲存資訊的物理單元的集合,這些單元的編號稱為實體地址儲存空間是實體地址的集合。

目的:為了利用和管理好計算機的資源–記憶體
根據分配時所採用的基本單位不同,可將離散分配的管理方式分為以下三種:
頁式儲存管理、段式儲存管理和段頁式儲存管理。其中段頁式儲存管理是前兩種結合的產物。

分頁儲存管理:
分段就是將一個程式分成程式碼段,資料段,堆疊段什麼的;
分頁就是將這些段,例如程式碼段分成均勻的小塊(頁),然後這些給這些小塊編號,然後就可以放到記憶體中去,由於編號了的,所以也不怕順序亂。
然後我們就能通過段號,頁號,頁內偏移找到程式的地址。
分頁系統能有效地提高記憶體的利用率,而分段系統能反映程式的邏輯結構,便於段的共享與保護,將分頁與分段兩種儲存方式結合起來,就形成了段頁式儲存管理方式。

將程式的邏輯地址空間劃分為固定大小的頁(page),而實體記憶體劃分為同樣大小的頁框(page frame)。程式載入時,可將任意一頁放人記憶體中任意一個頁框,這些頁框不必連續,從而實現了離散分配。
在這裡插入圖片描述
在這裡插入圖片描述
在分頁儲存管理方式中,任一個邏輯地址都可轉變為:頁號+頁內偏移量。
分頁系統中處理機每次存取指令或資料至少需要訪問兩次實體記憶體:
第一次訪問頁表,以得到實體地址
第二次訪問實體地址,以得到資料。
邏輯地址=邏輯頁號x頁的大小+頁偏移量
實體地址=物理塊號x頁的大小+頁偏移量
存取速度幾乎降低了一倍,代價太高!(引入快表)
邏輯(虛擬)地址和實體地址
我們程式設計一般只有可能和邏輯地址打交道,比如在 C 語言中,指標裡面儲存的數值就可以理解成為記憶體裡的一個地址,這個地址也就是我們說的邏輯地址,邏輯地址由作業系統決定。實體地址指的是真實實體記憶體中地址,更具體一點來說就是記憶體地址暫存器中的地址。實體地址是記憶體單元真正的地址。

快表和多級頁表
在分頁記憶體管理中,很重要的兩點是:
虛擬地址到實體地址的轉換要快。
解決虛擬地址空間大,頁表也會很大的問題。
快表:
為了解決虛擬地址到實體地址的轉換速度,作業系統在 頁表方案 基礎之上引入了 快表 來加速虛擬地址到實體地址的轉換。
快表的工作原理類似於系統中的 資料快取記憶體,其中專門儲存當前程式 最近訪問過的一組頁表項。程式最近訪問過的頁面在不久的將來還可能被訪問。
我們可以把塊表理解為一種特殊的高速緩衝儲存器(Cache),其中的內容是頁表的一部分或者全部內容。作為頁表的 Cache,它的作用與頁表相似,但是提高了訪問速率。由於採用頁表做地址轉換,讀寫記憶體資料時 CPU 要訪問兩次主存。有了快表,有時只要訪問一次高速緩衝儲存器,一次主存,這樣可加速查詢並提高指令執行速度。
使用快表之後的地址轉換流程是這樣的:

  1. 根據虛擬地址(邏輯地址)中的頁號查快表中是否存在對應的頁表項;
  2. 如果該頁在快表中,稱為命中,取出其中的頁框號,加上頁內偏移量,計算出實體地址;
  3. 如果該頁不在快表中,稱為命中失敗,就訪問記憶體中的頁表,找到邏輯地址中指定頁號對應的頁框號。同時,更新快表,將該表項插入快表中。並計算實體地址;
  4. 當快表填滿後,又要登記新頁時,就按照一定的淘汰策略淘汰掉快表中的一個頁。

看完了之後你會發現快表和我們平時經常在我們開發的系統使用的快取(比如 Redis)很像,的確是這樣的,作業系統中的很多思想、很多經典的演算法,你都可以在我們日常開發使用的各種工具或者框架中找到它們的影子。
多級頁表:
引入多級頁表的主要目的是為了避免把全部頁表一直放在記憶體中佔用過多空間,特別是那些根本就不需要的頁表就不需要保留在記憶體中。
總結
為了提高記憶體的空間效能,提出了多級頁表的概念;但是提到空間效能是以浪費時間效能為基礎的,因此為了補充損失的時間效能,提出了快表(即 TLB)的概念。 不論是快表還是多級頁表實際上都利用到了程式的區域性性原理,區域性性原理在後面的虛擬記憶體這部分會介紹到。

頁面置換演算法(缺頁排程演算法):
I. OPT - 最佳置換演算法
演算法思想:從主存中移出永遠不再需要的頁面;如無這樣的頁面存在,則應選擇最長時間不需要訪問的頁面。
最佳置換策略本身不是一種實際的方法,因為頁面訪問的未來順序是不知道的,但是,可將其它的實用方法與之比較來評價這些方法的優劣。所以,這種最佳策略具有理論上的意義。
II. FIFO - 先進先出頁面置換演算法
演算法思想:總是選擇作業中駐留時間最長(即最老)的一頁淘汰。即:先進入主存的頁面先退出主存 。
III. LRU - 最近最久未使用置換演算法
演算法思想:利用區域性性原理,根據一個作業在執行過程中過去的頁面訪問蹤跡來推測未來的行為。它認為過去一段時間裡不曾被訪問過的頁面,在最近的將來可能也不會再被訪問。即:當需要置換一頁面時,選擇在最近一段時間內最久不用的頁面予以淘汰。
IV. LFU - 最近最少使用置換演算法
演算法思想:選擇到當前時間為止被訪問次數最少的頁面被置換。
LRU實現:
一個雜湊表和一個雙向連結串列
在這裡插入圖片描述
LRU 快取機制可以通過雜湊表輔以雙向連結串列實現,我們用一個雜湊表和一個雙向連結串列維護所有在快取中的鍵值對。
雙向連結串列按照被使用的順序儲存了這些鍵值對,靠近頭部的鍵值對是最近使用的,而靠近尾部的鍵值對是最久未使用的。
雜湊表即為普通的雜湊對映(HashMap),通過快取資料的鍵對映到其在雙向連結串列中的位置。
這樣以來,我們首先使用雜湊表進行定位,找出快取項在雙向連結串列中的位置,隨後將其移動到雙向連結串列的頭部,即可在 O(1) 的時間內完成 get 或者 put 操作。具體的方法如下:
對於 get 操作,首先判斷 key 是否存在:
如果 key 不存在,則返回 -1;
如果 key 存在,則 key 對應的節點是最近被使用的節點。通過雜湊表定位到該節點在雙向連結串列中的位置,並將其移動到雙向連結串列的頭部,最後返回該節點的值。
對於 put 操作,首先判斷 key 是否存在:
如果 key 不存在,使用 key 和 value 建立一個新的節點,在雙向連結串列的頭部新增該節點,並將 key 和該節點新增進雜湊表中。然後判斷雙向連結串列的節點數是否超出容量,如果超出容量,則刪除雙向連結串列的尾部節點,並刪除雜湊表中對應的項;
如果 key 存在,則與 get 操作類似,先通過雜湊表定位,再將對應的節點的值更新為 value,並將該節點移到雙向連結串列的頭部。
上述各項操作中,訪問雜湊表的時間複雜度為 O(1),在雙向連結串列的頭部新增節點、在雙向連結串列的尾部刪除節點的複雜度也為 O(1)。而將一個節點移到雙向連結串列的頭部,可以分成「刪除該節點」和「在雙向連結串列的頭部新增節點」兩步操作,都可以在 O(1) 時間內完成。

缺頁中斷
指的是當軟體試圖訪問已對映在虛擬地址空間中,但是並未被載入在實體記憶體中的一個分頁時,由中央處理器的記憶體管理單元所發出的中斷。
請求分頁的系統當中,可以查詢頁表當前的狀態位來查詢當前頁是否在記憶體當中,如果不在記憶體當中可以通過頁表當中的外存地址將缺的一頁讀到記憶體當中。
步驟:
保護cpu現場
分析中斷原因
轉入缺頁中斷處理函式
恢復cpu現場,繼續執行
缺頁中斷與一般中斷的區別;
1、一般中斷只需要保護現場然後就直接跳到需及時處理的地方。
2、缺頁中斷除了保護現場之外,還要判斷記憶體中是否有足夠的空間儲存所需的頁或段,然後再把所需頁調進來再使用。

系統抖動
含義:在請求分頁儲存管理中,從主存(DRAM)中剛剛換出(Swap Out)某一頁面後(換出到Disk),根據請求馬上又換入(Swap In)該頁,這種反覆換出換入的現象,稱為系統顛簸,也叫系統抖動。產生該現象的主要原因是置換演算法選擇不當。
如果系統花費大量的時間把程式和資料 頻繁地換入和換出 記憶體而不是執行使用者指令,那麼,稱系統出現了抖動。
出現抖動現象時,系統顯得非常繁忙,但是吞吐量很低,甚至產出為零。
根本原因:選擇的頁面或段不恰當。
1.如果分配給程式的儲存塊數量小於程式所需要的最小值,程式的執行將很頻繁地產生缺頁中斷,這種頻率非常高的頁面置換現象稱為抖動。解決方案優化置換演算法。
2.在請求分頁儲存管理中,可能出現這種情況,即對剛被替換出去的頁,立即又要被訪問。需要將它調入,因無空閒記憶體又要替換另一頁,而後者又是即將被訪問的頁,於是造成了系統需花費大量的時間忙於進行這種頻繁的頁面交換,致使系統的實際效率很低,嚴重導致系統癱瘓,這種現象稱為抖動現象。

分段儲存管理:
在段式儲存管理中,將程式的地址空間劃分為若干個段(segment),這樣每個程式有一個二維的地址空間。在前面所介紹的動態分割槽分配方式中,系統為整個程式分配一個連續的記憶體空間。而在段式儲存管理系統中,則為每個段分配一個連續的分割槽,而程式中的各個段可以不連續地存放在記憶體的不同分割槽中。程式載入時,作業系統為所有段分配其所需記憶體,這些段不必連續,實體記憶體的管理採用動態分割槽的管理方法。
在分段系統中,任意一個邏輯地址則由所在段的段名和段內地址組成。

理解:
分段就是將一個程式分成程式碼段,資料段,堆疊段什麼的;
分頁就是將這些段,例如程式碼段分成均勻的小塊(頁),然後這些給這些小塊編號,然後就可以放到記憶體中去,由於編號了的,所以也不怕順序亂。
然後我們就能通過段號,頁號,頁內偏移找到程式的地址。
分頁系統能有效地提高記憶體的利用率,而分段系統能反映程式的邏輯結構,便於段的共享與保護,將分頁與分段兩種儲存方式結合起來,就形成了段頁式儲存管理方式。

分頁分段的區別:
可見與不可見
分頁是系統活動,使用者無法介入,頁的大小固定;
分段是使用者可見的,段大小可變。
物理單位與邏輯單位
頁是資訊的物理單位,不是完整的邏輯單位;
段是完整的邏輯資訊單位。
地址空間
分頁的使用者程式地址空間是一維的,是單一線性空間;
分段的使用者程式地址空間是二維的。
分頁 ––– 是為了提高記憶體利用率,是系統管理的需要。
分段 ––– 是為了更好地滿足使用者需要。
分頁 ––– 使用者不關心(頁的長度由機器地址結構決定)
分段 ––– 使用者或編輯程式確定(段的最大長度由位移量欄位的位數決定)
分頁——實現程式段的共享較為困難。
分段——易於實現段的共享和段的保護。
共同點 :
分頁機制和分段機制都是為了提高記憶體利用率,較少記憶體碎片。
頁和段都是離散儲存的,所以兩者都是離散分配記憶體的方式。但是,每個頁和段中的記憶體是連續的。
區別 :
頁的大小是固定的,由作業系統決定;而段的大小不固定,取決於我們當前執行的程式。
分頁僅僅是為了滿足作業系統記憶體管理的需求,而段是邏輯資訊的單位,在程式中可以體現為程式碼段,資料段,能夠更好滿足使用者的需要。

段頁式儲存管理:
分頁管理記憶體管理效率高,沒有外部碎片,內部碎片小;
分段管理符合模組化思想,每個分段都具備完整的功能,方便程式碼共享、保護,沒有內部碎片,存在外部碎片。
段頁式儲存管理的原理:分段和分頁相結合。
先將使用者程式分段,每段內再劃分成若干頁,每段有段名(段號),每段內部的頁有一連續的頁號。
記憶體劃分:按頁式儲存管理方案。
記憶體分配:以頁為單位進行離散分配。
邏輯地址結構:由於段頁式系統給作業地址空間增加了另一級結構,現在地址空間是由段號、段內頁號和頁內偏移量構成。

虛擬記憶體:
實體記憶體
實體記憶體指通過實體記憶體條而獲得的記憶體空間。
虛擬記憶體
這個在我們平時使用電腦特別是 Windows 系統的時候太常見了。很多時候我們使用點開了很多佔記憶體的軟體,這些軟體佔用的記憶體可能已經遠遠超出了我們電腦本身具有的實體記憶體。為什麼可以這樣呢? 正是因為 虛擬記憶體 的存在,通過 虛擬記憶體 可以讓程式可以擁有超過系統實體記憶體大小的可用記憶體空間。另外,虛擬記憶體為每個程式提供了一個一致的、私有的地址空間,它讓每個程式產生了一種自己在獨享主存的錯覺(每個程式擁有一片連續完整的記憶體空間)。這樣會更加有效地管理記憶體並減少出錯。
虛擬記憶體是計算機系統記憶體管理的一種技術,我們可以手動設定自己電腦的虛擬記憶體。不要單純認為虛擬記憶體只是“使用硬碟空間來擴充套件記憶體“的技術。虛擬記憶體的重要意義是它定義了一個連續的虛擬地址空間,並且 把記憶體擴充套件到硬碟空間。
虛擬記憶體是計算機系統記憶體管理的一種技術。 它使得應用程式認為它擁有連續的可用的記憶體(一個連續完整的地址空間)。而實際上,虛擬記憶體通常是被分隔成多個實體記憶體碎片,還有部分暫時儲存在外部磁碟儲存器上,在需要時進行資料交換,載入到實體記憶體中來。 目前,大多數作業系統都使用了虛擬記憶體,如 Windows 系統的虛擬記憶體、Linux 系統的交換空間等等。
離開程式談虛擬記憶體沒有任何意義,不同程式裡的同一個虛擬地址指向的實體地址是不一樣的。每個使用者程式維護了一個單獨的頁表(Page Table),虛擬記憶體和實體記憶體就是通過這個頁表實現地址空間的對映的,頁表(Page Table)裡面的資料由作業系統維護。
引入虛擬記憶體的好處
在程式和實體記憶體之間,加了一層虛擬記憶體的概念,好處有:

  1. 提供更大的地址空間,因為虛擬記憶體還可以放在磁碟上或者暫存器中,而實體記憶體並不行,而且虛擬地址空間是連續的,我們不需要操心具體是如何存放的,作業系統會幫我們對映好;
  2. 安全性更好,虛擬記憶體設有讀寫屬性,並且不同程式互不影響;
  3. 可以懶載入,只有在需要讀相應的檔案的時候,才將它真正的從磁碟上載入到記憶體中來,而在記憶體吃緊的時候又可以將這部分記憶體清空掉,提高實體記憶體利用效率,並且所有這些對應用程式是都透明的;
  4. 可以共享記憶體,動態庫只需要在記憶體中存一份就夠了,然後將它對映到不同程式的虛擬地址空間中,讓程式覺得自己獨佔了這個檔案。程式間的記憶體共享也可以通過對映同一塊實體記憶體到程式的不同虛擬地址空間來實現共享。

區域性性原理
區域性性原理是虛擬記憶體技術的基礎,正是因為程式執行具有區域性性原理,才可以只裝入部分程式到記憶體就開始執行。
區域性性原理表現在以下兩個方面:
時間區域性性 :如果程式中的某條指令一旦執行,不久以後該指令可能再次執行;如果某資料被訪問過,不久以後該資料可能再次被訪問。產生時間區域性性的典型原因,是由於在程式中存在著大量的迴圈操作。
空間區域性性 :一旦程式訪問了某個儲存單元,在不久之後,其附近的儲存單元也將被訪問,即程式在一段時間內所訪問的地址,可能集中在一定的範圍之內,這是因為指令通常是順序存放、順序執行的,資料也一般是以向量、陣列、表等形式簇聚儲存的。
時間區域性性是通過將近來使用的指令和資料儲存到快取記憶體儲存器中,並使用快取記憶體的層次結構實現。空間區域性性通常是使用較大的快取記憶體,並將預取機制整合到快取記憶體控制邏輯中實現。虛擬記憶體技術實際上就是建立了 “記憶體一外存”的兩級儲存器的結構,利用區域性性原理實現髙速快取。

實現:
虛擬記憶體的實現需要建立在離散分配的記憶體管理方式的基礎上。
虛擬記憶體的實現有以下三種方式:

  1. 請求分頁儲存管理 :建立在分頁管理之上,為了支援虛擬儲存器功能而增加了請求調頁功能和頁面置換功能。請求分頁是目前最常用的一種實現虛擬儲存器的方法。請求分頁儲存管理系統中,在作業開始執行之前,僅裝入當前要執行的部分段即可執行。假如在作業執行的過程中發現要訪問的頁面不在記憶體,則由處理器通知作業系統按照對應的頁面置換演算法將相應的頁面調入到主存,同時作業系統也可以將暫時不用的頁面置換到外存中。
  2. 請求分段儲存管理 :建立在分段儲存管理之上,增加了請求調段功能、分段置換功能。請求分段儲存管理方式就如同請求分頁儲存管理方式一樣,在作業開始執行之前,僅裝入當前要執行的部分段即可執行;在執行過程中,可使用請求調入中斷動態裝入要訪問但又不在記憶體的程式段;當記憶體空間已滿,而又需要裝入新的段時,根據置換功能適當調出某個段,以便騰出空間而裝入新的段。
  3. 請求段頁式儲存管理
    請求分頁儲存管理不要求將作業全部地址空間同時裝入主存。基於這一點,請求分頁儲存管理可以提供虛存,而分頁儲存管理卻不能提供虛存。

不管是上面那種實現方式,我們一般都需要:
一定容量的記憶體和外存:在載入程式的時候,只需要將程式的一部分裝入記憶體,而將其他部分留在外存,然後程式就可以執行了;
缺頁中斷:如果需執行的指令或訪問的資料尚未在記憶體(稱為缺頁或缺段),則由處理器通知作業系統將相應的頁面或段調入到記憶體,然後繼續執行程式;
虛擬地址空間 :邏輯地址到實體地址的變換。

核心空間和使用者空間:
為了避免使用者直接操作核心「可以操作一切,牛逼得很」,保證核心安全,所以將虛擬記憶體劃分為使用者空間和核心空間,程式在訪問到這兩個空間的時候需要進行狀態的轉變(核心態、使用者態)。
這裡不考慮實體記憶體怎麼劃分的,因為程式一般直接訪問虛擬記憶體!
像我們在 jvm 中談到的,堆、棧、方法區等等都是預設是使用者空間,因為我們可以直接訪問的到。
核心態 & 使用者態
核心態可以執行任意命令,呼叫系統的一切資源,而使用者態只能執行簡單的運算,不能直接呼叫系統資源。使用者態必須通過系統介面(System Call),才能向核心發出指令。比如,當使用者程式啟動一個 bash 時,它會通過 getpid() 對核心的 pid 服務發起系統呼叫,獲取當前使用者程式的 ID;當使用者程式通過 cat 命令檢視主機配置時,它會對核心的檔案子系統發起系統呼叫。

我們都知道unix(like)世界裡,一切皆檔案,而檔案是什麼呢?檔案就是一串二進位制流而已,不管socket,還是FIFO、管道、終端,對我們來說,一切都是檔案,一切都是流。在資訊 交換的過程中,我們都是對這些流進行資料的收發操作,簡稱為I/O操作(input and output),往流中讀出資料,系統呼叫read,寫入資料,系統呼叫write。不過話說回來了 ,計算機裡有這麼多的流,我怎麼知道要操作哪個流呢?對,就是檔案描述符,即通常所說的fd,一個fd就是一個整數,所以,對這個整數的操作,就是對這個檔案(流)的操作。我們建立一個socket,通過系統呼叫會返回一個檔案描述符,那麼剩下對socket的操作就會轉化為對這個描述符的操作。不能不說這又是一種分層和抽象的思想。

I/O 模型(socket)

一個輸入操作通常包括兩個階段:
• 等待資料準備好
• 從核心向程式複製資料
在這裡插入圖片描述
對於一個套接字上的輸入操作,第一步通常涉及等待資料從網路中到達。當所等待資料到達時,它被複制到核心中的某個緩衝區。第二步就是把資料從核心緩衝區複製到應用程式緩衝區。
通俗的講,將IO分為兩步:
1.等;
2.資料搬遷。
如果要想提高IO效率,需要將等的時間降低。
Unix 有五種 I/O 模型:
• 阻塞式 I/O
• 非阻塞式 I/O
• I/O 複用(select 和 poll)
• 訊號驅動式 I/O(SIGIO)
• 非同步 I/O(AIO)

同步 I/O:
將資料從核心緩衝區複製到應用程式緩衝區的階段(上述過程中第二階段),應用程式會阻塞。
同步 I/O 包括阻塞式 I/O、非阻塞式 I/O、I/O 複用和訊號驅動 I/O ,它們的主要區別在第一個階段。
非阻塞式 I/O 、訊號驅動 I/O 和非同步 I/O 在第一階段不會阻塞。
非同步 I/O:
第二階段應用程式不會阻塞。
五種IO總結:
在這裡插入圖片描述

阻塞式I/O
在這裡插入圖片描述
我們最熟悉的I/O模型就是阻塞式I/O模型,在上圖中,應用程式系統呼叫recvfrom接收資料,但是此時核心緩衝區中資料包還未準備好,所以應用程式會一直阻塞直到核心緩衝區有資料包到達且被複制到應用程式緩衝區
應用程式被阻塞,直到資料從核心緩衝區複製到應用程式緩衝區中才返回。
這裡我們提一下這裡的核心緩衝區和應用程式緩衝區所處的階段
一個輸入操作通常包括兩個不同的階段:

  1. 等待資料包準備好 (通常是等待資料從網路上到達,當所等待的分組資料到達後,會被複制到核心的某個緩衝區中)
  2. 從核心向程式複製資料 (把資料從核心緩衝區複製到應用程式緩衝區中)

非阻塞式I/O
應用程式執行系統呼叫之後,核心返回一個錯誤碼。應用程式可以繼續執行,但是需要不斷的執行系統呼叫來獲知 I/O 是否完成,這種方式稱為輪詢(polling)。
由於 CPU 要處理更多的系統呼叫,因此這種模型的 CPU 利用率比較低。
舉例:B也在河邊釣魚,但是B不想將自己的所有時間都花費在釣魚上,在等魚上鉤這個時間段中,B也在做其他的事情,但每隔一個固定的時間檢查魚是否上鉤。一旦檢查到有魚上鉤,就停下手中的事情,把魚釣上來。
在這裡插入圖片描述
檢視上圖可知,在設定連線為非阻塞時,當應用程式系統呼叫recvfrom沒有資料返回時,核心會立即返回一個EWOULDBLOCK錯誤,而不會一直阻塞到資料準備好。如上圖在第四次呼叫時有一個資料包準備好了,所以這時資料會被複制到應用程式緩衝區,於是recvfrom成功返回資料
當一個應用程式這樣迴圈呼叫recvfrom時,我們稱之為輪詢polling。這麼做往往會耗費大量CPU時間,實際使用很少。

I/O 複用
在這裡插入圖片描述
IO多路轉接是多了一個select函式,select函式有一個引數是檔案描述符集合,對這些檔案描述符進行迴圈監聽,當某個檔案描述符就緒時,就對這個檔案描述符進行處理。
I/O多路複用就是通過一種機制,一個程式可以監視多個檔案描述符,一旦某個描述符就緒(讀就緒或寫就緒),能夠通知程式進行相應的讀寫操作 。
其中,select只負責等,recvfrom只負責拷貝。
IO多路轉接是屬於阻塞IO,但可以對多個檔案描述符進行阻塞監聽,所以效率較阻塞IO的高。
在這裡插入圖片描述
• (1)當使用者程式呼叫了select,那麼整個程式會被block;
• (2)而同時,kernel會“監視”所有select負責的socket;
• (3)當任何一個socket中的資料準備好了,select就會返回;
• (4)這個時候使用者程式再呼叫read操作,將資料從kernel拷貝到使用者程式(空間)。
select/epoll的優勢並不是對於單個連線能處理得更快,而是在於能處理更多的連線。
Linux I/O複用模型提供了select poll epoll三組系統呼叫可做選擇,程式通過將一個或多個檔案描述符(fd)傳遞給select或poll或epoll系統呼叫,通過它們來監測多個fd是否處於就緒狀態。select或poll是順序掃描fd是否就緒,而且支援的fd數量有限,因此使用上有制約。epoll呼叫基於事件驅動,因此效能更高,當fd就緒時會立即回撥rollback
解釋一下檔案描述符
Linux 核心將所有外部裝置都看做一個檔案來操作,對一個檔案的讀寫操作會呼叫核心提供的系統命令,返回一個file descriptor(fd 檔案描述符)。而對一個socket的讀寫也會有相應的描述符,稱為socket fd。
現在再來看一下上圖,上圖以select為例。不難發現程式會阻塞於select呼叫,直到所關注的某一個檔案描述符(套接字)變為可讀狀態

訊號驅動I/O
訊號驅動I/O的意思就是我們現在不用傻等著了,也不用去輪詢。而是讓核心在資料就緒時,傳送訊號通知我們。
在這裡插入圖片描述
呼叫的步驟是,我們通過系統呼叫sigaction,並註冊一個訊號處理的回撥函式,該呼叫會立即返回,但是當核心資料就緒時,核心會為該程式產生一個SIGIO訊號,並回撥我們註冊的訊號回撥函式,這樣我們就可以在訊號回撥函式中系統呼叫recvfrom獲取資料
訊號驅動IO模型,應用程式告訴核心:當資料包準備好的時候,給我傳送一個訊號,對SIGIO訊號進行捕捉,並且呼叫我的訊號處理函式來獲取資料包。

非同步I/O
在這裡插入圖片描述
非同步I/O 與 訊號驅動I/O最大區別在於,訊號驅動是核心通知我們何時開始我們I/O操作,而非同步I/O是由核心通知我們I/O操作何時完成,兩者有本質區別
應用程式執行 aio_read 系統呼叫會立即返回,應用程式可以繼續執行,不會被阻塞,核心會在所有操作完成之後嚮應用程式傳送訊號。
當應用程式呼叫aio_read時,核心一方面去取資料包內容返回,另一方面將程式控制權還給應用程式,應用程式繼續處理其他事情,是一種非阻塞的狀態。
當核心中有資料包就緒時,由核心將資料包拷貝到應用程式中,返回aio_read中定義好的函式處理程式。

阻塞程度:阻塞IO>非阻塞IO>多路轉接IO>訊號驅動IO>非同步IO,效率是由低到高的。

I/O 複用
select/poll/epoll 都是 I/O 多路複用的具體實現,select 出現的最早,之後是 poll,再是 epoll。
select、poll、epoll都是I/O多路複用的機制。I/O多路複用就是通過一種機制,一個程式可以監視多個檔案描述符,一旦某個描述符就緒(讀就緒或寫就緒),能夠通知程式進行相應的讀寫操作 。
但是,select,poll,epoll本質還是同步I/O(I/O多路複用本身就是同步IO)的範疇,因為它們都需要在讀寫事件就緒後執行緒自己進行讀寫,讀寫的過程阻塞的。而非同步I/O的實現是系統會把負責把資料從核心空間拷貝到使用者空間,無需執行緒自己再進行阻塞的讀寫,核心已經準備完成。
linux中 socket 的 fd 是什麼?
這個FD就是File Discriptor 中文翻譯為檔案描述符。
Socket起源於unix,Unix中把所有的資源都看作是檔案,包括裝置,比如網路卡、印表機等等,所以,針對Socket通訊,我們在使用網路卡,網路卡又處理N多連結,每個連結都需要一個對應的描述,也就是惟一的ID,即對應的檔案描述符。簡單點說也就是 int fd = socket(AF_INET,SOCK_STREAM, 0); 函式socket()返回的就是這個描述符。在傳輸中我們都要使用這個惟一的ID來確定要往哪個連結上傳輸資料。

select
說的通俗一點就是各個客戶端連線的檔案描述符也就是套接字,都被放到了一個集合中,呼叫select函式之後會一直監視這些檔案描述符中有哪些可讀,如果有可讀的描述符那麼我們的工作程式就去讀取資源。
基本原理:
select 函式監視的檔案描述符分3類,分別是writefds、readfds、和exceptfds。呼叫後select函式會阻塞,直到有描述符就緒(有資料 可讀、可寫、或者有異常),或者超時(timeout指定等待時間,如果立即返回設為null即可),函式返回。當select函式返回後,可以通過遍歷fdset,來找到就緒的描述符。
在這裡插入圖片描述
int select (int __nfds,
fd_set *__restrict __readfds,
fd_set *__restrict __writefds,
fd_set *__restrict __exceptfds,
struct timeval *__restrict __timeout);
fd_set
其中有一個很重要的結構體fd_set,該結構體可以看作是一個描述符的集合,可以將fa_set看作是一個點陣圖,類似於作業系統中的點陣圖,其中每個整數的每一bit代表一個描述符,。
舉個簡單的例子,fd_set中元素的個數為2,初始化都為0,則fd_set中含有兩個整數0,假設一個整數的長度8位(為了好舉例子),則展開fd_set的結構就是 00000000 0000000,如果這個時候新增一個描述符為3,則對應fd_set程式設計 00000000 00001000,可以看到在這種情況下,第一個整數標記描述符07,第二個整數標記815,依次類推
select函式中存在三個fd_set集合,分別代表三種事件,__readfds表示讀描述符集合,__writefds表示寫描述符集合,__exceptfds表示異常描述符集合,當對應的fd_set = NULL時,表示不監聽該類描述符。
__nfds
__nfds是fd_set中最大的描述符+1,當呼叫select的時候,核心態會判斷fd_set中描述符是否就緒,__nfds告訴核心最多判斷到哪一個描述符。
timeval
struct timeval {
long tv_sec; //秒
long tv_usec; //微秒
}
引數__timeout指定select的工作方式:
• __timeout= NULL,表示select永遠等待下去,直到其中至少存在一個描述符就緒
• __timeout結構體中秒或者微妙是一個大於0的整數,表示select等待一段固定的事件,若該短時間內未有描述符就緒則返回
• __timeout= 0,表示不等待,直接返回
函式返回
select函式返回產生事件的描述符的數量,如果為-1表示產生錯誤
值得注意的是,比如使用者態要監聽描述符1和3的讀事件,則將readset對應bit置為1,當呼叫select函式之後,若只有1描述符就緒,則readset對應bit為1,但是描述符3對應的位置為0,這就需要注意,每次呼叫select的時候,都需要重新初始化並賦值readset結構體,將需要監聽的描述符對應的bit置為1,而不能直接使用readset,因為這個時候readset已經被核心改變了。

select目前幾乎在所有的平臺上支援,其良好跨平臺支援也是它的一個優點。
select的一個缺點在於單個程式能夠監視的檔案描述符的數量存在最大限制,在Linux上一般為1024,可以通過修改巨集定義甚至重新編譯核心的方式提升這一限制,但是這樣也會造成效率的降低。
select本質上是通過設定或者檢查存放fd標誌位的資料結構來進行下一步處理。這樣所帶來的缺點是:(見下方總結)

  1. select最大的缺陷就是單個程式所開啟的FD是有一定限制的,它由FD_SETSIZE設定,預設值是1024。
    一般來說這個數目和系統記憶體關係很大,具體數目可以cat /proc/sys/fs/file-max察看。32位機預設是1024個。64位機預設是2048.
  2. 對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低。
    當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成排程,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字註冊某個回撥函式,當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的。
  3. 需要維護一個用來存放大量fd的資料結構,這樣會使得使用者空間和核心空間在傳遞該結構時複製開銷大。
    總結缺點:
    (1)每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,這個開銷在fd很多時會很大
    (2)同時每次呼叫select都需要在核心遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
    (3)select支援的檔案描述符數量太小了,預設是1024

poll
select中,每個fd_set結構體最多隻能標識1024個描述符,在poll中去掉了這種限制。
區別:
• select 會修改描述符,而 poll 不會;
• select 的描述符型別使用陣列實現,FD_SETSIZE 大小預設為 1024,因此預設只能監聽少於 1024 個描述符。如果要監聽更多描述符的話,需要修改 FD_SETSIZE 之後重新編譯;而 poll 沒有描述符數量的限制;
• poll 提供了更多的事件型別,並且對描述符的重複利用上比 select 高。
• 如果一個執行緒對某個描述符呼叫了 select 或者 poll,另一個執行緒關閉了該描述符,會導致呼叫結果不確定。
select 和 poll 速度都比較慢,每次呼叫都需要將全部描述符從應用程式緩衝區複製到核心緩衝區。
基本原理:
poll本質上和select沒有區別,它將使用者傳入的陣列拷貝到核心空間,然後查詢每個fd對應的裝置狀態,如果裝置就緒則在裝置等待佇列中加入一項並繼續遍歷,如果遍歷完所有fd後沒有發現就緒裝置,則掛起當前程式,直到裝置就緒或者主動超時,被喚醒後它又要再次遍歷fd。這個過程經歷了多次無謂的遍歷。
它沒有最大連線數的限制,原因是它是基於連結串列來儲存的,但是同樣有一個缺點:

  1. 大量的fd的陣列被整體複製於使用者態和核心地址空間之間,而不管這樣的複製是不是有意義。
  2. poll還有一個特點是“水平觸發”,如果報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。

注意:
從上面看,select和poll都需要在返回後,通過遍歷檔案描述符來獲取已經就緒的socket。事實上,同時連線的大量客戶端在一時刻可能只有很少的處於就緒狀態,因此隨著監視的描述符數量的增長,其效率也會線性下降。
int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);
struct pollfd {
int fd; // poll的檔案描述符
short int events; // poll關心的事件型別
short int revents; // 發生的事件型別
};
Poll使用結構體pollfd來指定一個需要監聽的描述符,結構體中fd為需要監聽的檔案描述符,events為需要監聽的事件型別,而revents為經過poll呼叫之後返回的事件型別,在呼叫poll的時候,一般會傳入一個pollfd的結構體陣列,陣列的元素個數表示監控的描述符個數,所以pollfd相對於select,沒有最大1024個描述符的限制。
__fds
__fds的作用同select中的__nfds,表示pollfd陣列中最大的下標索引
__timeout
__timeout = -1:poll阻塞直到有事件產生
__timeout = -0:poll立刻返回
__timeout != -1 && __timeout != 0:poll阻塞__timeout對應的時候,如果超過該時間沒有事件產生則返回
函式返回
poll函式返回產生事件的描述符的數量,如果返回0表示超時,如果為-1表示產生錯誤

epoll
epoll是在2.6核心中提出的,是之前的select和poll的增強版本。
相對於select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個檔案描述符管理多個描述符,將使用者關係的檔案描述符的事件存放到核心的一個事件表中,這樣在使用者空間和核心空間的copy只需一次。
基本原理:
epoll支援水平觸發和邊緣觸發,最大的特點在於邊緣觸發,它只告訴程式哪些fd剛剛變為就緒態,並且只會通知一次。還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl註冊fd,一旦該fd就緒,核心就會採用類似callback的回撥機制來啟用該fd,epoll_wait便可以收到通知。
epoll的優點:

  1. 沒有最大併發連線的限制,能開啟的FD的上限遠大於1024(1G的記憶體上能監聽約10萬個埠)。
  2. 效率提升,不是輪詢的方式,不會隨著FD數目的增加效率下降。只有活躍可用的FD才會呼叫callback函式;即Epoll最大的優點就在於它只管你“活躍”的連線,而跟連線總數無關,因此在實際的網路環境中,Epoll的效率就會遠遠高於select和poll。
  3. 記憶體拷貝,利用mmap()檔案對映記憶體加速與核心空間的訊息傳遞;即epoll使用mmap減少複製開銷。
    int epoll_create(int size);
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    epoll_ctl() 用於向核心註冊新的描述符或者是改變某個檔案描述符的狀態。已註冊的描述符在核心中會被維護在一棵紅黑樹上,通過回撥函式callback核心會將 I/O 準備好的描述符加入到一個連結串列中管理,程式呼叫 epoll_wait() 便可以得到事件完成的描述符。
    從上面的描述可以看出,epoll 只需要將描述符從程式緩衝區向核心緩衝區拷貝一次,並且程式不需要通過輪詢來獲得事件完成的描述符。
    epoll 僅適用於 Linux OS。

工作模式
epoll 的描述符事件有兩種觸發模式:LT(level trigger)和 ET(edge trigger)。

  1. LT 模式
    在這種做法中,核心告訴你一個檔案描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。
    當 epoll_wait() 檢測到描述符事件到達時,將此事件通知程式,程式可以不立即處理該事件,下次呼叫 epoll_wait() 會再次通知程式。是預設的一種模式,並且同時支援 Blocking 和 No-Blocking。
  2. ET 模式
    在這種模式下,當描述符從未就緒變為就緒時,核心通過epoll告訴你。然後它會假設你知道檔案描述符已經就緒,並且不會再為那個檔案描述符傳送更多的就緒通知,直到你做了某些操作導致那個檔案描述符不再為就緒狀態了(比如,你在傳送,接收或者接收請求,或者傳送接收的資料少於一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),核心不會傳送更多的通知(only once)。
    和 LT 模式不同的是,通知之後程式必須立即處理事件,下次再呼叫 epoll_wait() 時不會再得到事件到達的通知。
    很大程度上減少了 epoll 事件被重複觸發的次數,因此效率要比 LT 模式高。只支援 No-Blocking,以避免由於一個檔案控制程式碼的阻塞讀/阻塞寫操作把處理多個檔案描述符的任務餓死。

三者區別:
三者比較

  1. 使用者態將檔案描述符傳入核心的方式
    • select:建立3個檔案描述符集並拷貝到核心中,分別監聽讀、寫、異常動作。這裡受到單個程式可以開啟的fd數量限制,預設是1024。
    • poll:將傳入的struct pollfd結構體陣列拷貝到核心中進行監聽。
    • epoll:執行epoll_create會在核心的高速cache區中建立一顆紅黑樹以及就緒連結串列(該連結串列儲存已經就緒的檔案描述符)。接著使用者執行的epoll_ctl函式新增檔案描述符會在紅黑樹上增加相應的結點。
  2. 核心態檢測檔案描述符讀寫狀態的方式
    • select:採用輪詢方式,遍歷所有fd,最後返回一個描述符讀寫操作是否就緒的mask掩碼,根據這個掩碼給fd_set賦值。
    • poll:同樣採用輪詢方式,查詢每個fd的狀態,如果就緒則在等待佇列中加入一項並繼續遍歷。
    • epoll:採用回撥機制。在執行epoll_ctl的add操作時,不僅將檔案描述符放到紅黑樹上,而且也註冊了回撥函式,核心在檢測到某檔案描述符可讀/可寫時會呼叫回撥函式,該回撥函式將檔案描述符放在就緒連結串列中。
  3. 找到就緒的檔案描述符並傳遞給使用者態的方式
    • select:將之前傳入的fd_set拷貝傳出到使用者態並返回就緒的檔案描述符總數。使用者態並不知道是哪些檔案描述符處於就緒態,需要遍歷來判斷。(使用者態再遍歷一次)
    • poll:將之前傳入的fd陣列拷貝傳出使用者態並返回就緒的檔案描述符總數。使用者態並不知道是哪些檔案描述符處於就緒態,需要遍歷來判斷。(使用者態再遍歷一次)
    • epoll:epoll_wait只用觀察就緒連結串列中有無資料即可,最後將連結串列的資料返回給陣列並返回就緒的數量。核心將就緒的檔案描述符放在傳入的陣列中,所以只用遍歷依次處理即可。這裡返回的檔案描述符是通過mmap讓核心和使用者空間共享同一塊記憶體實現傳遞的,減少了不必要的拷貝。
  4. 重複監聽的處理方式
    • select:將新的監聽檔案描述符集合拷貝傳入核心中,繼續以上步驟。
    • poll:將新的struct pollfd結構體陣列拷貝傳入核心中,繼續以上步驟。
    • epoll:無需重新構建紅黑樹,直接沿用已存在的即可。

總結:
select與poll中,每次呼叫都要把fd集合從使用者態往核心態拷貝一次,(建立一個待處理事件列表,然後把這個列表傳送給核心),返回的時候再去輪詢這個列表,以判斷事件是否發生。呼叫多次的話,就會重複將fd拷貝進核心。在描述符比較多的時候,效率極低。
epoll將檔案描述符列表的管理交給核心負責,每次註冊新的事件時,將fd拷貝進核心,epoll保證fd在整個過程中僅被拷貝一次,避免了反覆拷貝重複fd的巨大開銷。此外,一旦某個事件發生時,裝置就緒時,呼叫回撥函式,把就緒fd放入就緒連結串列中,並喚醒在epoll_wait中進入睡眠的程式(核心就把發生事件的描述符列表通知程式,避免對所有描述符列表進行輪詢),只要判斷一下就緒連結串列是否為空就行了。

在 windows 下,只支援 select,不支援 epoll,而 linux 2.6是支援 epoll的.
epoll 系統呼叫「複雜度 O(1)」
select 「複雜度 O(n)」

應用場景
很容易產生一種錯覺認為只要用 epoll 就可以了,select 和 poll 都已經過時了,其實它們都有各自的使用場景。

  1. select 應用場景
    select 的 timeout 引數精度為微秒,而 poll 和 epoll 為毫秒,因此 select 更加適用於實時性要求比較高的場景,比如核反應堆的控制。
    select 可移植性更好,幾乎被所有主流平臺所支援。
  2. poll 應用場景
    poll 沒有最大描述符數量的限制,如果平臺支援並且對實時性要求不高,應該使用 poll 而不是 select。
  3. epoll 應用場景
    只需要執行在 Linux 平臺上,有大量的描述符需要同時輪詢,並且這些連線最好是長連線。
    需要同時監控小於 1000 個描述符,就沒有必要使用 epoll,因為這個應用場景下並不能體現 epoll 的優勢。
    需要監控的描述符狀態變化多,而且都是非常短暫的,也沒有必要使用 epoll。因為 epoll 中的所有描述符都儲存在核心中,造成每次需要對描述符的狀態改變都需要通過 epoll_ctl() 進行系統呼叫,頻繁系統呼叫降低效率。並且 epoll 的描述符儲存在核心,不容易除錯。

磁碟排程:

磁碟排程在多道程式設計的計算機系統中,各個程式可能會不斷提出不同的對磁碟進行讀/寫操作的請求。由於有時候這些程式的傳送請求的速度比磁碟響應的還要快,因此我們有必要為每個磁碟裝置建立一個等待佇列。

一次磁碟讀/寫操作需要的時間
尋找時間(尋道時間)Ts:在讀/寫資料前,需要將磁頭移動到指定磁軌所花費的時間。
尋道時間分兩步:
(1) 啟動磁頭臂消耗的時間:s。
(2) 移動磁頭消耗的時間:假設磁頭勻速移動,每跨越一個磁軌消耗時間為m,共跨越n條磁軌。
則尋道時間 Ts = s + m * n。
延遲時間TR:通過旋轉磁碟,使磁頭定位到目標扇區所需要的時間。設磁碟轉速為r(單位:轉/秒,或轉/分),則平均所需延遲時間TR = (1/2)*(1/r) = 1/2r。
1/r就是轉一圈所需的時間。找到目標扇區平均需要轉半圈,因此再乘以1/2。
傳輸時間TR:從磁碟讀出或向磁碟中寫入資料所經歷的時間,假設磁碟轉速為r,此次讀/寫的位元組數為b,每個磁軌上的位元組數為N,則傳輸時間TR = (b/N) * (1/r) = b/(rN)。
每個磁軌可存N位元組資料,因此b位元組資料需要b/N個磁軌才能儲存。而讀/寫一個磁軌所需的時間剛好是轉一圈的時間1/r。
總的平均時間Ta = Ts + 1/2r + b/(rN),由於延遲時間和傳輸時間都是與磁碟轉速有關的,且是線性相關。而轉速又是磁碟的固有屬性,因此無法通過作業系統優化延遲時間和傳輸時間。所以只能優化尋找時間。
在這裡插入圖片描述
磁碟排程演算法:
1 先來先服務演算法(FCFS)
演算法思想:根據程式請求訪問磁碟的先後順序進行排程。
在這裡插入圖片描述
假設磁頭的初始位置是100號磁軌,有多個程式先後陸續地請求訪問55、58、39、18、90、160、150、38、184號磁軌。
按照先來先服務演算法規則,按照請求到達的順序,磁頭需要一次移動到55、58、39、18、90、160、150、38、184號磁軌。
優點:公平;如果請求訪問的磁軌比較集中的話,演算法效能還算可以。
缺點:如果大量程式競爭使用磁碟,請求訪問的磁軌很分散,FCFS在效能上很差,尋道時間長。

2 最短尋找時間優先(SSTF)
演算法思想:優先處理的磁軌是與當前磁頭最近的磁軌。可以保證每次尋道時間最短,但是不能保證總的尋道時間最短。(其實是貪心演算法的思想,只是選擇眼前最優,但是總體未必最優)。
假設磁頭的初始位置是100號磁軌,有多個程式先後陸續地請求訪問55、58、39、18、90、160、150、38、184號磁軌。
在這裡插入圖片描述
缺點:可能產生飢餓現象。
本例中,如果在處理18號磁軌的訪問請求時又來了一個38號磁軌的訪問請求,處理38號磁軌的訪問請求又來了一個18號磁軌訪問請求。如果有源源不斷的18號、38號磁軌訪問請求,那麼150、160、184號磁軌請求的訪問就永遠得不到滿足,從而產生飢餓現象。這裡產生飢餓的原因是磁頭在一小塊區域來回移動。

3 掃描演算法(SCAN)
SSTF最短尋找時間優先演算法會產生飢餓的原因在於:磁頭有可能再一個小區域內來回得移動。為了防止這個問題,可以規定:磁頭只有移動到請求最外側磁軌或最內側磁軌才可以反向移動,如果在磁頭移動的方向上已經沒有請求,就可以立即改變磁頭移動,不必移動到最內/外側的磁軌。這就是掃描演算法的思想。由於磁頭移動的方式很像電梯,因此也叫電梯演算法。
假設某磁碟的磁軌為0~200號,磁頭的初始位置是100號磁軌,且此時磁頭正在往磁軌號增大的方向移動,有多個程式先後陸續的訪問55、58、39、18、90、160、150、38、184號磁軌。
在這裡插入圖片描述
優點:效能較好,尋道時間較短,不會產生飢餓現象。
缺點:SCAN演算法對於各個位置磁軌的響應頻率不平均。(假設此時磁頭正在往右移動,且剛處理過90號磁軌,那麼下次處理90號磁軌的請求就需要等待低頭移動很長一段距離;而響應了184號磁軌的請求之後,很快又可以再次響應184號磁軌請求了。)

4 迴圈掃描演算法(C-SCAN)
SCAN演算法對各個位置磁軌的響應頻率不平均,而C-SCAN演算法就是為了解決這個問題。規定只有磁頭朝某個特定方向移動時才處理磁軌訪問請求,而返回時直接快速移動至最靠邊緣的並且需要訪問的磁軌上而不處理任何請求。
通俗理解就是SCAN算在改變磁頭方向時不處理磁碟訪問請求而是直接移動到另一端最靠邊的磁碟訪問請求的磁軌上。
假設某磁碟的磁軌為0~200號,磁頭的初始位置是100號磁軌,且此時磁頭正在往磁軌號增大的方向移動,有多個程式先後陸續的訪問55、58、39、18、90、160、150、38、184號磁軌。
在這裡插入圖片描述
優點:相比於SCAN演算法,對於各個位置磁軌響應頻率很平均。
缺點:相比於SCAN演算法,平均尋道時間更長。

相關文章