課程引入:進程同步與信號量
接下來這節課開始,我們再開始講多進程圖像。講多進程圖像的下一個點,前面我們講清楚了多進程圖像要想實現切換,調度是如何做的。同時,多個進程放在內存中,就會存在多進程合作的情況,而這種合作應該是合理有序的,這部分內容就是進程同步——讓多進程之間的合作變得合理有序。那么怎么來實現這種合理有序呢?就要靠信號量。這堂課我們要講清楚為什么會有信號量,以及如何依靠信號量來實現多個進程推進的合理有序,即同步。
現實案例:進程合作與同步的重要性
首先從一個例子看起,在現實社會中,司機和售貨員就如同兩個進程,司機的動作是啟動車、運行、到站停車,售貨員的動作是關門、售票、開門 ,他們各自有一套執行流程。這兩個進程在同一臺車上,為完成車輛的合理有序行駛,必須進行合作。
- 不合作的后果:如果二者執行順序沒有約束,比如售票員開門售票時,司機啟動車輛,就會造成嚴重后果。所以,司機啟動車輛不能隨意進行,需要等待一個信號,比如售票員賣完票告知可以走了;同樣,售票員開門也不是隨便進行的,要等車輛到站停車的信號。
- 合作與同步的體現:司機等待售票員關門的信號再啟動車輛,售票員在車輛到站后得到停車信號才開門,這就體現了進程間的合作。一個進程等待信號,另一個進程在合適的時候發送信號,從而使多個進程按照一定順序向前推進,這就是同步。每個進程有自己的執行程序,但不是每條程序都能隨便執行,有時需要停下來等待信號,當收到信號后再繼續執行,這就是多進程合理有序的合作與同步。
技術案例:生產者 - 消費者模型中的同步問題
接下來以生產者 - 消費者模型為例進一步說明。有一個共享緩沖區,生產者不斷向里放內容,每放完一個 counter
加一;消費者不斷從里取內容,每取出一個 counter
減一 ,這是典型的多進程合作場景。
-
同步需求:在這個模型中,也需要合理有序的推進。當生產者發現
counter
等于緩沖區大小buff size
,即緩沖區滿了,就不能繼續放入,必須等待;同理,當緩沖區為空時,消費者也應該停止。
所以,進程同步的關鍵在于分析進程在哪些地方該停、什么時候該走。在生產者 - 消費者模型中,緩沖區滿時生產者停,消費者消費后產生空閑緩沖區,就給生產者發信號讓其繼續;緩沖區空時消費者停,生產者生產后給消費者發信號。 -
信號的局限性:僅依靠信號存在問題。例如,當緩沖區滿時,生產者
p1
嘗試放入會因counter
等于buff size
而sleep
,之后另一個生產者p2
進來,同樣會sleep
。此時若消費者執行一次循環,取出一個內容,counter
變為buff size - 1
,消費者判斷緩沖區未滿,認為無人等待緩沖區,不會再發信號喚醒p2
,導致p2
永遠無法喚醒。這說明單純依靠counter
進行語義判斷不足,不僅需要知道緩沖區中空閑個數,還需知道有多少進程在睡眠等待。
信號量的引入與原理
為解決上述問題,引入信號量。信號量是一個整數,它能記錄更多信息。例如信號量等于 -2
,表示有兩個進程在等待 。當消費者執行時,發現信號量為 -2
,就會喚醒阻塞隊列頭部的進程(如 p1
),同時信號量變為 -1
;再次執行,喚醒 p2
,信號量變為 0
。
- 信號量的語義:信號量為負數時,表示有進程阻塞,其絕對值代表等待進程的數量;為
0
時,表示沒有進程等待,但也沒有可用資源;為正數時,表示有可用資源,數值代表資源數量 。如消費者執行使信號量變為1
,表示還有一個空閑緩沖區,此時若有新的生產者p3
來,無需睡眠可直接執行,執行后信號量變為0
;再有生產者來,信號量變為-1
,該生產者需阻塞等待。 - 基于信號量的進程決策:進程根據信號量的值決定是否等待或喚醒其他進程。生產者申請使用資源(如空閑緩沖區)時,若信號量為負或零,說明資源不足或已用完,需等待;消費者釋放資源(產生空閑緩沖區)時,若信號量為負,說明有進程在等待,需喚醒一個等待進程。
信號量與資源等待的關系
我們可以根據這個習題來回答,對于一種數量為八的資源,思考進程等待的原因。進程等待肯定是因為申請資源時沒有可用資源了。在進程同步中,競爭合作體現為走走停停,而“停滯”是其中的核心,所以明確進程何時等待至關重要。當一個進程訪問資源卻發現沒有資源時,就會進入等待狀態。
這種資源對應的信號量初值應該設為八,這表示初始狀態下可以使用八個該資源。當信號量的值變為零時,意味著沒有資源剩余,若值再變為負數,進程就需要等待。當信號量的值為二時,說明還有兩個資源可供使用,此時沒有進程在等待該資源;而當信號量的值為 -2 時,則表示有兩個進程正在等待該資源 。
通過信號量的值,我們能夠判斷系統中有多少進程在等待資源,以及還有多少資源可供使用。基于這樣的判斷,我們可以控制進程的執行與暫停,從而實現進程同步,其核心就在于依據信號量的值來判斷進程何時該“走”、何時該“停”。
信號量的核心概念與操作
-
信號量的定義與作用
進程之間的同步是多個進程走走停停的合理有序推進,判斷何時停要看信號量的值。當信號量為負值或 0 時,進程申請信號量會變成負值,此時進程等待;其他進程根據信號量的值,若為負,在釋放信號量時進行喚醒操作;若為正,直接累加,無需發信號。 -
信號量的實現方式
在編程實現中,判斷進程是否需要等待資源是通過調用函數來完成的,這就涉及到信號量的具體實現。信號量的核心是一個整數,它記錄著資源的相關信息。為了方便用戶操作,我們通過定義P
、V
操作這兩個接口函數來實現對信號量的控制。P
操作:當進程想要申請資源,判斷自己是否應該暫停時,就調用P
操作。以P(sem)
為例,執行該操作時,首先將信號量sem
的值減 1。這是因為進程申請資源,相當于資源數量減少。如果減 1 后信號量的值小于 0,說明在本次申請之前,資源要么已經沒有剩余(值為 0),要么已經處于供不應求的狀態(值為負),當前進程無法獲得資源,此時進程就會進入睡眠狀態,并被放入與該信號量相關聯的阻塞隊列中。例如,生產者進程每次使用空閑緩沖區時,就需要對空閑緩沖區對應的信號量執行P
操作,以此判斷是否有空閑緩沖區可供使用,如果沒有則進入等待。V
操作:有進程等待,就需要有喚醒操作,這就是V
操作的作用。當進程釋放資源時,會調用V
操作。執行V
操作時,將信號量的值加 1 ,這表示資源數量增加。如果加 1 后信號量的值仍然小于等于 0,說明在釋放資源之前,有進程在等待該資源(信號量為負表示等待進程數,為 0 表示剛有進程等到資源),此時就需要調用wake up
函數,從阻塞隊列中喚醒一個進程;如果加 1 后信號量的值大于 0 ,則表示沒有進程在睡眠等待,無需進行喚醒操作。比如消費者進程產生空閑緩沖區后,就會對相應的信號量執行V
操作。- 系統調用:由于信號量操作涉及到進程睡眠等在內核態完成的操作,所以
P
和V
操作需要做成系統調用,這樣上層應用程序就能通過調用系統調用來使用信號量。其中,P
操作源于荷蘭語“test”,表示測試是否需要阻塞;V
操作源于荷蘭語“increment”,表示增加資源數量,進而實現喚醒等待進程的功能 。
信號量解決生產者 - 消費者問題
利用信號量及其 P
、V
操作,可以有效解決生產者 - 消費者問題,實現進程間的同步與合作。在解決該問題時,關鍵在于分析生產者和消費者何時會暫停,并據此定義相應的信號量。
- 分析生產者與消費者的等待條件
- 生產者:當緩沖區滿時會停,所以定義一個信號量
empty
表示空閑緩沖區個數,初值為buff_size
。生產者每次操作前先執行P(empty)
,測試empty
是否為 0 ,即緩沖區是否滿,若滿則等待。當消費者釋放空閑緩沖區時,執行V(empty)
增加empty
的值 。 - 消費者:當緩沖區沒有內容時會停,定義一個信號量
full
表示已生產內容的個數,初值為 0 。消費者每次操作前先執行P(full)
,測試full
是否為 0 ,即是否有內容,若無則等待。當生產者生產內容后,執行V(full)
增加full
的值 。
- 生產者:當緩沖區滿時會停,所以定義一個信號量
- 互斥信號量實現共享資源互斥訪問
共享緩沖區(可視為文件)的操作需要互斥,即同一時刻只能有一個進程訪問。定義一個互斥信號量mutex
,初值為 1 。生產者和消費者在訪問共享緩沖區前,先執行P(mutex)
,若mutex
等于 1 ,則變為 0 ,進程進入;訪問結束后執行V(mutex)
釋放資源,使其他進程可以進入 。
通過上述信號量的設置以及 P
、V
操作的合理運用,依據信號量數值所代表的語義,準確判斷進程是否需要睡眠或喚醒其他進程,從而實現了生產者和消費者之間執行過程的合理有序,最終解決了進程同步問題,實現了二者的合作 。