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