操作系统:第二章 进程管理3 - 进程同步与互斥
本文已收錄至 Github(MD-Notes),若博客中有圖片打不開,可以來我的 Github 倉庫:https://github.com/HanquanHq/MD-Notes,涵蓋了互聯網大廠面試必問的知識點,講解透徹,長期更新中,歡迎一起學習探討。
面試必會系列專欄:https://blog.csdn.net/sinat_42483341/category_10300357.html
操作系統系列專欄:https://blog.csdn.net/sinat_42483341/category_10519484.html
第二章 進程管理3 - 進程同步與互斥
目錄
- 第二章 進程管理3 - 進程同步與互斥
- 什么是進程同步
- 進程互斥的原則
- 進程互斥的軟件實現方法
- 1、單標志法
- 2、雙標志先檢查法
- 3、雙標志后檢查法
- 4、Peterson 算法
- 進程互斥的硬件實現方法
- 1、中斷屏蔽方法
- 2、TestAndSetLock 指令
- TSL和中斷屏蔽的區別
- 利用TSL完成進程間互斥 - 《現代操作系統》P71
- 3、XCHG 指令
- 信號量機制
- 1、整型信號量
- 2、記錄型信號量(默認)
- 記錄型信號量定義
- P 操作(wait 操作)
- V 操作(signal 操作)
- 信號量機制實現進程互斥
- 信號量機制實現進程同步 - 前 V 后 P
- 信號量機制實現前驅關系 - 前 V 后 P
- 經典的 IPC 問題
- 多生產者 - 多消費者問題
- 分析同步關系(一前一后):
- 代碼
- 吸煙者問題
- 可以生產多個產品的單生產者問題
- 分析關系
- 三種組合
- 同步關系(從事件角度分析)
- 代碼
- 讀者寫者問題
- 代碼
- 哲學家就餐問題
- 關系分析
- 如何防止死鎖的發生呢?
- 代碼
- 管程
- 管程的特征
- 死鎖
- 易混概念辨析
- 死鎖產生的必要條件
- 1、互斥條件
- 2、不剝奪條件
- 3、請求和保持條件
- 4、循環等待條件
- 什么時候會發生死鎖
- 1、對系統資源的競爭
- 2、進程推進順序非法
- 2、信號量的使用不當
- 死鎖的處理策略
- 1、預防死鎖
- (1)破壞互斥條件
- 方案
- 缺點:
- (2)破壞不剝奪條件
- 不剝奪條件
- 方案
- 缺點
- (3)破壞請求和保持條件
- 請求和保持條件
- 方案
- 缺點
- (4)破壞循環等待條件
- 循環等待條件
- 方案
- 原理
- 缺點:
- 2、避免死鎖
- 銀行家算法
- 銀行家算法的步驟:
- 安全性算法的步驟:
- 安全序列
- 3、死鎖的檢測和解除
- 死鎖檢測思想:
- 死鎖檢測算法:
- 解除死鎖方法:
什么是進程同步
知識點回顧:進程具有異步性的特征。異步性是指,各并發執行的進程以各自獨立的、不可預知的速度向前推進。
由于并發必然導致異步性。而實際應用中,又必須按照某種順序執行,如何解決這種異步問題,就是“進程同步”所討論的內容。
同步 亦稱直接 制約關系,它是指為完成某種任務而建立的兩個或多個進程,這些進程因為需要在某些位置上 協調它們的工作次序 而產生的制約關系。
對臨界資源的互斥訪問,可以在邏輯上分為如下四個部分:
進程互斥的原則
為了實現對臨界資源的互斥訪問,同時保證系統整體性能,需要遵循以下原則:
進程互斥的軟件實現方法
1、單標志法
算法思想:兩個進程在訪問完臨界區后會把使用臨界區的權限轉交給另一個進程。也就是說,每個進程進入臨界區的權限只能被另一個進程賦予。
turn 的初值為 0,即剛開始只允許 0 號進程進入臨界區。
該算法可以實現同一時刻最多只允許一個進程訪問臨界區,但是兩個進程必須輪流訪問。如果 P0 一直不訪問臨界區,雖然臨界區空閑,但并不允許 P1 訪問。違背“空閑讓進”原則。
2、雙標志先檢查法
算法思想:設置一個布爾型數組flag[],數組中各個元素用來標記各進程想進入臨界區的意愿,比如“flag[0] = ture”意味著0 號進程P0 現在想要進入臨界區。每個進程在進入臨界區之前先檢查當前有沒有別的進程想進入臨界區,如果沒有,則把自身對應的標志flag[i] 設為true,之后開始訪問臨界區。
由于進入區的“檢查”和“上鎖” 兩個處理不是一氣呵成的,“檢查”后、“上鎖”前 可能發生進程切換。
主要問題是:**違反“忙則等待”**原則,并發時可能導致兩個進程同時訪問臨界區。
3、雙標志后檢查法
先“上鎖”后“檢查”的方法,來避免上述問題。
若按照①⑤②⑥的順序執行,P0 和 P1 將無法進入臨界區。
此方法雖然 解決了“忙則等待” 的問題,但是又 違背了“空閑讓進”、“有限等待”原則。
4、Peterson 算法
算法思想:結合雙標志法、單標志法的思想。如果雙方都爭著想進入臨界區,那可以讓進程嘗試謙讓
誰最后設置了 turn 的值,誰就失去了行動的優先權。
Peterson 算法用軟件方法解決了進程互斥問題,遵循了空閑讓進、忙則等待、有限等待三個原則,但是依然未遵循讓權等待的原則(進程無法獲得使用權的時候,一直while循環檢測,消耗CPU資源)。
Peterson 算法相較于之前三種軟件解決方案來說是最好的,但依然不夠好。
進程互斥的硬件實現方法
1、中斷屏蔽方法
與原語的實現思想相同,即在某進程開始訪問臨界區到結束訪問為止,都不允許被中斷。
開中斷; 臨界區; 關中斷;優點:
- 簡單,高效
缺點:
- 不適用于多處理機
- 由于開/關中斷指令是特權指令,只能運行在內核態,因此只適用于內核級進程,不適用于用戶級進程
2、TestAndSetLock 指令
TSL 是 Test and Set Lock 的縮寫。要實現 TSL 需要硬件的支持。
硬件指令:
TSL RX, LOCK # 測試并加鎖該指令所做的事情:
- 讀取 Lock 的值,存入寄存器RX中
- 給 Lock 設置一個非0值(設置到LOCK對應的內存中)
以上三個步驟是一個 不可拆分 的原子操作,執行該指令的CPU將會 鎖住內存總線(memory bus),以禁止其他CPU在本指令結束之前訪問內存。
TSL和中斷屏蔽的區別
當一個CPU將中斷屏蔽后,只影響當前屏蔽中斷的CPU,其他CPU還是依然可以照樣訪問內存的(想要中斷)。唯一一個當一個CPU在訪問內存時阻止其他CPU訪問內存的方法就是將內存總線鎖住,這個需要硬件的支持,TSL可以達到該目的。
利用TSL完成進程間互斥 - 《現代操作系統》P71
enter_region:TSL REGISTER, LOCK /*復制鎖到寄存器并將鎖置1*/CMP REGISTER, #0 /*判斷寄存器內容是否為0*/JNE enter_region /*若不是0,說明鎖已經被設置,跳轉到enter_region循環*/RET /*返回調用者,進入臨界區*/leave_region:MOVE LOCK, #0 /*在鎖中置0*/RET /*返回調用者*/(下圖圖源王道)
3、XCHG 指令
一個可替換 TSL 的指令是 XCHG,它原子性地交換了兩個位置的內容。它本質上與 TSL 的解決方法一樣。
enter_region:MOVE REGISTER, #1 /*給寄存器中置1*/XCHG REGISTER, LOCK /*交換寄存器與鎖變量的內容*/CMP REGISTER, #0 /*判斷寄存器內容是否為0?*/JNE enter_region /*若不是0跳轉到enter_region*/RET /*返回調用者,進入臨界區*/ leave_region:MOVE LOCK, #0 /*在鎖中置0*/RET /*返回調用者*/優點:
- 使用硬件方式實現簡單;適用于多處理機環境
缺點:
- 不滿足“讓權等待”原則,暫時無法進入臨界區的進程會占用 CPU 資源并循環執行 TSL 指令,導致忙等
信號量機制
以上所有方案都無法實現讓權等待,而信號量機制實現了讓權等待。
用戶進程通過使用操作系統提供的一對原語來對信號量進行操作,實現了進程互斥、進程同步。
-
P 操作:申請 / wait(S) / P(S)
-
V 操作:釋放 / signal(S) / V(S)
1、整型信號量
用一個 整數型的變量 作為信號量,表示系統中某種資源的數量。對信號量的三種操作:
- 初始化
- P 操作(將“檢查”和“上鎖”一氣呵成,避免并發 / 異步導致的問題)
- V 操作
存在的問題:不滿足“讓權等待”原則,會發生“忙等”,一直 while 占用處理機
2、記錄型信號量(默認)
記錄型信號量定義
S.value 表示系統中某種資源的數目。
P 操作(wait 操作)
對信號量 S 執行一次 P 操作,即執行 S.value–,表示資源數減 1
若 S.value < 0 時,該資源已分配完畢,進程調用 block 原語自我阻塞(運行態 -> 阻塞態),主動放棄處理機,并插入該類資源的等待隊列 S.L 中。遵循了“讓權等待”原則。
V 操作(signal 操作)
對信號量 S 執行一次 V 操作,即執行 S.value++,表示資源數加 1
若 S.value <= 0,仍有進程在等待資源,則調用 wakeup 原語喚醒等待隊列中第一個進程(阻塞態 -> 就緒態)
信號量機制實現進程互斥
信號量機制實現進程同步 - 前 V 后 P
P2 需要這種資源,而只有 P1 才能產生這種資源。即,只有執行了 V 操作之后,P 操作之后的代碼才會執行。
信號量機制實現前驅關系 - 前 V 后 P
進程P1 中有句代碼S1,P2 中有句代碼S2 ,P3中有句代碼S3 …… P6 中有句代碼S6。這些代碼要求按如下前驅圖所示的順序來執行:
其實每一對前驅關系都是一個進程同步問題(需要保證一前一后的操作)因此,
經典的 IPC 問題
生產者、消費者共享一個初始為空、大小為n的緩沖區。緩沖區是臨界資源,各進程必須互斥地訪問。
-
若緩沖區 不滿,生產者可以 生產 -> V(full) 釋放
-
若緩沖區 非空,消費者可以 消費 -> P(full) 申請
仍然滿足 前 V 后 P:consumer 需要這種資源,而只有 producer 才能產生這種資源。即,只有執行了 V(full) 操作之后,P(full) 操作之后的代碼才會執行。
semaphore mutex = 1; // 互斥信號量,實現對緩沖區的互斥訪問 semaphore empty = n; // 同步信號量,表示空閑緩沖區的數量 semaphore full = 0; // 同步信號量,表示產品的數量,也即非空緩沖區的數量producer (){ // 生產者while(1){生產一個產品;P(empty); // 實現互斥的 P 操作必須在實現同步的 P 操作之后,否則會導致死鎖P(mutex);把產品放入緩沖區;V(mutex); // 可以改變相鄰 V 操作的順序V(full);} }consumer (){ // 消費者while(1){P(full);P(mutex);從緩沖區取出一個產品;V(mutex);V(empty);使用產品;} }多生產者 - 多消費者問題
桌上有一盤子,每次只能放入一個水果。爸爸只放蘋果,媽媽只放橘子,兒子只吃橘子,女兒只吃蘋果。
盤子空才能放,盤子有正確的水果才能取。用 PV 操作實現上述過程。
生產者生產的產品、消費者消費的產品類別各不相同。
分析同步關系(一前一后):
總結:在生產者-消費者問題中,如果緩沖區大小為1,那么有可能不需要設置互斥信號量就可以實現互斥訪問緩沖區的功能。這不是絕對的,要具體問題具體分析。
建議:在考試中如果來不及仔細分析,可以加上互斥信號量,保證各進程一定會互斥地訪問緩沖區。但需要注意的是,實現互斥的 P 操作一定要在實現同步的 P 操作之后,否則可能引起“死鎖”。
代碼
// semaphore mutex = 1; // 實現互斥訪問盤子(緩沖區) semaphore apple = 0; // 盤子中有幾個蘋果 semaphore orange = 0; // 盤子中有幾個橘子 semaphore plate = 1; // 盤子中還可以放多少個水果 dad (){while(1){準備一個蘋果;P(plate); // P 申請盤子里的一個空位把蘋果放入盤子;V(apple); // V 釋放一個蘋果} } mom (){while(1){準備一個橘子;P(plate);把橘子放入盤子;V(orange);} } daughter (){while(1){P(apple); // P 申請一個蘋果從盤中取出蘋果;V(plate); // V 釋放盤子一個空位吃掉蘋果;} } son(){while(1){P(orange);從盤中取出橘子;V(plate);吃掉橘子;} }吸煙者問題
可以生產多個產品的單生產者問題
系統中有 三個抽煙者進程,每個抽煙者不斷地卷煙并抽煙。抽煙者卷起并抽掉一顆煙需要有三種材料:煙草、紙、膠水。一個抽煙者有煙草,一個有紙,另一個有膠水。
系統中還有 一個供應者進程,它們無限地供應所有三種材料,但每次僅輪流提供三種材料中的兩種。
得到缺失的兩種材料的抽煙者在卷起并抽掉一顆煙后,會發信號通知供應者,讓它繼續提供另外的兩種材料。這一過程重復進行。
請用以上介紹的 IPC 同步機制編程,實現該問題要求的功能。
分析關系
桌子:容量為1的緩沖區,要互斥訪問
三種組合
同步關系(從事件角度分析)
發出完成信號 -> 供應者將下一個組合放到桌上
代碼
不需要設置一個專門的同步信號量。因為緩沖區大小為 1,故同一時刻,四個同步信號量中至多有一個為 1。
semaphore offer1 = 0; // 桌上組合一的數量 semaphore offer2 = 0; // 桌上組合二的數量 semaphore offer3 = 0; // 桌上組合三的數量 semaphore finish = 0; // 抽煙是否完成 int i = 0; // 用于實現“三個抽煙者輪流流抽煙”provider (){while(1){if(i==0) {將組合一放桌上;V(offer1);} else if(i==1){將組合二放桌上;V(offer2);} else if(i==2){將組合三放桌上;V(offer3);}i = (i+1)%3;P(finish);} }smoker1 (){while(1){P(offer1);從桌上拿走組合一;卷煙;抽掉;V(finish);} }smoker2 (){while(1){P(offer2);從桌上拿走組合二;卷煙;抽掉;V(finish);} }smoker3 (){while(1){P(offer3);從桌上拿走組合三;卷煙;抽掉;V(finish);} }讀者寫者問題
有讀者、寫者兩組并發進程,共享一個文件。規則:
- 允許 多個讀者 同時 讀 文件
- 只允許 一個寫者 寫文件
- 寫完成 之前 不允許讀
- 讀完成 之前 不允許寫
互斥關系
-
互斥:寫 - 寫 / 寫 - 讀
-
不互斥:讀 - 讀
代碼
讀寫公平法:即使連續有 多個讀者 到來,也 不會使寫者一直饑餓,是相對公平的先來連服務原則。
semaphore rw = 1; // 讀寫信號量,實現只讀或只寫(互斥訪問) int count = 0; // 記錄當前有幾個讀者程在訪問文件 semaphore mutex = 1; // 用于保證對count變量的互斥訪問 semaphore w = 1; // 用于保證寫者不會饑餓writer (){ // 寫者while(1){P(w); // 用于保證寫者不會饑餓P(rw); // 寫者申請一個讀寫信號量(意味著只寫)寫文件;V(rw);V(w);} }reader (){ // 讀者while(1){P(w); // 用于保證寫者不會饑餓P(mutex); // 對count操作前,需要申請count變量的獨占if(count == 0){ // 沒有讀者P(rw); // 讀者申請一個讀寫信號量(意味著只讀)}count++; // 全局變量增加一個寫進程V(mutex); // 釋放count變量的獨占V(w);讀文件;P(mutex);count--;if(count == 0){V(rw);}V(mutex);} }哲學家就餐問題
圓桌坐著 5 名哲學家,每兩個哲學家之間有一根筷子,哲學家交替進行思考和進餐。進餐時,試圖一根一根拿起左、右兩根筷子,只有同時拿起兩根筷子才能進餐,否則需要等待。進餐完畢后,哲學家放下筷子繼續思考。
關系分析
- 5 個哲學家進程
- 相鄰哲學家對中間筷子的訪問是互斥關系
每個哲學家進程需要同時持有兩個臨界資源才能開始吃飯。如何避免死鎖現象,才是哲學家問題的精髓。
定義互斥信號量數組 chopstick[5] = {1, 1, 1, 1, 1} 用于實現對 5 個筷子的互斥訪問。并對哲學家按 0 ~ 4 編號,哲學家 i 左邊的筷子編號為 i,右邊的筷子編號為 (i + 1) % 5
如何防止死鎖的發生呢?
有很多種方式,舉幾個例子:
代碼
思想:使用 mutex 來實現 只允許一個哲學家 處于 只拿起了一只筷子的狀態
semaphore chopstick[5]={1,1,1,1,1}; semaphore mutex = 1; // 互斥地取筷子 Pi (){ // i 號哲學家的進程while(1){P(mutex);P(chopstick[i]); // 拿左P(chopstick[(i+1)%5]); // 拿右V(mutex);吃飯;V(chopstick[i]); // 放左V(chopstick[(i+1)%5]); // 放右思考;} }管程
《現代操作系統》P79
由于使用信號量時要非常小心,而且出現的錯誤都是競爭條件、死鎖以及其它一些不可預測或不可再現的行為,而為了更易于編寫正確的程序,提出了一種高級同步原語,稱為管程(monitor)
一個 管程 是由一個過程、變量及數據結構等組成的一個集合,它們組成一個 特殊的模塊 或 軟件包。
管程有一個很重要的特性:任一時刻,管程中只能有一個活躍進程。
管程是編程語言的組成部分,編譯器知道它們的特殊性,因此可以采用與其它過程調用不同的方法來處理對管程的調用。由編譯器而非程序員來安排互斥,出錯的可能性要小很多。
-
Java 的 syncronized 就是一個管程,也可使用 wait() / notify() 實現進程的同步。
-
C,Pascal 以及多數其他語言都沒有管程。
管程的特征
死鎖
易混概念辨析
死鎖:各進程互相等待對方手里的資源,導致各進程都阻塞,無法向前推進(操作系統考慮)
饑餓:由于長期得不到想要的資源,某進程無法向前推進(操作系統考慮)
死循環:某進程執行過程中一直跳不出某個循環,是可以上處理機運行的(開發人員考慮)
死鎖產生的必要條件
1、互斥條件
只有對必須互斥使用的資源的爭搶才會導致死鎖(如哲學家的筷子、打印機設備)。像內存、揚聲器這樣可以同時讓多個進程使用的資源是不會導致死鎖的(因為進程不用阻塞等待這種資源)。
2、不剝奪條件
進程所獲得的資源在未使用完之前,不能由其他進程強行奪走,只能主動釋放。
3、請求和保持條件
進程已經保持了至少一個資源,但又提出了新的資源請求,而該資源又被其他進程占有,此時請求進程被阻塞,但又對自己已有的資源保持不放。
4、循環等待條件
存在一種進程資源的循環等待鏈,鏈中的每一個進程已獲得的資源同時被下一個進程所請求。
什么時候會發生死鎖
1、對系統資源的競爭
各進程對不可剝奪的資源(如打印機)的競爭可能引起死鎖,對可剝奪的資源(CPU)的競爭是不會引起死鎖的。
2、進程推進順序非法
請求和釋放資源的順序不當,也同樣會導致死鎖。例如,并發執行的進程P1、P2 分別申請并占有R1、R2,之后進程P1申請R2,進程P2申請R1,兩者因為申請的資源被對方占有而阻塞,發生死鎖。
2、信號量的使用不當
如生產者-消費者問題中,如果實現互斥的P操作在實現同步的P操作之前,就有可能導致死鎖。(可以把互斥信號量、同步信號量也看做是一種抽象的系統資源)
死鎖的處理策略
1、預防死鎖
破壞死鎖產生的四個必要條件中的一個或幾個。
(1)破壞互斥條件
方案
把只能互斥使用的資源改造為允許共享使用,則系統不會進入死鎖狀態。比如,操作系統可以采用 SPOOLing 技術把獨占設備在邏輯上改造成共享設備。
缺點:
并不是所有的資源都可以改造成可共享使用的資源。很多時候無法破壞互斥條件。
(2)破壞不剝奪條件
不剝奪條件
進程所獲得的資源在未使用完之前,不能由其他進程強行奪走,只能主動釋放。
方案
- 方案一:當某個進程請求新的資源得不到滿足時,它必須立即釋放保持的所有資源,待以后需要時再重新申請。也就是說,即使某些資源尚未使用完,也需要主動釋放,從而破壞了不可剝奪條件。
- 方案二:當某個進程需要的資源被其他進程所占有的時候,可以由操作系統協助,將想要的資源強行剝奪。這種方式一般需要考慮各進程的優先級(比如:剝奪調度方式,就是將處理機資源強行剝奪給優先級更高的進程使用)
缺點
(3)破壞請求和保持條件
請求和保持條件
進程已經保持了至少一個資源,但又提出了新的資源請求,而該資源又被其他進程占有,此時請求進程被阻塞,但又對自己已有的資源保持不放。
方案
采用 靜態分配 方法,進程在運行前,一次申請完它需要的全部資源才能投入運行。一旦投入運行,一直保持資源,且不再請求別的任何資源,直到運行結束,釋放資源。
缺點
(4)破壞循環等待條件
循環等待條件
存在一種進程資源的循環等待鏈,鏈中的每一個進程已獲得的資源同時被下一個進程所請求。
方案
順序資源分配法。首先給系統中的資源編號,規定每個進程必須按編號遞增的順序請求資源,同類資源(即編號相同的資源)一次申請完。
原理
一個進程只有已占有小編號的資源時,才有資格申請更大編號的資源。
- 任一時刻,總有一進程擁有的資源編號是最大的,此進程對其余資源的獲取必暢通無阻
- 已持有大編號資源的進程不可能逆向地回來申請小編號的資源,從而就不會產生循環等待的現象
缺點:
2、避免死鎖
用某種方法防止系統進入不安全狀態,從而避免死鎖(銀行家算法)
銀行家算法
一個小城鎮的銀行家向一群客戶分別承諾了一定的貸款額度,只有當借款總數達到客戶最大要求時,客戶才歸還貸款,否則無論之前借了多少錢,都拿不回來。
銀行家算法的步驟:
安全性算法的步驟:
安全序列
所謂的安全序列,就是指系統如果按照這種序列分配資源,則每個進程都能順利完成。只要能找出一個安全序列,系統就處于安全狀態。當然,安全序列可以有多個。
一個 找不到 安全序列的例子:(剩余資源總數 3 3 2)可以拓展到有 n 個進程,m 種資源
| P0 | 8 5 3 | 0 1 0 | 8 4 3 |
| P1 | 3 2 2 | 2 0 0 | 1 2 2 |
| P2 | 9 5 2 | 3 0 2 | 6 5 0 |
| P3 | 2 2 2 | 2 1 1 | 0 1 1 |
| P4 | 4 3 6 | 0 0 2 | 4 3 4 |
3、死鎖的檢測和解除
允許死鎖的發生,不過操作系統會負責檢測出死鎖的發生,然后采取某種措施解除死鎖。
- 死鎖檢測算法:用于檢測系統狀態,以確定系統中是否發生了死鎖。
- 死鎖解除算法:當認定系統中已經發生了死鎖,利用該算法可將系統從死鎖狀態中解脫出來。
死鎖檢測思想:
依次消除與不阻塞進程相連的邊,直到無邊可消。
-
如果最終 能消除所有邊,就稱這個圖是可完全簡化的。此時一定沒有發生死鎖(即找到一個 安全序列)
-
如果最終 不能消除所有邊,那么此時就是 發生了死鎖。剩余連著邊的進程,就是處于死鎖狀態的進程。
死鎖檢測算法:
解除死鎖方法:
總結
以上是生活随笔為你收集整理的操作系统:第二章 进程管理3 - 进程同步与互斥的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 操作系统:第二章 进程管理2 - 处理机
- 下一篇: 操作系统:第三章 内存管理2 - 详解虚