來源:Java建設(shè)者 作者:cxuan 下面是本文的結(jié)構(gòu)圖 我們平常說的進(jìn)程和線程更多的是基于編程語言的角度來說的,那么你真的了解什么是線程和進(jìn)程嗎?那么我們就從操作系統(tǒng)的角度來了解一下什么是進(jìn)程和線程。 進(jìn)程操作系統(tǒng)中最核心的概念就是 所有現(xiàn)代的計算機(jī)會在同一時刻做很多事情,過去使用計算機(jī)的人(單 CPU)可能完全無法理解現(xiàn)在這種變化,舉個例子更能說明這一點(diǎn):首先考慮一個 Web 服務(wù)器,請求都來自于 Web 網(wǎng)頁。當(dāng)一個請求到達(dá)時,服務(wù)器會檢查當(dāng)前頁是否在緩存中,如果是在緩存中,就直接把緩存中的內(nèi)容返回。如果緩存中沒有的話,那么請求就會交給磁盤來處理。但是,從 CPU 的角度來看,磁盤請求需要更長的時間,因?yàn)榇疟P請求會很慢。當(dāng)硬盤請求完成時,更多其他請求才會進(jìn)入。如果有多個磁盤的話,可以在第一個請求完成前就可以連續(xù)的對其他磁盤發(fā)出部分或全部請求。很顯然,這是一種并發(fā)現(xiàn)象,需要有并發(fā)控制條件來控制并發(fā)現(xiàn)象。 現(xiàn)在考慮只有一個用戶的 PC。當(dāng)系統(tǒng)啟動時,許多進(jìn)程也在后臺啟動,用戶通常不知道這些進(jìn)程的啟動,試想一下,當(dāng)你自己的計算機(jī)啟動的時候,你能知道哪些進(jìn)程是需要啟動的么?這些后臺進(jìn)程可能是一個需要輸入電子郵件的電子郵件進(jìn)程,或者是一個計算機(jī)病毒查殺進(jìn)程來周期性的更新病毒庫。某個用戶進(jìn)程可能會在所有用戶上網(wǎng)的時候打印文件以及刻錄 CD-ROM,這些活動都需要管理。于是一個支持多進(jìn)程的多道程序系統(tǒng)就會顯得很有必要了。 在許多多道程序系統(tǒng)中,CPU 會在
因?yàn)?CPU 執(zhí)行速度很快,進(jìn)程間的換進(jìn)換出也非常迅速,因此我們很難對多個并行進(jìn)程進(jìn)行跟蹤,所以,在經(jīng)過多年的努力后,操作系統(tǒng)的設(shè)計者開發(fā)了用于描述并行的一種概念模型(順序進(jìn)程),使得并行更加容易理解和分析,對該模型的探討,也是本篇文章的主題。下面我們就來探討一下進(jìn)程模型 進(jìn)程模型在進(jìn)程模型中,所有計算機(jī)上運(yùn)行的軟件,通常也包括操作系統(tǒng),被組織為若干 如上圖所示,這是一個具有 4 個程序的多道處理程序,在進(jìn)程不斷切換的過程中,程序計數(shù)器也在不同的變化。 在上圖中,這 4 道程序被抽象為 4 個擁有各自控制流程(即每個自己的程序計數(shù)器)的進(jìn)程,并且每個程序都獨(dú)立的運(yùn)行。當(dāng)然,實(shí)際上只有一個物理程序計數(shù)器,每個程序要運(yùn)行時,其邏輯程序計數(shù)器會裝載到物理程序計數(shù)器中。當(dāng)程序運(yùn)行結(jié)束后,其物理程序計數(shù)器就會是真正的程序計數(shù)器,然后再把它放回進(jìn)程的邏輯計數(shù)器中。 從下圖我們可以看到,在觀察足夠長的一段時間后,所有的進(jìn)程都運(yùn)行了,但在任何一個給定的瞬間僅有一個進(jìn)程真正運(yùn)行。 因此,當(dāng)我們說一個 CPU 只能真正一次運(yùn)行一個進(jìn)程的時候,即使有 2 個核(或 CPU),每一個核也只能一次運(yùn)行一個線程。 由于 CPU 會在各個進(jìn)程之間來回快速切換,所以每個進(jìn)程在 CPU 中的運(yùn)行時間是無法確定的。并且當(dāng)同一個進(jìn)程再次在 CPU 中運(yùn)行時,其在 CPU 內(nèi)部的運(yùn)行時間往往也是不固定的。進(jìn)程和程序之間的區(qū)別是非常微妙的,但是通過一個例子可以讓你加以區(qū)分:想想一位會做飯的計算機(jī)科學(xué)家正在為他的女兒制作生日蛋糕。他有做生日蛋糕的食譜,廚房里有所需的原諒:面粉、雞蛋、糖、香草汁等。在這個比喻中,做蛋糕的食譜就是程序、計算機(jī)科學(xué)家就是 CPU、而做蛋糕的各種原料都是輸入數(shù)據(jù)。進(jìn)程就是科學(xué)家閱讀食譜、取來各種原料以及烘焙蛋糕等一系列動作的總和。 現(xiàn)在假設(shè)科學(xué)家的兒子跑過來告訴他,說他的頭被蜜蜂蜇了一下,那么此時科學(xué)家會記錄出來他做蛋糕這個過程到了哪一步,然后拿出急救手冊,按照上面的步驟給他兒子實(shí)施救助。這里,會涉及到進(jìn)程之間的切換,科學(xué)家(CPU)會從做蛋糕(進(jìn)程)切換到實(shí)施醫(yī)療救助(另一個進(jìn)程)。等待傷口處理完畢后,科學(xué)家會回到剛剛記錄做蛋糕的那一步,繼續(xù)制作。 這里的關(guān)鍵思想是 進(jìn)程的創(chuàng)建操作系統(tǒng)需要一些方式來創(chuàng)建進(jìn)程。下面是一些創(chuàng)建進(jìn)程的方式
系統(tǒng)初始化啟動操作系統(tǒng)時,通常會創(chuàng)建若干個進(jìn)程。其中有些是 系統(tǒng)調(diào)用創(chuàng)建除了在啟動階段創(chuàng)建進(jìn)程之外,一些新的進(jìn)程也可以在后面創(chuàng)建。通常,一個正在運(yùn)行的進(jìn)程會發(fā)出 用戶請求創(chuàng)建在許多交互式系統(tǒng)中,輸入一個命令或者雙擊圖標(biāo)就可以啟動程序,以上任意一種操作都可以選擇開啟一個新的進(jìn)程,在基本的 UNIX 系統(tǒng)中運(yùn)行 X,新進(jìn)程將接管啟動它的窗口。在 Windows 中啟動進(jìn)程時,它一般沒有窗口,但是它可以創(chuàng)建一個或多個窗口。每個窗口都可以運(yùn)行進(jìn)程。通過鼠標(biāo)或者命令就可以切換窗口并與進(jìn)程進(jìn)行交互。
批處理創(chuàng)建最后一種創(chuàng)建進(jìn)程的情形會在 從技術(shù)上講,在所有這些情況下,讓現(xiàn)有流程執(zhí)行流程是通過創(chuàng)建系統(tǒng)調(diào)用來創(chuàng)建新流程的。該進(jìn)程可能是正在運(yùn)行的用戶進(jìn)程,是從鍵盤或鼠標(biāo)調(diào)用的系統(tǒng)進(jìn)程或批處理程序。這些就是系統(tǒng)調(diào)用創(chuàng)建新進(jìn)程的過程。該系統(tǒng)調(diào)用告訴操作系統(tǒng)創(chuàng)建一個新進(jìn)程,并直接或間接指示在其中運(yùn)行哪個程序。 在 UNIX 中,僅有一個系統(tǒng)調(diào)用來創(chuàng)建一個新的進(jìn)程,這個系統(tǒng)調(diào)用就是 在 Windows 中,情況正相反,一個簡單的 Win32 功能調(diào)用 在 UNIX 和 Windows 中,進(jìn)程創(chuàng)建之后,父進(jìn)程和子進(jìn)程有各自不同的地址空間。如果其中某個進(jìn)程在其地址空間中修改了一個詞,這個修改將對另一個進(jìn)程不可見。在 UNIX 中,子進(jìn)程的地址空間是父進(jìn)程的一個拷貝,但是卻是兩個不同的地址空間;不可寫的內(nèi)存區(qū)域是共享的。某些 UNIX 實(shí)現(xiàn)是正是在兩者之間共享,因?yàn)樗荒鼙恍薷??;蛘?,子進(jìn)程共享父進(jìn)程的所有內(nèi)存,但是這種情況下內(nèi)存通過 進(jìn)程的終止進(jìn)程在創(chuàng)建之后,它就開始運(yùn)行并做完成任務(wù)。然而,沒有什么事兒是永不停歇的,包括進(jìn)程也一樣。進(jìn)程早晚會發(fā)生終止,但是通常是由于以下情況觸發(fā)的
正常退出多數(shù)進(jìn)程是由于完成了工作而終止。當(dāng)編譯器完成了所給定程序的編譯之后,編譯器會執(zhí)行一個系統(tǒng)調(diào)用告訴操作系統(tǒng)它完成了工作。這個調(diào)用在 UNIX 中是 錯誤退出進(jìn)程發(fā)生終止的第二個原因是發(fā)現(xiàn)嚴(yán)重錯誤,例如,如果用戶執(zhí)行如下命令 cc foo.c 為了能夠編譯 foo.c 但是該文件不存在,于是編譯器就會發(fā)出聲明并退出。在給出了錯誤參數(shù)時,面向屏幕的交互式進(jìn)程通常并不會直接退出,因?yàn)檫@從用戶的角度來說并不合理,用戶需要知道發(fā)生了什么并想要進(jìn)行重試,所以這時候應(yīng)用程序通常會彈出一個對話框告知用戶發(fā)生了系統(tǒng)錯誤,是需要重試還是退出。 嚴(yán)重錯誤進(jìn)程終止的第三個原因是由進(jìn)程引起的錯誤,通常是由于程序中的錯誤所導(dǎo)致的。例如,執(zhí)行了一條非法指令,引用不存在的內(nèi)存,或者除數(shù)是 0 等。在有些系統(tǒng)比如 UNIX 中,進(jìn)程可以通知操作系統(tǒng),它希望自行處理某種類型的錯誤,在這類錯誤中,進(jìn)程會收到信號(中斷),而不是在這類錯誤出現(xiàn)時直接終止進(jìn)程。 被其他進(jìn)程殺死第四個終止進(jìn)程的原因是,某個進(jìn)程執(zhí)行系統(tǒng)調(diào)用告訴操作系統(tǒng)殺死某個進(jìn)程。在 UNIX 中,這個系統(tǒng)調(diào)用是 kill。在 Win32 中對應(yīng)的函數(shù)是 進(jìn)程的層次結(jié)構(gòu)在一些系統(tǒng)中,當(dāng)一個進(jìn)程創(chuàng)建了其他進(jìn)程后,父進(jìn)程和子進(jìn)程就會以某種方式進(jìn)行關(guān)聯(lián)。子進(jìn)程它自己就會創(chuàng)建更多進(jìn)程,從而形成一個進(jìn)程層次結(jié)構(gòu)。 UNIX 進(jìn)程體系在 UNIX 中,進(jìn)程和它的所有子進(jìn)程以及子進(jìn)程的子進(jìn)程共同組成一個進(jìn)程組。當(dāng)用戶從鍵盤中發(fā)出一個信號后,該信號被發(fā)送給當(dāng)前與鍵盤相關(guān)的進(jìn)程組中的所有成員(它們通常是在當(dāng)前窗口創(chuàng)建的所有活動進(jìn)程)。每個進(jìn)程可以分別捕獲該信號、忽略該信號或采取默認(rèn)的動作,即被信號 kill 掉。 這里有另一個例子,可以用來說明層次的作用,考慮 Windows 進(jìn)程體系相反,Windows 中沒有進(jìn)程層次的概念,Windows 中所有進(jìn)程都是平等的,唯一類似于層次結(jié)構(gòu)的是在創(chuàng)建進(jìn)程的時候,父進(jìn)程得到一個特別的令牌(稱為句柄),該句柄可以用來控制子進(jìn)程。然而,這個令牌可能也會移交給別的操作系統(tǒng),這樣就不存在層次結(jié)構(gòu)了。而在 UNIX 中,進(jìn)程不能剝奪其子進(jìn)程的 進(jìn)程狀態(tài)盡管每個進(jìn)程是一個獨(dú)立的實(shí)體,有其自己的程序計數(shù)器和內(nèi)部狀態(tài),但是,進(jìn)程之間仍然需要相互幫助。例如,一個進(jìn)程的結(jié)果可以作為另一個進(jìn)程的輸入,在 shell 命令中
第一個進(jìn)程是 當(dāng)一個進(jìn)程開始運(yùn)行時,它可能會經(jīng)歷下面這幾種狀態(tài) 圖中會涉及三種狀態(tài)
邏輯上來說,運(yùn)行態(tài)和就緒態(tài)是很相似的。這兩種情況下都表示進(jìn)程 三種狀態(tài)會涉及四種狀態(tài)間的切換,在操作系統(tǒng)發(fā)現(xiàn)進(jìn)程不能繼續(xù)執(zhí)行時會發(fā)生 轉(zhuǎn)換 2 和轉(zhuǎn)換 3 都是由進(jìn)程調(diào)度程序(操作系統(tǒng)的一部分)引起的,進(jìn)程本身不知道調(diào)度程序的存在。轉(zhuǎn)換 2 的出現(xiàn)說明進(jìn)程調(diào)度器認(rèn)定當(dāng)前進(jìn)程已經(jīng)運(yùn)行了足夠長的時間,是時候讓其他進(jìn)程運(yùn)行 CPU 時間片了。當(dāng)所有其他進(jìn)程都運(yùn)行過后,這時候該是讓第一個進(jìn)程重新獲得 CPU 時間片的時候了,就會發(fā)生轉(zhuǎn)換 3。
當(dāng)進(jìn)程等待的一個外部事件發(fā)生時(如從外部輸入一些數(shù)據(jù)后),則發(fā)生轉(zhuǎn)換 4。如果此時沒有其他進(jìn)程在運(yùn)行,則立刻觸發(fā)轉(zhuǎn)換 3,該進(jìn)程便開始運(yùn)行,否則該進(jìn)程會處于就緒階段,等待 CPU 空閑后再輪到它運(yùn)行。 從上面的觀點(diǎn)引入了下面的模型 操作系統(tǒng)最底層的就是調(diào)度程序,在它上面有許多進(jìn)程。所有關(guān)于中斷處理、啟動進(jìn)程和停止進(jìn)程的具體細(xì)節(jié)都隱藏在調(diào)度程序中。事實(shí)上,調(diào)度程序只是一段非常小的程序。 進(jìn)程的實(shí)現(xiàn)操作系統(tǒng)為了執(zhí)行進(jìn)程間的切換,會維護(hù)著一張表格,這張表就是 下面展示了一個典型系統(tǒng)中的關(guān)鍵字段 第一列內(nèi)容與 存儲管理的 text segment 、 data segment、stack segment 更多了解見下面這篇文章 現(xiàn)在我們應(yīng)該對進(jìn)程表有個大致的了解了,就可以在對單個 CPU 上如何運(yùn)行多個順序進(jìn)程的錯覺做更多的解釋。與每一 I/O 類相關(guān)聯(lián)的是一個稱作 當(dāng)中斷結(jié)束后,操作系統(tǒng)會調(diào)用一個 C 程序來處理中斷剩下的工作。在完成剩下的工作后,會使某些進(jìn)程就緒,接著調(diào)用調(diào)度程序,決定隨后運(yùn)行哪個進(jìn)程。然后將控制權(quán)轉(zhuǎn)移給一段匯編語言代碼,為當(dāng)前的進(jìn)程裝入寄存器值以及內(nèi)存映射并啟動該進(jìn)程運(yùn)行,下面顯示了中斷處理和調(diào)度的過程。
一個進(jìn)程在執(zhí)行過程中可能被中斷數(shù)千次,但關(guān)鍵每次中斷后,被中斷的進(jìn)程都返回到與中斷發(fā)生前完全相同的狀態(tài)。 線程在傳統(tǒng)的操作系統(tǒng)中,每個進(jìn)程都有一個地址空間和一個控制線程。事實(shí)上,這是大部分進(jìn)程的定義。不過,在許多情況下,經(jīng)常存在同一地址空間中運(yùn)行多個控制線程的情形,這些線程就像是分離的進(jìn)程。下面我們就著重探討一下什么是線程 線程的使用或許這個疑問也是你的疑問,為什么要在進(jìn)程的基礎(chǔ)上再創(chuàng)建一個線程的概念,準(zhǔn)確的說,這其實(shí)是進(jìn)程模型和線程模型的討論,回答這個問題,可能需要分三步來回答
多線程解決方案現(xiàn)在考慮一個線程使用的例子:一個萬維網(wǎng)服務(wù)器,對頁面的請求發(fā)送給服務(wù)器,而所請求的頁面發(fā)送回客戶端。在多數(shù) web 站點(diǎn)上,某些頁面較其他頁面相比有更多的訪問。例如,索尼的主頁比任何一個照相機(jī)詳情介紹頁面具有更多的訪問,Web 服務(wù)器可以把獲得大量訪問的頁面集合保存在內(nèi)存中,避免到磁盤去調(diào)入這些頁面,從而改善性能。這種頁面的集合稱為 上面是一個 web 服務(wù)器的組織方式,一個叫做 當(dāng)工作線程啟動后,它會檢查請求是否在 web 頁面的高速緩存中存在,這個高速緩存是所有線程都可以訪問的。如果高速緩存不存在這個 web 頁面的話,它會調(diào)用一個 這種模型允許將服務(wù)器編寫為順序線程的集合,在分派線程的程序中包含一個死循環(huán),該循環(huán)用來獲得工作請求并且把請求派給工作線程。每個工作線程的代碼包含一個從調(diào)度線程接收的請求,并且檢查 web 高速緩存中是否存在所需頁面,如果有,直接把該頁面返回給客戶,接著工作線程阻塞,等待一個新請求的到達(dá)。如果沒有,工作線程就從磁盤調(diào)入該頁面,將該頁面返回給客戶機(jī),然后工作線程阻塞,等待一個新請求。 下面是調(diào)度線程和工作線程的代碼,這里假設(shè) TRUE 為常數(shù) 1 ,buf 和 page 分別是保存工作請求和 Web 頁面的相應(yīng)結(jié)構(gòu)。 調(diào)度線程的大致邏輯 while(TRUE){ 工作線程的大致邏輯
單線程解決方案現(xiàn)在考慮沒有多線程的情況下,如何編寫 Web 服務(wù)器。我們很容易的就想象為單個線程了,Web 服務(wù)器的主循環(huán)獲取請求并檢查請求,并爭取在下一個請求之前完成工作。在等待磁盤操作時,服務(wù)器空轉(zhuǎn),并且不處理任何到來的其他請求。結(jié)果會導(dǎo)致每秒中只有很少的請求被處理,所以這個例子能夠說明多線程提高了程序的并行性并提高了程序的性能。 狀態(tài)機(jī)解決方案到現(xiàn)在為止,我們已經(jīng)有了兩種解決方案,單線程解決方案和多線程解決方案,其實(shí)還有一種解決方案就是 如果目前只有一個非阻塞版本的 read 系統(tǒng)調(diào)用可以使用,那么當(dāng)請求到達(dá)服務(wù)器時,這個唯一的 read 調(diào)用的線程會進(jìn)行檢查,如果能夠從高速緩存中得到響應(yīng),那么直接返回,如果不能,則啟動一個非阻塞的磁盤操作 服務(wù)器在表中記錄當(dāng)前請求的狀態(tài),然后進(jìn)入并獲取下一個事件,緊接著下一個事件可能就是一個新工作的請求或是磁盤對先前操作的回答。如果是新工作的請求,那么就開始處理請求。如果是磁盤的響應(yīng),就從表中取出對應(yīng)的狀態(tài)信息進(jìn)行處理。對于非阻塞式磁盤 I/O 而言,這種響應(yīng)一般都是信號中斷響應(yīng)。 每次服務(wù)器從某個請求工作的狀態(tài)切換到另一個狀態(tài)時,都必須顯示的保存或者重新裝入相應(yīng)的計算狀態(tài)。這里,每個計算都有一個被保存的狀態(tài),存在一個會發(fā)生且使得相關(guān)狀態(tài)發(fā)生改變的事件集合,我們把這類設(shè)計稱為 這三種解決方案各有各的特性,多線程使得順序進(jìn)程的思想得以保留下來,并且實(shí)現(xiàn)了并行性,但是順序進(jìn)程會阻塞系統(tǒng)調(diào)用;單線程服務(wù)器保留了阻塞系統(tǒng)的簡易性,但是卻放棄了性能。有限狀態(tài)機(jī)的處理方法運(yùn)用了非阻塞調(diào)用和中斷,通過并行實(shí)現(xiàn)了高性能,但是給編程增加了困難。
經(jīng)典的線程模型理解進(jìn)程的另一個角度是,用某種方法把相關(guān)的資源集中在一起。進(jìn)程有存放程序正文和數(shù)據(jù)以及其他資源的地址空間。這些資源包括打開的文件、子進(jìn)程、即將發(fā)生的定時器、信號處理程序、賬號信息等。把這些信息放在進(jìn)程中會比較容易管理。 另一個概念是,進(jìn)程中擁有一個執(zhí)行的線程,通常簡寫為 線程給進(jìn)程模型增加了一項(xiàng)內(nèi)容,即在同一個進(jìn)程中,允許彼此之間有較大的獨(dú)立性且互不干擾。在一個進(jìn)程中并行運(yùn)行多個線程類似于在一臺計算機(jī)上運(yùn)行多個進(jìn)程。在多個線程中,各個線程共享同一地址空間和其他資源。在多個進(jìn)程中,進(jìn)程共享物理內(nèi)存、磁盤、打印機(jī)和其他資源。因?yàn)榫€程會包含有一些進(jìn)程的屬性,所以線程被稱為 下圖我們可以看到三個傳統(tǒng)的進(jìn)程,每個進(jìn)程有自己的地址空間和單個控制線程。每個線程都在不同的地址空間中運(yùn)行 下圖中,我們可以看到有一個進(jìn)程三個線程的情況。每個線程都在相同的地址空間中運(yùn)行。 線程不像是進(jìn)程那樣具備較強(qiáng)的獨(dú)立性。同一個進(jìn)程中的所有線程都會有完全一樣的地址空間,這意味著它們也共享同樣的全局變量。由于每個線程都可以訪問進(jìn)程地址空間內(nèi)每個內(nèi)存地址,因此一個線程可以讀取、寫入甚至擦除另一個線程的堆棧。線程之間除了共享同一內(nèi)存空間外,還具有如下不同的內(nèi)容 上圖左邊的是同一個進(jìn)程中 和進(jìn)程一樣,線程可以處于下面這幾種狀態(tài):運(yùn)行中、阻塞、就緒和終止(進(jìn)程圖中沒有畫)。正在運(yùn)行的線程擁有 CPU 時間片并且狀態(tài)是運(yùn)行中。一個被阻塞的線程會等待某個釋放它的事件。例如,當(dāng)一個線程執(zhí)行從鍵盤讀入數(shù)據(jù)的系統(tǒng)調(diào)用時,該線程就被阻塞直到有輸入為止。線程通常會被阻塞,直到它等待某個外部事件的發(fā)生或者有其他線程來釋放它。線程之間的狀態(tài)轉(zhuǎn)換和進(jìn)程之間的狀態(tài)轉(zhuǎn)換是一樣的。 每個線程都會有自己的堆棧,如下圖所示 線程系統(tǒng)調(diào)用進(jìn)程通常會從當(dāng)前的某個單線程開始,然后這個線程通過調(diào)用一個庫函數(shù)(比如 當(dāng)一個線程完成工作后,可以通過調(diào)用一個函數(shù)(比如 另一個常見的線程是調(diào)用 POSIX 線程為了使編寫可移植線程程序成為可能,IEEE 在 IEEE 標(biāo)準(zhǔn) 1003.1c 中定義了線程標(biāo)準(zhǔn)。線程包被定義為
所有的 Pthreads 都有特定的屬性,每一個都含有標(biāo)識符、一組寄存器(包括程序計數(shù)器)和一組存儲在結(jié)構(gòu)中的屬性。這個屬性包括堆棧大小、調(diào)度參數(shù)以及其他線程需要的項(xiàng)目。 新的線程會通過 當(dāng)線程完成指派給他的工作后,會通過 一般一個線程在繼續(xù)運(yùn)行前需要等待另一個線程完成它的工作并退出??梢酝ㄟ^ 有時會出現(xiàn)這種情況:一個線程邏輯上沒有阻塞,但感覺上它已經(jīng)運(yùn)行了足夠長的時間并且希望給另外一個線程機(jī)會去運(yùn)行。這時候可以通過 下面兩個線程調(diào)用是處理屬性的。 最后, 為了更好的理解 pthread 是如何工作的,考慮下面這個例子 #include <pthread.h> 主線程在宣布它的指責(zé)之后,循環(huán) 線程實(shí)現(xiàn)主要有三種實(shí)現(xiàn)方式
下面我們分開討論一下 在用戶空間中實(shí)現(xiàn)線程第一種方法是把整個線程包放在用戶空間中,內(nèi)核對線程一無所知,它不知道線程的存在。所有的這類實(shí)現(xiàn)都有同樣的通用結(jié)構(gòu) 線程在運(yùn)行時系統(tǒng)之上運(yùn)行,運(yùn)行時系統(tǒng)是管理線程過程的集合,包括前面提到的四個過程:pthread_create, pthread_exit, pthread_join 和 pthread_yield。
在用戶空間管理線程時,每個進(jìn)程需要有其專用的 在用戶空間實(shí)現(xiàn)線程的優(yōu)勢在用戶空間中實(shí)現(xiàn)線程要比在內(nèi)核空間中實(shí)現(xiàn)線程具有這些方面的優(yōu)勢:考慮如果在線程完成時或者是在調(diào)用 在用戶空間實(shí)現(xiàn)線程還有一個優(yōu)勢就是它允許每個進(jìn)程有自己定制的調(diào)度算法。例如在某些應(yīng)用程序中,那些具有垃圾收集線程的應(yīng)用程序(知道是誰了吧)就不用擔(dān)心自己線程會不會在不合適的時候停止,這是一個優(yōu)勢。用戶線程還具有較好的可擴(kuò)展性,因?yàn)閮?nèi)核空間中的內(nèi)核線程需要一些表空間和堆棧空間,如果內(nèi)核線程數(shù)量比較大,容易造成問題。 在用戶空間實(shí)現(xiàn)線程的劣勢盡管在用戶空間實(shí)現(xiàn)線程會具有一定的性能優(yōu)勢,但是劣勢還是很明顯的,你如何實(shí)現(xiàn) 與阻塞調(diào)用類似的問題是 另外一個問題是,如果一個線程開始運(yùn)行,該線程所在進(jìn)程中的其他線程都不能運(yùn)行,除非第一個線程自愿的放棄 CPU,在一個單進(jìn)程內(nèi)部,沒有時鐘中斷,所以不可能使用輪轉(zhuǎn)調(diào)度的方式調(diào)度線程。除非其他線程能夠以自己的意愿進(jìn)入運(yùn)行時環(huán)境,否則調(diào)度程序沒有可以調(diào)度線程的機(jī)會。 在內(nèi)核中實(shí)現(xiàn)線程現(xiàn)在我們考慮使用內(nèi)核來實(shí)現(xiàn)線程的情況,此時不再需要運(yùn)行時環(huán)境了。另外,每個進(jìn)程中也沒有線程表。相反,在內(nèi)核中會有用來記錄系統(tǒng)中所有線程的線程表。當(dāng)某個線程希望創(chuàng)建一個新線程或撤銷一個已有線程時,它會進(jìn)行一個系統(tǒng)調(diào)用,這個系統(tǒng)調(diào)用通過對線程表的更新來完成線程創(chuàng)建或銷毀工作。 內(nèi)核中的線程表持有每個線程的寄存器、狀態(tài)和其他信息。這些信息和用戶空間中的線程信息相同,但是位置卻被放在了內(nèi)核中而不是用戶空間中。另外,內(nèi)核還維護(hù)了一張進(jìn)程表用來跟蹤系統(tǒng)狀態(tài)。 所有能夠阻塞的調(diào)用都會通過系統(tǒng)調(diào)用的方式來實(shí)現(xiàn),當(dāng)一個線程阻塞時,內(nèi)核可以進(jìn)行選擇,是運(yùn)行在同一個進(jìn)程中的另一個線程(如果有就緒線程的話)還是運(yùn)行一個另一個進(jìn)程中的線程。但是在用戶實(shí)現(xiàn)中,運(yùn)行時系統(tǒng)始終運(yùn)行自己的線程,直到內(nèi)核剝奪它的 CPU 時間片(或者沒有可運(yùn)行的線程存在了)為止。 由于在內(nèi)核中創(chuàng)建或者銷毀線程的開銷比較大,所以某些系統(tǒng)會采用可循環(huán)利用的方式來回收線程。當(dāng)某個線程被銷毀時,就把它標(biāo)志為不可運(yùn)行的狀態(tài),但是其內(nèi)部結(jié)構(gòu)沒有受到影響。稍后,在必須創(chuàng)建一個新線程時,就會重新啟用舊線程,把它標(biāo)志為可用狀態(tài)。 如果某個進(jìn)程中的線程造成缺頁故障后,內(nèi)核很容易的就能檢查出來是否有其他可運(yùn)行的線程,如果有的話,在等待所需要的頁面從磁盤讀入時,就選擇一個可運(yùn)行的線程運(yùn)行。這樣做的缺點(diǎn)是系統(tǒng)調(diào)用的代價比較大,所以如果線程的操作(創(chuàng)建、終止)比較多,就會帶來很大的開銷。 混合實(shí)現(xiàn)結(jié)合用戶空間和內(nèi)核空間的優(yōu)點(diǎn),設(shè)計人員采用了一種 在這種模型中,編程人員可以自由控制用戶線程和內(nèi)核線程的數(shù)量,具有很大的靈活度。采用這種方法,內(nèi)核只識別內(nèi)核級線程,并對其進(jìn)行調(diào)度。其中一些內(nèi)核級線程會被多個用戶級線程多路復(fù)用。 進(jìn)程間通信進(jìn)程是需要頻繁的和其他進(jìn)程進(jìn)行交流的。例如,在一個 shell 管道中,第一個進(jìn)程的輸出必須傳遞給第二個進(jìn)程,這樣沿著管道進(jìn)行下去。因此,進(jìn)程之間如果需要通信的話,必須要使用一種良好的數(shù)據(jù)結(jié)構(gòu)以至于不能被中斷。下面我們會一起討論有關(guān) 關(guān)于進(jìn)程間的通信,這里有三個問題
需要注意的是,這三個問題中的后面兩個問題同樣也適用于線程 第一個問題在線程間比較好解決,因?yàn)樗鼈児蚕硪粋€地址空間,它們具有相同的運(yùn)行時環(huán)境,可以想象你在用高級語言編寫多線程代碼的過程中,線程通信問題是不是比較容易解決? 另外兩個問題也同樣適用于線程,同樣的問題可用同樣的方法來解決。我們后面會慢慢討論這三個問題,你現(xiàn)在腦子中大致有個印象即可。 競態(tài)條件在一些操作系統(tǒng)中,協(xié)作的進(jìn)程可能共享一些彼此都能讀寫的公共資源。公共資源可能在內(nèi)存中也可能在一個共享文件。為了講清楚進(jìn)程間是如何通信的,這里我們舉一個例子:一個后臺打印程序。當(dāng)一個進(jìn)程需要打印某個文件時,它會將文件名放在一個特殊的 假設(shè)我們的后臺目錄有非常多的
進(jìn)程 A 讀到 in 的值為 7,將 7 存在一個局部變量 進(jìn)程 B 現(xiàn)在繼續(xù)運(yùn)行,它會將打印文件名寫入到 slot 7 中,然后把 in 的指針更改為 8 ,然后進(jìn)程 B 離開去做其他的事情 現(xiàn)在進(jìn)程 A 開始恢復(fù)運(yùn)行,由于進(jìn)程 A 通過檢查 臨界區(qū)不僅共享資源會造成競態(tài)條件,事實(shí)上共享文件、共享內(nèi)存也會造成競態(tài)條件、那么該如何避免呢?或許一句話可以概括說明:禁止一個或多個進(jìn)程在同一時刻對共享資源(包括共享內(nèi)存、共享文件等)進(jìn)行讀寫。換句話說,我們需要一種 避免競爭問題的條件可以用一種抽象的方式去描述。大部分時間,進(jìn)程都會忙于內(nèi)部計算和其他不會導(dǎo)致競爭條件的計算。然而,有時候進(jìn)程會訪問共享內(nèi)存或文件,或者做一些能夠?qū)е赂倯B(tài)條件的操作。我們把對共享內(nèi)存進(jìn)行訪問的程序片段稱作 盡管上面這種設(shè)計避免了競爭條件,但是不能確保并發(fā)線程同時訪問共享數(shù)據(jù)的正確性和高效性。一個好的解決方案,應(yīng)該包含下面四種條件
從抽象的角度來看,我們通常希望進(jìn)程的行為如上圖所示,在 t1 時刻,進(jìn)程 A 進(jìn)入臨界區(qū),在 t2 的時刻,進(jìn)程 B 嘗試進(jìn)入臨界區(qū),因?yàn)榇藭r進(jìn)程 A 正在處于臨界區(qū)中,所以進(jìn)程 B 會阻塞直到 t3 時刻進(jìn)程 A 離開臨界區(qū),此時進(jìn)程 B 能夠允許進(jìn)入臨界區(qū)。最后,在 t4 時刻,進(jìn)程 B 離開臨界區(qū),系統(tǒng)恢復(fù)到?jīng)]有進(jìn)程的原始狀態(tài)。 忙等互斥下面我們會繼續(xù)探討實(shí)現(xiàn)互斥的各種設(shè)計,在這些方案中,當(dāng)一個進(jìn)程正忙于更新其關(guān)鍵區(qū)域的共享內(nèi)存時,沒有其他進(jìn)程會進(jìn)入其關(guān)鍵區(qū)域,也不會造成影響。 屏蔽中斷在單處理器系統(tǒng)上,最簡單的解決方案是讓每個進(jìn)程在進(jìn)入臨界區(qū)后立即 這個方案可行嗎?進(jìn)程進(jìn)入臨界區(qū)域是由誰決定的呢?不是用戶進(jìn)程嗎?當(dāng)進(jìn)程進(jìn)入臨界區(qū)域后,用戶進(jìn)程關(guān)閉中斷,如果經(jīng)過一段較長時間后進(jìn)程沒有離開,那么中斷不就一直啟用不了,結(jié)果會如何?可能會造成整個系統(tǒng)的終止。而且如果是多處理器的話,屏蔽中斷僅僅對執(zhí)行 另一方面,對內(nèi)核來說,當(dāng)它在執(zhí)行更新變量或列表的幾條指令期間將中斷屏蔽是很方便的。例如,如果多個進(jìn)程處理就緒列表中的時候發(fā)生中斷,則可能會發(fā)生競態(tài)條件的出現(xiàn)。所以,屏蔽中斷對于操作系統(tǒng)本身來說是一項(xiàng)很有用的技術(shù),但是對于用戶線程來說,屏蔽中斷卻不是一項(xiàng)通用的互斥機(jī)制。 鎖變量作為第二種嘗試,可以尋找一種軟件層面解決方案。考慮有單個共享的(鎖)變量,初始為值為 0 。當(dāng)一個線程想要進(jìn)入關(guān)鍵區(qū)域時,它首先會查看鎖的值是否為 0 ,如果鎖的值是 0 ,進(jìn)程會把它設(shè)置為 1 并讓進(jìn)程進(jìn)入關(guān)鍵區(qū)域。如果鎖的狀態(tài)是 1,進(jìn)程會等待直到鎖變量的值變?yōu)?0 。因此,鎖變量的值是 0 則意味著沒有線程進(jìn)入關(guān)鍵區(qū)域。如果是 1 則意味著有進(jìn)程在關(guān)鍵區(qū)域內(nèi)。我們對上圖修改后,如下所示 這種設(shè)計方式是否正確呢?是否存在紕漏呢?假設(shè)一個進(jìn)程讀出鎖變量的值并發(fā)現(xiàn)它為 0 ,而恰好在它將其設(shè)置為 1 之前,另一個進(jìn)程調(diào)度運(yùn)行,讀出鎖的變量為0 ,并將鎖的變量設(shè)置為 1 。然后第一個線程運(yùn)行,把鎖變量的值再次設(shè)置為 1,此時,臨界區(qū)域就會有兩個進(jìn)程在同時運(yùn)行。 也許有的讀者可以這么認(rèn)為,在進(jìn)入前檢查一次,在要離開的關(guān)鍵區(qū)域再檢查一次不就解決了嗎?實(shí)際上這種情況也是于事無補(bǔ),因?yàn)樵诘诙螜z查期間其他線程仍有可能修改鎖變量的值,換句話說,這種 嚴(yán)格輪詢法第三種互斥的方式先拋出來一段代碼,這里的程序是用 C 語言編寫,之所以采用 C 是因?yàn)椴僮飨到y(tǒng)普遍是用 C 來編寫的(偶爾會用 C++),而基本不會使用 Java 、Modula3 或 Pascal 這樣的語言,Java 中的 native 關(guān)鍵字底層也是 C 或 C++ 編寫的源碼。對于編寫操作系統(tǒng)而言,需要使用 C 語言這種強(qiáng)大、高效、可預(yù)知和有特性的語言,而對于 Java ,它是不可預(yù)知的,因?yàn)樗陉P(guān)鍵時刻會用完存儲器,而在不合適的時候會調(diào)用垃圾回收機(jī)制回收內(nèi)存。在 C 語言中,這種情況不會發(fā)生,C 語言中不會主動調(diào)用垃圾回收回收內(nèi)存。有關(guān) C 、C++ 、Java 和其他四種語言的比較可以參考 鏈接 進(jìn)程 0 的代碼
進(jìn)程 1 的代碼 while(TRUE){ 在上面代碼中,變量 進(jìn)程 0 離開臨界區(qū)時,它將 turn 的值設(shè)置為 1,以便允許進(jìn)程 1 進(jìn)入其臨界區(qū)。假設(shè)進(jìn)程 1 很快便離開了臨界區(qū),則此時兩個進(jìn)程都處于臨界區(qū)之外,turn 的值又被設(shè)置為 0 ?,F(xiàn)在進(jìn)程 0 很快就執(zhí)行完了整個循環(huán),它退出臨界區(qū),并將 turn 的值設(shè)置為 1。此時,turn 的值為 1,兩個進(jìn)程都在其臨界區(qū)外執(zhí)行。 突然,進(jìn)程 0 結(jié)束了非臨界區(qū)的操作并返回到循環(huán)的開始。但是,這時它不能進(jìn)入臨界區(qū),因?yàn)?turn 的當(dāng)前值為 1,此時進(jìn)程 1 還忙于非臨界區(qū)的操作,進(jìn)程 0 只能繼續(xù) while 循環(huán),直到進(jìn)程 1 把 turn 的值改為 0 。這說明,在一個進(jìn)程比另一個進(jìn)程執(zhí)行速度慢了很多的情況下,輪流進(jìn)入臨界區(qū)并不是一個好的方法。 這種情況違反了前面的敘述 3 ,即 位于臨界區(qū)外的進(jìn)程不得阻塞其他進(jìn)程,進(jìn)程 0 被一個臨界區(qū)外的進(jìn)程阻塞。由于違反了第三條,所以也不能作為一個好的方案。 Peterson 解法荷蘭數(shù)學(xué)家 T.Dekker 通過將鎖變量與警告變量相結(jié)合,最早提出了一個不需要嚴(yán)格輪換的軟件互斥算法,關(guān)于 Dekker 的算法,參考 鏈接 后來, G.L.Peterson 發(fā)現(xiàn)了一種簡單很多的互斥算法,它的算法如下
在使用共享變量時(即進(jìn)入其臨界區(qū))之前,各個進(jìn)程使用各自的進(jìn)程號 0 或 1 作為參數(shù)來調(diào)用 現(xiàn)在來看看這個辦法是如何工作的。一開始,沒有任何進(jìn)程處于臨界區(qū)中,現(xiàn)在進(jìn)程 0 調(diào)用 那么上面討論的是順序進(jìn)入的情況,現(xiàn)在來考慮一種兩個進(jìn)程同時調(diào)用 TSL 指令現(xiàn)在來看一種需要硬件幫助的方案。一些計算機(jī),特別是那些設(shè)計為多處理器的計算機(jī),都會有下面這條指令 TSL RX,LOCK 稱為 很重要的一點(diǎn)是鎖住內(nèi)存總線和禁用中斷不一樣。禁用中斷并不能保證一個處理器在讀寫操作之間另一個處理器對內(nèi)存的讀寫。也就是說,在處理器 1 上屏蔽中斷對處理器 2 沒有影響。讓處理器 2 遠(yuǎn)離內(nèi)存直到處理器 1 完成讀寫的最好的方式就是鎖住總線。這需要一個特殊的硬件(基本上,一根總線就可以確??偩€由鎖住它的處理器使用,而其他的處理器不能使用) 為了使用 TSL 指令,要使用一個共享變量 lock 來協(xié)調(diào)對共享內(nèi)存的訪問。當(dāng) lock 為 0 時,任何進(jìn)程都可以使用 TSL 指令將其設(shè)置為 1,并讀寫共享內(nèi)存。當(dāng)操作結(jié)束時,進(jìn)程使用 這條指令如何防止兩個進(jìn)程同時進(jìn)入臨界區(qū)呢?下面是解決方案
我們可以看到這個解決方案的思想和 Peterson 的思想很相似。假設(shè)存在如下共 4 指令的匯編語言程序。第一條指令將 lock 原來的值復(fù)制到寄存器中并將 lock 設(shè)置為 1 ,隨后這個原來的值和 0 做對比。如果它不是零,說明之前已經(jīng)被加過鎖,則程序返回到開始并再次測試。經(jīng)過一段時間后(可長可短),該值變?yōu)?0 (當(dāng)前處于臨界區(qū)中的進(jìn)程退出臨界區(qū)時),于是過程返回,此時已加鎖。要清除這個鎖也比較簡單,程序只需要將 0 存入 lock 即可,不需要特殊的同步指令。 現(xiàn)在有了一種很明確的做法,那就是進(jìn)程在進(jìn)入臨界區(qū)之前會先調(diào)用 還有一個可以替換 TSL 的指令是 enter_region: XCHG 的本質(zhì)上與 TSL 的解決辦法一樣。所有的 Intel x86 CPU 在底層同步中使用 XCHG 指令。 睡眠與喚醒上面解法中的 Peterson 、TSL 和 XCHG 解法都是正確的,但是它們都有忙等待的缺點(diǎn)。這些解法的本質(zhì)上都是一樣的,先檢查是否能夠進(jìn)入臨界區(qū),若不允許,則該進(jìn)程將原地等待,直到允許為止。 這種方式不但浪費(fèi)了 CPU 時間,而且還可能引起意想不到的結(jié)果。考慮一臺計算機(jī)上有兩個進(jìn)程,這兩個進(jìn)程具有不同的優(yōu)先級, 現(xiàn)在讓我們看一下進(jìn)程間的通信原語,這些原語在不允許它們進(jìn)入關(guān)鍵區(qū)域之前會阻塞而不是浪費(fèi) CPU 時間,最簡單的是 生產(chǎn)者-消費(fèi)者問題作為這些私有原語的例子,讓我們考慮 如果緩沖隊(duì)列已滿,那么當(dāng)生產(chǎn)者仍想要將數(shù)據(jù)寫入緩沖區(qū)的時候,會出現(xiàn)問題。它的解決辦法是讓生產(chǎn)者睡眠,也就是阻塞生產(chǎn)者。等到消費(fèi)者從緩沖區(qū)中取出一個或多個數(shù)據(jù)項(xiàng)時再喚醒它。同樣的,當(dāng)消費(fèi)者試圖從緩沖區(qū)中取數(shù)據(jù),但是發(fā)現(xiàn)緩沖區(qū)為空時,消費(fèi)者也會睡眠,阻塞。直到生產(chǎn)者向其中放入一個新的數(shù)據(jù)。 這個邏輯聽起來比較簡單,而且這種方式也需要一種稱作
為了在 C 語言中描述像是 現(xiàn)在讓我們回到生產(chǎn)者-消費(fèi)者問題上來,上面代碼中會產(chǎn)生競爭條件,因?yàn)?count 這個變量是暴露在大眾視野下的。有可能出現(xiàn)下面這種情況:緩沖區(qū)為空,此時消費(fèi)者剛好讀取 count 的值發(fā)現(xiàn)它為 0 。此時調(diào)度程序決定暫停消費(fèi)者并啟動運(yùn)行生產(chǎn)者。生產(chǎn)者生產(chǎn)了一條數(shù)據(jù)并把它放在緩沖區(qū)中,然后增加 count 的值,并注意到它的值是 1 。由于 count 為 0,消費(fèi)者必須處于睡眠狀態(tài),因此生產(chǎn)者調(diào)用 引起上面問題的本質(zhì)是 喚醒尚未進(jìn)行睡眠狀態(tài)的進(jìn)程會導(dǎo)致喚醒丟失。如果它沒有丟失,則一切都很正常。一種快速解決上面問題的方式是增加一個 然而,當(dāng)進(jìn)程數(shù)量有許多的時候,這時你可以說通過增加喚醒等待位的數(shù)量來喚醒等待位,于是就有了 2、4、6、8 個喚醒等待位,但是并沒有從根本上解決問題。 信號量信號量是 E.W.Dijkstra 在 1965 年提出的一種方法,它使用一個整形變量來累計喚醒次數(shù),以供之后使用。在他的觀點(diǎn)中,有一個新的變量類型稱作 Dijkstra 提出了信號量有兩個操作,現(xiàn)在通常使用
up 操作會使信號量的值 + 1。如果一個或者多個進(jìn)程在信號量上睡眠,無法完成一個先前的 down 操作,則由系統(tǒng)選擇其中一個并允許該程完成 down 操作。因此,對一個進(jìn)程在其上睡眠的信號量執(zhí)行一次 up 操作之后,該信號量的值仍然是 0 ,但在其上睡眠的進(jìn)程卻少了一個。信號量的值增 1 和喚醒一個進(jìn)程同樣也是不可分割的。不會有某個進(jìn)程因執(zhí)行 up 而阻塞,正如在前面的模型中不會有進(jìn)程因執(zhí)行 wakeup 而阻塞是一樣的道理。 用信號量解決生產(chǎn)者 - 消費(fèi)者問題用信號量解決丟失的 wakeup 問題,代碼如下 /* 定義緩沖區(qū)槽的數(shù)量 */ 為了確保信號量能正確工作,最重要的是要采用一種不可分割的方式來實(shí)現(xiàn)它。通常是將 up 和 down 作為系統(tǒng)調(diào)用來實(shí)現(xiàn)。而且操作系統(tǒng)只需在執(zhí)行以下操作時暫時屏蔽全部中斷:檢查信號量、更新、必要時使進(jìn)程睡眠。由于這些操作僅需要非常少的指令,因此中斷不會造成影響。如果使用多個 CPU,那么信號量應(yīng)該被鎖進(jìn)行保護(hù)。使用 TSL 或者 XCHG 指令用來確保同一時刻只有一個 CPU 對信號量進(jìn)行操作。 使用 TSL 或者 XCHG 來防止幾個 CPU 同時訪問一個信號量,與生產(chǎn)者或消費(fèi)者使用忙等待來等待其他騰出或填充緩沖區(qū)是完全不一樣的。前者的操作僅需要幾個毫秒,而生產(chǎn)者或消費(fèi)者可能需要任意長的時間。 上面這個解決方案使用了三種信號量:一個稱為 full,用來記錄充滿的緩沖槽數(shù)目;一個稱為 empty,記錄空的緩沖槽數(shù)目;一個稱為 mutex,用來確保生產(chǎn)者和消費(fèi)者不會同時進(jìn)入緩沖區(qū)。 現(xiàn)在我們有了一個好的進(jìn)程間原語的保證。然后我們再來看一下中斷的順序保證
在使用 上面的代碼實(shí)際上是通過兩種不同的方式來使用信號量的,而這兩種信號量之間的區(qū)別也是很重要的。 另外一個信號量是關(guān)于 互斥量如果不需要信號量的計數(shù)能力時,可以使用信號量的一個簡單版本,稱為 互斥量是一個處于兩種狀態(tài)之一的共享變量: mutex 使用兩個過程,當(dāng)一個線程(或者進(jìn)程)需要訪問關(guān)鍵區(qū)域時,會調(diào)用 另一方面,如果 mutex 互斥量已經(jīng)鎖定的話,調(diào)用線程會阻塞直到關(guān)鍵區(qū)域內(nèi)的線程執(zhí)行完畢并且調(diào)用了 由于 mutex 互斥量非常簡單,所以只要有 TSL 或者是 XCHG 指令,就可以很容易地在用戶空間實(shí)現(xiàn)它們。用于用戶級線程包的
mutex_lock 的代碼和上面 enter_region 的代碼很相似,我們可以對比著看一下 上面代碼最大的區(qū)別你看出來了嗎?
上面就是 enter_region 和 mutex_lock 的差別所在。由于 thread_yield 僅僅是一個用戶空間的線程調(diào)度,所以它的運(yùn)行非常快捷。這樣, 我們上面描述的互斥量其實(shí)是一套調(diào)用框架中的指令。從軟件角度來說,總是需要更多的特性和同步原語。例如,有時線程包提供一個調(diào)用 Futexes隨著并行的增加,有效的 有一種有趣的解決方案是把兩者的優(yōu)點(diǎn)結(jié)合起來,提出一種新的思想,稱為 futex 是 對于一個進(jìn)程來說,把它放到等待隊(duì)列需要昂貴的系統(tǒng)調(diào)用,這種方式應(yīng)該被避免。在沒有競爭的情況下,futex 可以直接在用戶空間中工作。這些進(jìn)程共享一個 32 位 Pthreads 中的互斥量Pthreads 提供了一些功能用來同步線程。最基本的機(jī)制是使用互斥量變量,可以鎖定和解鎖,用來保護(hù)每個關(guān)鍵區(qū)域。希望進(jìn)入關(guān)鍵區(qū)域的線程首先要嘗試獲取 mutex。如果 mutex 沒有加鎖,線程能夠馬上進(jìn)入并且互斥量能夠自動鎖定,從而阻止其他線程進(jìn)入。如果 mutex 已經(jīng)加鎖,調(diào)用線程會阻塞,直到 mutex 解鎖。如果多個線程在相同的互斥量上等待,當(dāng)互斥量解鎖時,只有一個線程能夠進(jìn)入并且重新加鎖。這些鎖并不是必須的,程序員需要正確使用它們。 下面是與互斥量有關(guān)的函數(shù)調(diào)用 像我們想象中的一樣,mutex 能夠被創(chuàng)建和銷毀,扮演這兩個角色的分別是 除了互斥量以外, 下面再來重新認(rèn)識一下生產(chǎn)者和消費(fèi)者問題:一個線程將東西放在一個緩沖區(qū)內(nèi),由另一個線程將它們?nèi)〕?。如果生產(chǎn)者發(fā)現(xiàn)緩沖區(qū)沒有空槽可以使用了,生產(chǎn)者線程會阻塞起來直到有一個線程可以使用。生產(chǎn)者使用 mutex 來進(jìn)行原子性檢查從而不受其他線程干擾。但是當(dāng)發(fā)現(xiàn)緩沖區(qū)已經(jīng)滿了以后,生產(chǎn)者需要一種方法來阻塞自己并在以后被喚醒。這便是條件變量做的工作。 下面是一些與條件變量有關(guān)的最重要的 pthread 調(diào)用 上表中給出了一些調(diào)用用來創(chuàng)建和銷毀條件變量。條件變量上的主要屬性是
下面是一個使用互斥量和條件變量的例子 #include <stdio.h> 管程為了能夠編寫更加準(zhǔn)確無誤的程序,Brinch Hansen 和 Hoare 提出了一個更高級的同步原語叫做
管程有一個很重要的特性,即在任何時候管程中只能有一個活躍的進(jìn)程,這一特性使管程能夠很方便的實(shí)現(xiàn)互斥操作。管程是編程語言的特性,所以編譯器知道它們的特殊性,因此可以采用與其他過程調(diào)用不同的方法來處理對管程的調(diào)用。通常情況下,當(dāng)進(jìn)程調(diào)用管程中的程序時,該程序的前幾條指令會檢查管程中是否有其他活躍的進(jìn)程。如果有的話,調(diào)用進(jìn)程將被掛起,直到另一個進(jìn)程離開管程才將其喚醒。如果沒有活躍進(jìn)程在使用管程,那么該調(diào)用進(jìn)程才可以進(jìn)入。 進(jìn)入管程中的互斥由編譯器負(fù)責(zé),但是一種通用做法是使用 即使管程提供了一種簡單的方式來實(shí)現(xiàn)互斥,但在我們看來,這還不夠。因?yàn)槲覀冞€需要一種在進(jìn)程無法執(zhí)行被阻塞。在生產(chǎn)者-消費(fèi)者問題中,很容易將針對緩沖區(qū)滿和緩沖區(qū)空的測試放在管程程序中,但是生產(chǎn)者在發(fā)現(xiàn)緩沖區(qū)滿的時候該如何阻塞呢? 解決的辦法是引入
如果在一個條件變量上有若干進(jìn)程都在等待,則在對該條件執(zhí)行 signal 操作后,系統(tǒng)調(diào)度程序只能選擇其中一個進(jìn)程恢復(fù)運(yùn)行。 順便提一下,這里還有上面兩位教授沒有提出的第三種方式,它的理論是讓執(zhí)行 signal 的進(jìn)程繼續(xù)運(yùn)行,等待這個進(jìn)程退出管程時,其他進(jìn)程才能進(jìn)入管程。 條件變量不是計數(shù)器。條件變量也不能像信號量那樣積累信號以便以后使用。所以,如果向一個條件變量發(fā)送信號,但是該條件變量上沒有等待進(jìn)程,那么信號將會丟失。也就是說,wait 操作必須在 signal 之前執(zhí)行。 下面是一個使用 monitor ProducerConsumer 讀者可能覺得 wait 和 signal 操作看起來像是前面提到的 sleep 和 wakeup ,而且后者存在嚴(yán)重的競爭條件。它們確實(shí)很像,但是有個關(guān)鍵的區(qū)別:sleep 和 wakeup 之所以會失敗是因?yàn)楫?dāng)一個進(jìn)程想睡眠時,另一個進(jìn)程試圖去喚醒它。使用管程則不會發(fā)生這種情況。管程程序的自動互斥保證了這一點(diǎn),如果管程過程中的生產(chǎn)者發(fā)現(xiàn)緩沖區(qū)已滿,它將能夠完成 wait 操作而不用擔(dān)心調(diào)度程序可能會在 wait 完成之前切換到消費(fèi)者。甚至,在 wait 執(zhí)行完成并且把生產(chǎn)者標(biāo)志為不可運(yùn)行之前,是不會允許消費(fèi)者進(jìn)入管程的。 盡管類 Pascal 是一種想象的語言,但還是有一些真正的編程語言支持,比如 Java (終于輪到大 Java 出場了),Java 是能夠支持管程的,它是一種 下面是 Java 使用管程解決的生產(chǎn)者-消費(fèi)者問題
上面的代碼中主要設(shè)計四個類, 在前面的所有例子中,生產(chǎn)者和消費(fèi)者線程在功能上與它們是相同的。生產(chǎn)者有一個無限循環(huán),該無限循環(huán)產(chǎn)生數(shù)據(jù)并將數(shù)據(jù)放入公共緩沖區(qū)中;消費(fèi)者也有一個等價的無限循環(huán),該無限循環(huán)用于從緩沖區(qū)取出數(shù)據(jù)并完成一系列工作。 程序中比較耐人尋味的就是 Java 中的同步方法與其他經(jīng)典管程有本質(zhì)差別:Java 沒有內(nèi)嵌的條件變量。然而,Java 提供了 wait 和 notify 分別與 sleep 和 wakeup 等價。 通過臨界區(qū)自動的互斥,管程比信號量更容易保證并行編程的正確性。但是管程也有缺點(diǎn),我們前面說到過管程是一個編程語言的概念,編譯器必須要識別管程并用某種方式對其互斥作出保證。C、Pascal 以及大多數(shù)其他編程語言都沒有管程,所以不能依靠編譯器來遵守互斥規(guī)則。 與管程和信號量有關(guān)的另一個問題是,這些機(jī)制都是設(shè)計用來解決訪問共享內(nèi)存的一個或多個 CPU 上的互斥問題的。通過將信號量放在共享內(nèi)存中并用 消息傳遞上面提到的其他方法就是 send(destination, &message); send 方法用于向一個給定的目標(biāo)發(fā)送一條消息,receive 從一個給定的源接受一條消息。如果沒有消息,接受者可能被阻塞,直到接受一條消息或者帶著錯誤碼返回。 消息傳遞系統(tǒng)的設(shè)計要點(diǎn)消息傳遞系統(tǒng)現(xiàn)在面臨著許多信號量和管程所未涉及的問題和設(shè)計難點(diǎn),尤其對那些在網(wǎng)絡(luò)中不同機(jī)器上的通信狀況。例如,消息有可能被網(wǎng)絡(luò)丟失。為了防止消息丟失,發(fā)送方和接收方可以達(dá)成一致:一旦接受到消息后,接收方馬上回送一條特殊的 現(xiàn)在考慮消息本身被正確接收,而返回給發(fā)送著的確認(rèn)消息丟失的情況。發(fā)送者將重發(fā)消息,這樣接受者將收到兩次相同的消息。 對于接收者來說,如何區(qū)分新的消息和一條重發(fā)的老消息是非常重要的。通常采用在每條原始消息中嵌入一個連續(xù)的序號來解決此問題。如果接受者收到一條消息,它具有與前面某一條消息一樣的序號,就知道這條消息是重復(fù)的,可以忽略。 消息系統(tǒng)還必須處理如何命名進(jìn)程的問題,以便在發(fā)送或接收調(diào)用中清晰的指明進(jìn)程。 用消息傳遞解決生產(chǎn)者-消費(fèi)者問題現(xiàn)在我們考慮如何使用消息傳遞來解決生產(chǎn)者-消費(fèi)者問題,而不是共享緩存。下面是一種解決方式
假設(shè)所有的消息都有相同的大小,并且在尚未接受到發(fā)出的消息時,由操作系統(tǒng)自動進(jìn)行緩沖。在該解決方案中共使用 N 條消息,這就類似于一塊共享內(nèi)存緩沖區(qū)的 N 個槽。消費(fèi)者首先將 N 條空消息發(fā)送給生產(chǎn)者。當(dāng)生產(chǎn)者向消費(fèi)者傳遞一個數(shù)據(jù)項(xiàng)時,它取走一條空消息并返回一條填充了內(nèi)容的消息。通過這種方式,系統(tǒng)中總的消息數(shù)量保持不變,所以消息都可以存放在事先確定數(shù)量的內(nèi)存中。 如果生產(chǎn)者的速度要比消費(fèi)者快,則所有的消息最終都將被填滿,等待消費(fèi)者,生產(chǎn)者將被阻塞,等待返回一條空消息。如果消費(fèi)者速度快,那么情況將正相反:所有的消息均為空,等待生產(chǎn)者來填充,消費(fèi)者將被阻塞,以等待一條填充過的消息。 消息傳遞的方式有許多變體,下面先介紹如何對消息進(jìn)行
屏障最后一個同步機(jī)制是準(zhǔn)備用于進(jìn)程組而不是進(jìn)程間的生產(chǎn)者-消費(fèi)者情況的。在某些應(yīng)用中劃分了若干階段,并且規(guī)定,除非所有的進(jìn)程都就緒準(zhǔn)備著手下一個階段,否則任何進(jìn)程都不能進(jìn)入下一個階段,可以通過在每個階段的結(jié)尾安裝一個 在上圖中我們可以看到,有四個進(jìn)程接近屏障,這意味著每個進(jìn)程都在進(jìn)行運(yùn)算,但是還沒有到達(dá)每個階段的結(jié)尾。過了一段時間后,A、B、D 三個進(jìn)程都到達(dá)了屏障,各自的進(jìn)程被掛起,但此時還不能進(jìn)入下一個階段呢,因?yàn)檫M(jìn)程 B 還沒有執(zhí)行完畢。結(jié)果,當(dāng)最后一個 C 到達(dá)屏障后,這個進(jìn)程組才能夠進(jìn)入下一個階段。 避免鎖:讀-復(fù)制-更新最快的鎖是根本沒有鎖。問題在于沒有鎖的情況下,我們是否允許對共享數(shù)據(jù)結(jié)構(gòu)的并發(fā)讀寫進(jìn)行訪問。答案當(dāng)然是不可以。假設(shè)進(jìn)程 A 正在對一個數(shù)字?jǐn)?shù)組進(jìn)行排序,而進(jìn)程 B 正在計算其平均值,而此時你進(jìn)行 A 的移動,會導(dǎo)致 B 會多次讀到重復(fù)值,而某些值根本沒有遇到過。 然而,在某些情況下,我們可以允許寫操作來更新數(shù)據(jù)結(jié)構(gòu),即便還有其他的進(jìn)程正在使用。竅門在于確保每個讀操作要么讀取舊的版本,要么讀取新的版本,例如下面的樹 上面的樹中,讀操作從根部到葉子遍歷整個樹。加入一個新節(jié)點(diǎn) X 后,為了實(shí)現(xiàn)這一操作,我們要讓這個節(jié)點(diǎn)在樹中可見之前使它恰好正確:我們對節(jié)點(diǎn) X 中的所有值進(jìn)行初始化,包括它的子節(jié)點(diǎn)指針。然后通過原子寫操作,使 X 稱為 A 的子節(jié)點(diǎn)。所有的讀操作都不會讀到前后不一致的版本 在上面的圖中,我們接著移除 B 和 D。首先,將 A 的左子節(jié)點(diǎn)指針指向 C 。所有原本在 A 中的讀操作將會后續(xù)讀到節(jié)點(diǎn) C ,而永遠(yuǎn)不會讀到 B 和 D。也就是說,它們將只會讀取到新版數(shù)據(jù)。同樣,所有當(dāng)前在 B 和 D 中的讀操作將繼續(xù)按照原始的數(shù)據(jù)結(jié)構(gòu)指針并且讀取舊版數(shù)據(jù)。所有操作均能正確運(yùn)行,我們不需要鎖住任何東西。而不需要鎖住數(shù)據(jù)就能夠移除 B 和 D 的主要原因就是 調(diào)度當(dāng)一個計算機(jī)是多道程序設(shè)計系統(tǒng)時,會頻繁的有很多進(jìn)程或者線程來同時競爭 CPU 時間片。當(dāng)兩個或兩個以上的進(jìn)程/線程處于就緒狀態(tài)時,就會發(fā)生這種情況。如果只有一個 CPU 可用,那么必須選擇接下來哪個進(jìn)程/線程可以運(yùn)行。操作系統(tǒng)中有一個叫做 盡管有一些不同,但許多適用于進(jìn)程調(diào)度的處理方法同樣也適用于線程調(diào)度。當(dāng)內(nèi)核管理線程的時候,調(diào)度通常會以線程級別發(fā)生,很少或者根本不會考慮線程屬于哪個進(jìn)程。下面我們會首先專注于進(jìn)程和線程的調(diào)度問題,然后會明確的介紹線程調(diào)度以及它產(chǎn)生的問題。 調(diào)度介紹讓我們回到早期以磁帶上的卡片作為輸入的批處理系統(tǒng)的時代,那時候的調(diào)度算法非常簡單:依次運(yùn)行磁帶上的每一個作業(yè)。對于多道程序設(shè)計系統(tǒng),會復(fù)雜一些,因?yàn)橥ǔ卸鄠€用戶在等待服務(wù)。一些大型機(jī)仍然將 進(jìn)程行為幾乎所有的進(jìn)程(磁盤或網(wǎng)絡(luò))I/O 請求和計算都是交替運(yùn)行的 如上圖所示,CPU 不停頓的運(yùn)行一段時間,然后發(fā)出一個系統(tǒng)調(diào)用等待 I/O 讀寫文件。完成系統(tǒng)調(diào)用后,CPU 又開始計算,直到它需要讀更多的數(shù)據(jù)或者寫入更多的數(shù)據(jù)為止。當(dāng)一個進(jìn)程等待外部設(shè)備完成工作而被阻塞時,才是 I/O 活動。 上面 a 是 CPU 密集型進(jìn)程;b 是 I/O 密集型進(jìn)程進(jìn)程,a 因?yàn)樵谟嬎愕臅r間上花費(fèi)時間更長,因此稱為 值得注意的是,隨著 CPU 的速度越來越快,更多的進(jìn)程傾向于 I/O 密集型。這種情況出現(xiàn)的原因是 CPU 速度的提升要遠(yuǎn)遠(yuǎn)高于硬盤。這種情況導(dǎo)致的結(jié)果是,未來對 I/O 密集型進(jìn)程的調(diào)度處理似乎更為重要。這里的基本思想是,如果需要運(yùn)行 I/O 密集型進(jìn)程,那么就應(yīng)該讓它盡快得到機(jī)會,以便發(fā)出磁盤請求并保持磁盤始終忙碌。 何時調(diào)度第一個和調(diào)度有關(guān)的問題是 第二,在進(jìn)程退出時需要作出調(diào)度決定。因?yàn)榇诉M(jìn)程不再運(yùn)行(因?yàn)樗鼘⒉辉俅嬖冢?,因此必須從就緒進(jìn)程中選擇其他進(jìn)程運(yùn)行。如果沒有進(jìn)程處于就緒態(tài),系統(tǒng)提供的 什么是空閑進(jìn)程
第三種情況是,當(dāng)進(jìn)程阻塞在 I/O 、信號量或其他原因時,必須選擇另外一個進(jìn)程來運(yùn)行。有時,阻塞的原因會成為選擇進(jìn)程運(yùn)行的關(guān)鍵因素。例如,如果 A 是一個重要進(jìn)程,并且它正在等待 B 退出關(guān)鍵區(qū)域,讓 B 退出關(guān)鍵區(qū)域從而使 A 得以運(yùn)行。但是調(diào)度程序一般不會對這種情況進(jìn)行考量。 第四點(diǎn),當(dāng) I/O 中斷發(fā)生時,可以做出調(diào)度決策。如果中斷來自 I/O 設(shè)備,而 I/O 設(shè)備已經(jīng)完成了其工作,那么那些等待 I/O 的進(jìn)程現(xiàn)在可以繼續(xù)運(yùn)行。由調(diào)度程序來決定是否準(zhǔn)備運(yùn)行新的進(jìn)程還是重新運(yùn)行已經(jīng)中斷的進(jìn)程。 如果硬件時鐘以 50 或 60 Hz 或其他頻率提供周期性中斷,可以在每個時鐘中斷或第 k 個時鐘中斷處做出調(diào)度決策。根據(jù)如何處理時鐘中斷可以把調(diào)度算法可以分為兩類。 另外一種情況是 調(diào)度算法的分類毫無疑問,不同的環(huán)境下需要不同的調(diào)度算法。之所以出現(xiàn)這種情況,是因?yàn)椴煌膽?yīng)用程序和不同的操作系統(tǒng)有不同的目標(biāo)。也就是說,在不同的系統(tǒng)中,調(diào)度程序的優(yōu)化也是不同的。這里有必要劃分出三種環(huán)境
批處理系統(tǒng)廣泛應(yīng)用于商業(yè)領(lǐng)域,比如用來處理工資單、存貨清單、賬目收入、賬目支出、利息計算、索賠處理和其他周期性作業(yè)。在批處理系統(tǒng)中,一般會選擇使用非搶占式算法或者周期性比較長的搶占式算法。這種方法可以減少線程切換因此能夠提升性能。 在交互式用戶環(huán)境中,為了避免一個進(jìn)程霸占 CPU 拒絕為其他進(jìn)程服務(wù),所以需要搶占式算法。即使沒有進(jìn)程有意要一直運(yùn)行下去,但是,由于某個進(jìn)程出現(xiàn)錯誤也有可能無限期的排斥其他所有進(jìn)程。為了避免這種情況,搶占式也是必須的。服務(wù)器也屬于此類別,因?yàn)樗鼈兺ǔ槎鄠€(遠(yuǎn)程)用戶提供服務(wù),而這些用戶都非常著急。計算機(jī)用戶總是很忙。 在實(shí)時系統(tǒng)中,搶占有時是不需要的,因?yàn)檫M(jìn)程知道自己可能運(yùn)行不了很長時間,通常很快的做完自己的工作并阻塞。實(shí)時系統(tǒng)與交互式系統(tǒng)的差別是,實(shí)時系統(tǒng)只運(yùn)行那些用來推進(jìn)現(xiàn)有應(yīng)用的程序,而交互式系統(tǒng)是通用的,它可以運(yùn)行任意的非協(xié)作甚至是有惡意的程序。 調(diào)度算法的目標(biāo)為了設(shè)計調(diào)度算法,有必要考慮一下什么是好的調(diào)度算法。有一些目標(biāo)取決于環(huán)境(批處理、交互式或者實(shí)時)蛋大部分是適用于所有情況的,下面是一些需要考量的因素,我們會在下面一起討論。 所有系統(tǒng) 在所有的情況中, 與公平有關(guān)的是系統(tǒng)的 另一個共同的目標(biāo)是保持系統(tǒng)的 批處理系統(tǒng) 通常有三個指標(biāo)來衡量系統(tǒng)工作狀態(tài):吞吐量、周轉(zhuǎn)時間和 CPU 利用率,
交互式系統(tǒng) 對于交互式系統(tǒng),則有不同的指標(biāo)。最重要的是盡量 一個相關(guān)的問題是 實(shí)時系統(tǒng) 實(shí)時系統(tǒng)則有著和交互式系統(tǒng)不同的考量因素,因此也就有不同的調(diào)度目標(biāo)。實(shí)時系統(tǒng)的特點(diǎn)是 在一些實(shí)事系統(tǒng)中,特別是涉及到多媒體的, 批處理中的調(diào)度現(xiàn)在讓我們把目光從一般性的調(diào)度轉(zhuǎn)換為特定的調(diào)度算法。下面我們會探討在批處理中的調(diào)度。 先來先服務(wù)很像是先到先得。。。可能最簡單的非搶占式調(diào)度算法的設(shè)計就是 這個算法的強(qiáng)大之處在于易于理解和編程,在這個算法中,一個單鏈表記錄了所有就緒進(jìn)程。要選取一個進(jìn)程運(yùn)行,只要從該隊(duì)列的頭部移走一個進(jìn)程即可;要添加一個新的作業(yè)或者阻塞一個進(jìn)程,只要把這個作業(yè)或進(jìn)程附加在隊(duì)列的末尾即可。這是很簡單的一種實(shí)現(xiàn)。 不過,先來先服務(wù)也是有缺點(diǎn)的,那就是沒有優(yōu)先級的關(guān)系,試想一下,如果有 100 個 I/O 進(jìn)程正在排隊(duì),第 101 個是一個 CPU 密集型進(jìn)程,那豈不是需要等 100 個 I/O 進(jìn)程運(yùn)行完畢才會等到一個 CPU 密集型進(jìn)程運(yùn)行,這在實(shí)際情況下根本不可能,所以需要優(yōu)先級或者搶占式進(jìn)程的出現(xiàn)來優(yōu)先選擇重要的進(jìn)程運(yùn)行。 最短作業(yè)優(yōu)先批處理中,第二種調(diào)度算法是 如上圖 a 所示,這里有 4 個作業(yè) A、B、C、D ,運(yùn)行時間分別為 8、4、4、4 分鐘。若按圖中的次序運(yùn)行,則 A 的周轉(zhuǎn)時間為 8 分鐘,B 為 12 分鐘,C 為 16 分鐘,D 為 20 分鐘,平均時間內(nèi)為 14 分鐘。 現(xiàn)在考慮使用最短作業(yè)優(yōu)先算法運(yùn)行 4 個作業(yè),如上圖 b 所示,目前的周轉(zhuǎn)時間分別為 4、8、12、20,平均為 11 分鐘,可以證明最短作業(yè)優(yōu)先是最優(yōu)的。考慮有 4 個作業(yè)的情況,其運(yùn)行時間分別為 a、b、c、d。第一個作業(yè)在時間 a 結(jié)束,第二個在時間 a + b 結(jié)束,以此類推。平均周轉(zhuǎn)時間為 (4a + 3b + 2c + d) / 4 。顯然 a 對平均值的影響最大,所以 a 應(yīng)該是最短優(yōu)先作業(yè),其次是 b,然后是 c ,最后是 d 它就只能影響自己的周轉(zhuǎn)時間了。
最短剩余時間優(yōu)先最短作業(yè)優(yōu)先的搶占式版本被稱作為 交互式系統(tǒng)中的調(diào)度交互式系統(tǒng)中在個人計算機(jī)、服務(wù)器和其他系統(tǒng)中都是很常用的,所以有必要來探討一下交互式調(diào)度 輪詢調(diào)度一種最古老、最簡單、最公平并且最廣泛使用的算法就是 時間片輪詢調(diào)度中唯一有意思的一點(diǎn)就是時間片的長度。從一個進(jìn)程切換到另一個進(jìn)程需要一定的時間進(jìn)行管理處理,包括保存寄存器的值和內(nèi)存映射、更新不同的表格和列表、清除和重新調(diào)入內(nèi)存高速緩存等。這種切換稱作 為了提高 CPU 的效率,我們把時間片設(shè)置為 100 ms?,F(xiàn)在時間的浪費(fèi)只有 1%。但是考慮會發(fā)現(xiàn)下面的情況,如果在一個非常短的時間內(nèi)到達(dá) 50 個請求,并且對 CPU 有不同的需求,此時會發(fā)生什么?50 個進(jìn)程都被放在可運(yùn)行進(jìn)程列表中。如果 CP畫U 是空閑的,第一個進(jìn)程會立即開始執(zhí)行,第二個直到 100 ms 以后才會啟動,以此類推。不幸的是最后一個進(jìn)程需要等待 5 秒才能獲得執(zhí)行機(jī)會。大部分用戶都會覺得對于一個簡短的指令運(yùn)行 5 秒中是很慢的。如果隊(duì)列末尾的某些請求只需要幾號秒鐘的運(yùn)行時間的話,這種設(shè)計就非常糟糕了。 另外一個因素是如果時間片設(shè)置長度要大于 CPU 使用長度,那么搶占就不會經(jīng)常發(fā)生。相反,在時間片用完之前,大多數(shù)進(jìn)程都已經(jīng)阻塞了,那么就會引起進(jìn)程間的切換。消除搶占可提高性能,因?yàn)檫M(jìn)程切換僅在邏輯上必要時才發(fā)生,即流程阻塞且無法繼續(xù)時才發(fā)生。 結(jié)論可以表述如下:將上下文切換時間設(shè)置得太短會導(dǎo)致過多的進(jìn)程切換并降低 CPU 效率,但設(shè)置時間太長會導(dǎo)致一個短請求很長時間得不到響應(yīng)。最好的切換時間是在 20 - 50 毫秒之間設(shè)置。 優(yōu)先級調(diào)度輪詢調(diào)度假設(shè)了所有的進(jìn)程是同等重要的。但事實(shí)情況可能不是這樣。例如,在一所大學(xué)中的等級制度,首先是院長,然后是教授、秘書、后勤人員,最后是學(xué)生。這種將外部情況考慮在內(nèi)就實(shí)現(xiàn)了 它的基本思想很明確,每個進(jìn)程都被賦予一個優(yōu)先級,優(yōu)先級高的進(jìn)程優(yōu)先運(yùn)行。 但是也不意味著高優(yōu)先級的進(jìn)程能夠永遠(yuǎn)一直運(yùn)行下去,調(diào)度程序會在每個時鐘中斷期間降低當(dāng)前運(yùn)行進(jìn)程的優(yōu)先級。如果此操作導(dǎo)致其優(yōu)先級降低到下一個最高進(jìn)程的優(yōu)先級以下,則會發(fā)生進(jìn)程切換。或者,可以為每個進(jìn)程分配允許運(yùn)行的最大時間間隔。當(dāng)時間間隔用完后,下一個高優(yōu)先級的進(jìn)程會得到運(yùn)行的機(jī)會。 可以靜態(tài)或者動態(tài)的為進(jìn)程分配優(yōu)先級。在一臺軍用計算機(jī)上,可以把將軍所啟動的進(jìn)程設(shè)為優(yōu)先級 100,上校為 90 ,少校為 80,上尉為 70,中尉為 60,以此類推。UNIX 中有一條命令為 優(yōu)先級也可以由系統(tǒng)動態(tài)分配,用于實(shí)現(xiàn)某種目的。例如,有些進(jìn)程為 I/O 密集型,其多數(shù)時間用來等待 I/O 結(jié)束。當(dāng)這樣的進(jìn)程需要 CPU 時,應(yīng)立即分配 CPU,用來啟動下一個 I/O 請求,這樣就可以在另一個進(jìn)程進(jìn)行計算的同時執(zhí)行 I/O 操作。這類 I/O 密集型進(jìn)程長時間的等待 CPU 只會造成它長時間占用內(nèi)存。使 I/O 密集型進(jìn)程獲得較好的服務(wù)的一種簡單算法是,將其優(yōu)先級設(shè)為 可以很方便的將一組進(jìn)程按優(yōu)先級分成若干類,并且在各個類之間采用優(yōu)先級調(diào)度,而在各類進(jìn)程的內(nèi)部采用輪轉(zhuǎn)調(diào)度。下面展示了一個四個優(yōu)先級類的系統(tǒng) 它的調(diào)度算法主要描述如下:上面存在優(yōu)先級為 4 類的可運(yùn)行進(jìn)程,首先會按照輪轉(zhuǎn)法為每個進(jìn)程運(yùn)行一個時間片,此時不理會較低優(yōu)先級的進(jìn)程。若第 4 類進(jìn)程為空,則按照輪詢的方式運(yùn)行第三類進(jìn)程。若第 4 類和第 3 類進(jìn)程都為空,則按照輪轉(zhuǎn)法運(yùn)行第 2 類進(jìn)程。如果不對優(yōu)先級進(jìn)行調(diào)整,則低優(yōu)先級的進(jìn)程很容易產(chǎn)生饑餓現(xiàn)象。 多級隊(duì)列最早使用優(yōu)先級調(diào)度的系統(tǒng)是
CTSS 在每次切換前都需要將當(dāng)前進(jìn)程換出到磁盤,并從磁盤上讀入一個新進(jìn)程。CTSS 的設(shè)計者很快就認(rèn)識到,為 CPU 密集型進(jìn)程設(shè)置較長的時間片比頻繁地分給他們很短的時間要更有效(減少交換次數(shù))。另一方面,如前所述,長時間片的進(jìn)程又會影響到響應(yīng)時間,解決辦法是設(shè)置優(yōu)先級類。屬于最高優(yōu)先級的進(jìn)程運(yùn)行一個時間片,次高優(yōu)先級進(jìn)程運(yùn)行 2 個時間片,再下面一級運(yùn)行 4 個時間片,以此類推。當(dāng)一個進(jìn)程用完分配的時間片后,它被移到下一類。 最短進(jìn)程優(yōu)先對于批處理系統(tǒng)而言,由于最短作業(yè)優(yōu)先常常伴隨著最短響應(yīng)時間,所以如果能夠把它用于交互式進(jìn)程,那將是非常好的。在某種程度上,的確可以做到這一點(diǎn)。交互式進(jìn)程通常遵循下列模式:等待命令、執(zhí)行命令、等待命令、執(zhí)行命令。。。如果我們把每個命令的執(zhí)行都看作一個分離的作業(yè),那么我們可以通過首先運(yùn)行最短的作業(yè)來使響應(yīng)時間最短。這里唯一的問題是如何從當(dāng)前可運(yùn)行進(jìn)程中找出最短的那一個進(jìn)程。 一種方式是根據(jù)進(jìn)程過去的行為進(jìn)行推測,并執(zhí)行估計運(yùn)行時間最短的那一個。假設(shè)每個終端上每條命令的預(yù)估運(yùn)行時間為 可以看到,在三輪過后,T0 在新的估計值中所占比重下降至 1/8。 有時把這種通過當(dāng)前測量值和先前估計值進(jìn)行加權(quán)平均從而得到下一個估計值的技術(shù)稱作 保證調(diào)度一種完全不同的調(diào)度方法是對用戶做出明確的性能保證。一種實(shí)際而且容易實(shí)現(xiàn)的保證是:若用戶工作時有 n 個用戶登錄,則每個用戶將獲得 CPU 處理能力的 1/n。類似地,在一個有 n 個進(jìn)程運(yùn)行的單用戶系統(tǒng)中,若所有的進(jìn)程都等價,則每個進(jìn)程將獲得 1/n 的 CPU 時間。 彩票調(diào)度對用戶進(jìn)行承諾并在隨后兌現(xiàn)承諾是一件好事,不過很難實(shí)現(xiàn)。但是存在著一種簡單的方式,有一種既可以給出預(yù)測結(jié)果而又有一種比較簡單的實(shí)現(xiàn)方式的算法,就是 其基本思想是為進(jìn)程提供各種系統(tǒng)資源(例如 CPU 時間)的彩票。當(dāng)做出一個調(diào)度決策的時候,就隨機(jī)抽出一張彩票,擁有彩票的進(jìn)程將獲得該資源。在應(yīng)用到 CPU 調(diào)度時,系統(tǒng)可以每秒持有 50 次抽獎,每個中獎?wù)邔@得比如 20 毫秒的 CPU 時間作為獎勵。
如果希望進(jìn)程之間協(xié)作的話可以交換它們之間的票據(jù)。例如,客戶端進(jìn)程給服務(wù)器進(jìn)程發(fā)送了一條消息后阻塞,客戶端進(jìn)程可能會把自己所有的票據(jù)都交給服務(wù)器,來增加下一次服務(wù)器運(yùn)行的機(jī)會。當(dāng)服務(wù)完成后,它會把彩票還給客戶端讓其有機(jī)會再次運(yùn)行。事實(shí)上,如果沒有客戶機(jī),服務(wù)器也根本不需要彩票。
公平分享調(diào)度到目前為止,我們假設(shè)被調(diào)度的都是各個進(jìn)程自身,而不用考慮該進(jìn)程的擁有者是誰。結(jié)果是,如果用戶 1 啟動了 9 個進(jìn)程,而用戶 2 啟動了一個進(jìn)程,使用輪轉(zhuǎn)或相同優(yōu)先級調(diào)度算法,那么用戶 1 將得到 90 % 的 CPU 時間,而用戶 2 將之得到 10 % 的 CPU 時間。 為了阻止這種情況的出現(xiàn),一些系統(tǒng)在調(diào)度前會把進(jìn)程的擁有者考慮在內(nèi)。在這種模型下,每個用戶都會分配一些CPU 時間,而調(diào)度程序會選擇進(jìn)程并強(qiáng)制執(zhí)行。因此如果兩個用戶每個都會有 50% 的 CPU 時間片保證,那么無論一個用戶有多少個進(jìn)程,都將獲得相同的 CPU 份額。 實(shí)時系統(tǒng)中的調(diào)度
實(shí)時系統(tǒng)可以分為兩類, 實(shí)時系統(tǒng)中的事件可以按照響應(yīng)方式進(jìn)一步分類為 只有滿足這個條件的實(shí)時系統(tǒng)稱為 舉一個例子,考慮一個有三個周期性事件的軟實(shí)時系統(tǒng),其周期分別是 100 ms、200 m 和 500 ms。如果這些事件分別需要 50 ms、30 ms 和 100 ms 的 CPU 時間,那么該系統(tǒng)時可調(diào)度的,因?yàn)?0.5 + 0.15 + 0.2 < 1。如果此時有第四個事件加入,其周期為 1 秒,那么此時這個事件如果不超過 150 ms,那么仍然是可以調(diào)度的。忽略上下文切換的時間。 實(shí)時系統(tǒng)的調(diào)度算法可以是靜態(tài)的或動態(tài)的。前者在系統(tǒng)開始運(yùn)行之前做出調(diào)度決策;后者在運(yùn)行過程中進(jìn)行調(diào)度決策。只有在可以提前掌握所完成的工作以及必須滿足的截止時間等信息時,靜態(tài)調(diào)度才能工作,而動態(tài)調(diào)度不需要這些限制。 調(diào)度策略和機(jī)制到目前為止,我們隱含的假設(shè)系統(tǒng)中所有進(jìn)程屬于不同的分組用戶并且進(jìn)程間存在相互競爭 CPU 的情況。通常情況下確實(shí)如此,但有時也會發(fā)生一個進(jìn)程會有很多子進(jìn)程并在其控制下運(yùn)行的情況。例如,一個數(shù)據(jù)庫管理系統(tǒng)進(jìn)程會有很多子進(jìn)程。每一個子進(jìn)程可能處理不同的請求,或者每個子進(jìn)程實(shí)現(xiàn)不同的功能(如請求分析、磁盤訪問等)。主進(jìn)程完全可能掌握哪一個子進(jìn)程最重要(或最緊迫),而哪一個最不重要。但是,以上討論的調(diào)度算法中沒有一個算法從用戶進(jìn)程接收有關(guān)的調(diào)度決策信息,這就導(dǎo)致了調(diào)度程序很少能夠做出最優(yōu)的選擇。 解決問題的辦法是將 線程調(diào)度當(dāng)若干進(jìn)程都有多個線程時,就存在兩個層次的并行:進(jìn)程和線程。在這樣的系統(tǒng)中調(diào)度處理有本質(zhì)的差別,這取決于所支持的是用戶級線程還是內(nèi)核級線程(或兩者都支持)。 首先考慮用戶級線程,由于內(nèi)核并不知道有線程存在,所以內(nèi)核還是和以前一樣地操作,選取一個進(jìn)程,假設(shè)為 A,并給予 A 以時間片控制。A 中的線程調(diào)度程序決定哪個線程運(yùn)行。假設(shè)為 A1。由于多道線程并不存在時鐘中斷,所以這個線程可以按其意愿任意運(yùn)行多長時間。如果該線程用完了進(jìn)程的全部時間片,內(nèi)核就會選擇另一個進(jìn)程繼續(xù)運(yùn)行。 在進(jìn)程 A 終于又一次運(yùn)行時,線程 A1 會接著運(yùn)行。該線程會繼續(xù)耗費(fèi) A 進(jìn)程的所有時間,直到它完成工作。不過,線程運(yùn)行不會影響到其他進(jìn)程。其他進(jìn)程會得到調(diào)度程序所分配的合適份額,不會考慮進(jìn)程 A 內(nèi)部發(fā)生的事情。 現(xiàn)在考慮 A 線程每次 CPU 計算的工作比較少的情況,例如:在 50 ms 的時間片中有 5 ms 的計算工作。于是,每個線程運(yùn)行一會兒,然后把 CPU 交回給線程調(diào)度程序。這樣在內(nèi)核切換到進(jìn)程 B 之前,就會有序列 A1,A2,A3,A1,A2,A3,A1,A2,A3,A1 。如下所示 運(yùn)行時系統(tǒng)使用的調(diào)度算法可以是上面介紹算法的任意一種。從實(shí)用方面考慮,輪轉(zhuǎn)調(diào)度和優(yōu)先級調(diào)度更為常用。唯一的局限是,缺乏一個時鐘中斷運(yùn)行過長的線程。但由于線程之間的合作關(guān)系,這通常也不是問題。 現(xiàn)在考慮使用內(nèi)核線程的情況,內(nèi)核選擇一個特定的線程運(yùn)行。它不用考慮線程屬于哪個進(jìn)程,不過如果有必要的話,也可以這么做。對被選擇的線程賦予一個時間片,而且如果超過了時間片,就會強(qiáng)制掛起該線程。一個線程在 50 ms 的時間片內(nèi),5 ms 之后被阻塞,在 30 ms 的時間片中,線程的順序會是 A1,B1,A2,B2,A3,B3。如下圖所示 用戶級線程和內(nèi)核級線程之間的主要差別在于 從進(jìn)程 A 的一個線程切換到進(jìn)程 B 的一個線程,其消耗要遠(yuǎn)高于運(yùn)行進(jìn)程 A 的兩個線程(涉及修改內(nèi)存映像,修改高速緩存),內(nèi)核對這種切換的消耗是了解到,可以通過這些信息作出決定。 文章參考: 《現(xiàn)代操作系統(tǒng)》 《Modern Operating System》forth edition https://www./computing/news-wires-white-papers-and-books/interactive-systems https://j00ru./syscalls/nt/32/ https://www./process_hierarchy.xhtml https://en./wiki/Runtime_system https://en./wiki/Execution_model https://zhidao.baidu.com/question/113227654.html https://baike.baidu.com/item/等待隊(duì)列/9223804?fr=aladdin http://www./cu/computinghistory/7094.html https://baike.baidu.com/item/中斷向量/4947039?fr=aladdin |
|