操做系統知識——進程通訊

  有關進程通訊的知識主要分爲五個部分:算法

  ①什麼是進程通訊;數據庫

  ②實現進程通訊的誤區;編程

  ③如何正確實現進程通訊;併發

  ④經典的進程通訊問題與信號量機制;spa

  ⑤避免編程失誤的「管程」。操作系統

  本文將按照這五個部分的提出順序進行講解,力求通俗易懂、融會貫通。線程

 

  ①什麼是進程通訊?debug

  須要首先明確的是,進程通訊並非指進程間「傳遞數據」。指針

  爲了說明進程通訊,須要先介紹一下進程通訊的背景。現代操做系統中的進程間可能存在着共享的內存區,好比字處理進程A(能夠想象爲Word)、字處理進程B(能夠想象爲記事本)和打印機進程C共享一小塊內存:待打印文件地址隊列。該隊列中有一個指針out指向隊列中下一個被打印的文件地址,還有一個指針in指向隊列尾的後一位置,即新的待打印文件地址應存入的位置。顯然,指針out是供進程C訪問的,每當打印機空閒且out!=in,進程C就打印out所指的文件。而指針in則是供進程A與進程B訪問的,每當它們有但願打印的文件時就執行以下三步:「讀取in」、「向in所指位置寫入待打印文件地址」、「修改in使其指向下一位置」。code

  可是A和B都能讀寫指針in就會帶來衝突問題:假設如今A佔用着CPU並準備打印文件,A讀取了in並將待打印文件名寫入了in所指位置,可是A還沒來得及修改in,CPU就切換到了進程B執行,B在執行過程當中也準備打印文件,而且完成了對in的全部操做。一段時間後,CPU又切換到了進程A,但此時的進程A並不知道本身寫入到隊列的文件名已經被B給覆蓋了,A只會繼續執行「修改in使其指向下一位置」的操做,從而出現了進程A與進程B的「衝突」。

  這種存在共享內存區的進程間的衝突問題,解決方法的思路是統一的:當某個進程正在操做共享內存區時,其餘進程不得操做共享內存區。這個思路實現的關鍵點就是:令其餘進程知道「有一個進程在操做共享內存區」,所以這類問題就被稱爲進程通訊問題,通訊的「內容」就是:有沒有其餘進程在操做共享內存區。(講解到信號量機制時進程通訊將廣義化,但依然不是進程間的「實際通訊」,而是某些信號的共享)

  由於「操做共享內存區」太長,因此人們通常稱正在操做共享內存區的進程是在臨界區內,同時將進程中須要操做共享內存區的部分代碼稱之爲臨界區代碼。思路也就能夠稱做:當有進程在臨界區時,其餘進程不得進入臨界區。

 

  ②實現進程通訊的誤區

  由於實現進程通訊的關鍵,就是令其餘進程知道如今已經有進程在臨界區了,因此一個很簡單的解決思路就出來了:

  將臨界區想象成一個房子,同一時間內房子內只能有一個進程,那麼確保這一點的方法就是給房子加鎖(mutex),若是鎖是鎖上的,則準備進入的進程不得進入,若是鎖是打開的,則準備進入的進程能夠進入,而且進入後要將鎖鎖上,此外退出時也要負責將鎖打開。

  將上述想法轉換爲代碼表示,就是令每一個進程在臨界區代碼的先後,分別添加以下代碼,其中mutex爲共享內存區中的一個變量:

 1 int mutex=1; //mutex爲1表示鎖打開,爲0表示鎖關閉
 2 while(true)
 3 {
 4     //執行非臨界區代碼
 5     
 6     //準備執行臨界區代碼,即準備進入臨界區
 7 
 8     while(mutex==0);//若是mutex爲0,說明有其餘進程在臨界區內,當前進程應卡在此處
 9     mutex=0;//若代碼能執行至此處,說明mutex爲假,即沒有其餘進程在臨界區,因而將mutex設爲真,告知其餘進程當前有(本)進程在臨界區
10 
11     /*臨界區代碼*/
12 
13     //準備退出臨界區,解開鎖
14     mutex=1;
15 
16     //執行非臨界區代碼
17 }

  可是上述代碼是沒法解決進程通訊問題的!緣由就是:若是沒有計算機底層(硬件或操做系統)的限制,那麼進程間的切換可能發生在任意兩條機器指令之間,更遑論高級程序語言的兩條語句之間。

  如今假設三個進程A、B、C共享某內存區,A已進入臨界區,因而欲進入臨界區的B在第8行代碼處卡住(想進入房子,一直循環判斷「鎖」的狀態)。忽然,A退出了臨界區,而且CPU切換到了B,因而B結束了第8行的循環(進入了房子),準備執行第9行代碼(上鎖),可是B還沒來得及「上鎖」,CPU又由於某特殊緣由如中斷,被切換到了進程C,而且進程C也想進入臨界區,因爲此時「鎖」是打開的,因而C直接結束了第8行的循環(直接進入房子),準備執行第9行代碼。顯然,此時臨界區內有兩個進程了。

  所以,實現進程通訊時須要注意的最大誤區就是:若是代碼中的語句(指令)不是特殊的,那麼任意兩條語句(指令)間均有被「打斷」的可能性。

 

  ③如何正確實現進程通訊

  咱們先看看一種不須要藉助計算機底層支持的解決進程通訊的方法:嚴格輪換法。而後再說說現代計算機實現進程通訊的基本技術。

  所謂嚴格輪換法,依然能夠抽象地將臨界區當作一個房子,可是此次咱們不是靠鎖來實現房子內只有一個進程,而是靠「鑰匙」,鑰匙在哪一個進程手上,哪一個進程就能夠進入臨界區,當該進程退出臨界區時,須要將鑰匙交給下一個進程(無論它要不要進入臨界區,反正「輪到你了」)。

  以三個進程0,1,2共享內存區爲例,則三個進程的臨界區代碼分別以下,假設key初始化爲0:

int key=0;
//
進程0 while(true) { //不斷判斷鑰匙是否在本身手上,若是不在則一直循環等下去 while(key!=0); //執行臨界區代碼 //退出臨界區,將鑰匙交給下一個進程 key=1; } //進程1 while(true) { //不斷判斷鑰匙是否在本身手上,若是不在則一直循環等下去 while(key!=1); //執行臨界區代碼 //退出臨界區,將鑰匙交給下一個進程 key=2; } //進程2 while(true) { //不斷判斷鑰匙是否在本身手上,若是不在則一直循環等下去 while(key!=2); //執行臨界區代碼 //退出臨界區,將鑰匙交給下一個進程 key=0; }

  嚴格輪換法的確能夠解決進程通訊,可是其效率很是低,緣由以下:

  1.嚴格輪換:若是此時key爲2,那麼只有進程2能夠進入臨界區,哪怕進程2從始至終都沒有進入臨界區,進程0和進程1也不得進入臨界區。

  2.忙等待:若是此時進程0佔用着CPU而key爲1或2,那麼進程0不能進入臨界區,只能一直循環判斷key的值,從整個系統的角度來講,CPU在「忙」,但進程卻在「等待」,也就是說CPU此時一直在空轉(就像汽車空擋猛踩油門)

 

  爲了解決忙等待和嚴格輪換的缺陷,一種新的思路被提了出來,我稱之爲沉睡與喚醒策略:令進程的共享內存區中保存一個特殊的「沉睡進程隊列」,若是進程A在準備進入臨界區時發現已有其餘進程在臨界區內,則進程A將本身的信息加入到沉睡進程隊列,即「沉睡」,而後自我阻塞,從而讓出CPU、避免忙等待,當臨界區內的進程退出臨界區時,須要負責檢查沉睡進程隊列,若隊列不爲空,則須要將其中的某個進程移出隊列,並將其「喚醒」,使其從阻塞態進入就緒態。固然,根據實現方式的不一樣,沉睡、阻塞多是一個操做,移出隊列和喚醒也是一個操做。

int mutex=1;//mutex爲1表示鎖打開,爲0表示鎖關閉
//
沉睡 void sleep(int process) { //將process所表示的進程加入到沉睡進程隊列 //將process由運行態轉換爲阻塞態 } //喚醒 void wakeup() { //檢查沉睡進程隊列,若不爲空,則移出某進程,並將其由阻塞態轉換爲就緒態 } //進程中的代碼 while(true) { //準備進入臨界區 if(mutex==0) sleep();

//上鎖
mutex=0;
//臨界區代碼 //退出臨界區,開鎖,喚醒沉睡進程
mutex=1; wakeup();
}

  顯然,上述代碼又踩了②中所提到的誤區。

  假設進程A在臨界區內,如今進程B佔用CPU而且想進入臨界區,B檢查mutex發現爲0,因而準備執行sleep(),可是此時CPU被進程A搶去了,進程A在本身的時間片斷內退出了臨界區,試着去「喚醒」沉睡的進程,可是卻沒有沉睡的進程,因而喚醒操做不了了之,接着進程A任務完成、結束。因而CPU又切換到了B,B繼續執行sleep(),進入沉睡並阻塞,可是再也沒有進程會來叫醒B了……

  上述現象能夠這麼說:B原本應該由A來喚醒,但恰恰A的喚醒沒有被B收到,由於B後來才沉睡。該現象出現的根本緣由就是:B決定沉睡和執行沉睡是兩個操做,這兩個操做之間可能被「打斷」。

  再假設,進程B沉睡,進程A在臨界區內且佔用CPU,進程A在時間片斷內執行完了臨界區代碼,解開了鎖,準備執行wakeup(),可是CPU此時被進程C搶走,由於鎖已解開,因此C進入了臨界區,一段時間後,C還沒有退出臨界區,但CPU再次切換至進程A,A繼續執行wakeup(),將B叫醒,因而B上鎖(其實鎖已經上了),進入臨界區,可是此刻C已在臨界區內!

  這個現象能夠這麼說:A原本打算解開鎖、叫醒B,但A解開鎖後,C乘虛而入了。這個現象出現的根本緣由就是:A解開鎖、叫醒沉睡進程是兩個操做,這兩個操做之間可能被「打斷」。此外,即便A先叫醒了B,B也不會再去上鎖,由於對於B來講被叫醒即sleep()結束,能夠直接開始臨界區操做,所以其餘進程仍是會由於鎖打開而進入臨界區!

  要想解決上述兩個問題,最直接的想法就是令「判斷是否沉睡」和「沉睡」合爲「一個操做」,「解開鎖」和「叫醒沉睡進程」也合爲「一個操做」,從而避免被打斷,假設下面的sleep()、wakeup()爲「原子操做」,即執行時不會被中斷,則沉睡與喚醒策略能夠實現:

//沉睡,假設爲原子操做
void sleep(int mutex)
{
    //檢查鎖mutex,若爲鎖上,則沉睡並阻塞調用者,不然上鎖並返回
}

//喚醒,假設爲原子操做
void wakeup(int mutex)
{
    //檢查沉睡進程隊列,若隊列不爲空,則喚醒其中某進程(不開鎖,由於被喚醒的進程不會再去上鎖),若隊列空,則解開鎖
}

//進程代碼
while(true)
{
    //準備進入臨界區
    sleep(mutex);

    /*臨界區代碼*/

    //退出臨界區,並喚醒存在的沉睡進程
    wakeup(mutex);
}

  在只有一個CPU的系統中,令sleep()、wakeup()成爲原子操做並不複雜,只要將sleep()設爲一個系統調用,而且操做系統在執行時(僅僅幾條指令的時間而已)暫時屏蔽中斷,從而避免執行sleep()、wakeup()時出現進程切換便可,多CPU系統中令sleep()成爲原子操做須要一些更特殊的指令或技術。

 

 

 

  ④經典的進程通訊問題與信號量機制

  首先看看「生產者-消費者」問題,該問題將引出沉睡與喚醒策略的升級版——信號量機制。

  生產者-消費者問題的背景以下:有兩個或多個進程分別爲producer和consumer,它們共享的內存區最多能夠存放N個產品,producer負責生產產品,consumer負責消費產品,若是共享內存區中已有N個產品,則producer須要沉睡,若是共享內存區中沒有產品,則consumer須要沉睡,而且producer和consumer不能同時訪問共享內存區。

  一個簡單的想法是這樣的:

int count=0; //表示共享內存區中的產品個數
int mutex=1; //進入臨界區的鎖,臨界區內的進程能夠操做共享內存區

//num即生產者編號,容許存在多個生產者進程
void
producer(int num) {
produce();
if(count==N) //沉睡 //欲操做共享區,即欲進入臨界區 sleep(mutex); put_product(); count++; if(count==1) //count爲1說明以前count爲0,consumer可能已沉睡,因此喚醒consumer //離開臨界區 wakeup(mutex); }
//num即消費者編號,容許存在多個消費者
void consumer(int num) { if(count==0) //沉睡 //欲操做共享區,即欲進入臨界區 sleep(mutex); take_product(); count--; if(count==N-1) //說明以前count爲N,producer可能已沉睡,因此喚醒producer //離開臨界區 wakeup(mutex);

consume(); }

 

  很顯然,上述代碼又踩了誤區,對count的判斷和對應的操做之間可能被打斷,從而可能錯過另外一方的喚醒:假設只有一個生產者和一個消費者,生產者發現count爲N,而後CPU就被消費者佔用,消費者取走一個產品並發現須要喚醒沉睡的生產者,可是此刻生產者沒有沉睡,因此喚醒操做不了了之,接着消費者退出臨界區,CPU被生產者佔用,生產者執行因count已滿而致使的沉睡,可是消費者再也不有機會叫醒它……

  不幸的是,sleep()和wakeup()並不能解決對count的判斷,由於count並非一把「鎖」(鎖只有兩個狀態,count不是),count是一種「信號量」,進程們經過count這個信號量的值來判斷本身是否須要沉睡,與進入臨界區而沉睡不一樣,這種沉睡是受條件所限而沉睡。可是解決這個問題只須要將sleep()和wakeup()稍加修改就能夠:

//down即新的sleep,也是一個「原子操做」,semaphore表示信號量,對應sleep()中的鎖
void down(int semaphore)
{
    //檢查semaphore,若大於0則使其減一併返回,若爲0則沉睡調用者
}

//up即新的wakeup,也是一個「原子操做」,semaphore表示信號量,對應wakeup()中的鎖
void up(int semaphore)
{
    //檢查semaphore,若爲0則喚醒沉睡進程隊列中的某沉睡進程,不然令semaphore+1
}

 

  與sleep()和wakeup()的參數爲鎖不一樣,信號量機制的參數爲「信號量」,從而能夠解決生產者-消費者問題,同時也能夠替代sleep()和wakeup(),由於鎖也能夠當作是一個信號量:

//對共享內存區稍做修改,新增變量empty表示空位置個數,count依然表示產品個數
int count=0;
int empty=N;
int mutex=1; //進入臨界區的鎖,1開0閉,臨界區內的進程容許操做共享內存區

//num表示生產者編號,容許存在多個生產者 void producer(int num) { produce(); down(empty);//減小一個空位置,若已爲0則沉睡 //進入臨界區 down(mutex); put_product(); //離開臨界區 up(mutex); up(count);//若產品數原先爲0,則喚醒由於沒有產品而沉睡的消費者進程,不然令產品數+1 }
//num表示消費者編號,容許存在多個消費者
void consumer(int num) { down(count);//減小一個產品數,若已爲0則沉睡 //進入臨界區 down(mutex); take_product(); //離開臨界區 up(mutex); up(empty);//若空位置原先爲0,則喚醒由於沒有空位置而沉睡的生產者進程,不然令空位置+1 consume(); }

  

  信號量機制的理解並不困難,我的估計惟一的困惑點就是爲何當semaphore爲0時,up()不須要令semaphore+1,這一點的解釋用一句話來講就是:由於當semaphore爲0時,down()沒有令semaphore-1。也能夠類比wakeup(),wakeup()在有沉睡進程時是不打開鎖的,由於被喚醒的進程不會再去上鎖。

  生產者-消費者問題,以及信號量機制帶來了一個新的思考:進程間可能不只僅存在共享內存區的讀寫衝突問題,還可能存在「資源」共享的問題。在生-消背景下,空位置就是生產者須要的資源,而產品就是消費者須要的資源,進程得在有了須要的資源後才能作本身要作的事。本文最初提到的字處理-打印問題就是生-消問題,只是咱們忽略了打印機進程在發現out==in時的操做,若是打印機進程在out==in時選擇沉睡,那麼字處理進程就得負責將其喚醒。

 

 

  接下來看看哲學家就餐問題,該問題能夠進一步地體現進程間資源搶佔可能致使的問題。假設有5個哲學家圍着桌子坐,每一個人面前都有足夠的食物,但筷子只有5根,見圖:

  

  每一個哲學家只會作兩件事:思考、吃飯。而每當須要吃飯時,哲學家必須取兩根筷子才能吃,而且只能取本身左手和右手的筷子,如上圖哲學家A只能用筷子0和筷子1吃飯。

  顯然,各個哲學家就至關於各個進程,筷子就是它們須要共享的資源(而且共享方式是連鎖的)。先來看看錯誤的代碼:

int chopMutex[5]={1}; //5根筷子各自的信號量(鎖),1可取0不可取

//
哲學家進程,num表示哲學家編號,從0到4對應從A到E void philosopher(int num) { while(true) { think(); //思考 down(chopMutex[num]); //拿左邊的筷子,若已被左邊的哲學家取走,則阻塞 down(chopMutex[(num+1)%5]); //拿右邊的筷子,若已被右邊的哲學家取走,則阻塞 eat(); //吃飯 //逐個放下筷子 up(chopMutex[num]); up(chopMutex[(num+1)%5]); } }

  上述代碼初看彷彿沒有問題,每一個哲學家都利用信號量機制(此處的信號量即單根筷子的鎖)來取筷子。但其實上述代碼是有問題的:若A拿起了左邊的筷子後就切換到了B,B也拿起了左邊的筷子,而後又切換到了C,C也拿起了左邊的筷子……最後,每一個哲學家都拿到了本身左手邊的筷子,可是每一個哲學家都會由於拿不到右邊的筷子而一直阻塞下去。這種現象咱們稱之爲「死鎖」:一個進程的集合中,每個進程都在等待只能由同一集合中的其餘進程才能觸發的事件(好比釋放某資源)。

  解決哲學家問題的簡單解法是:令同一時間只容許一個哲學家吃飯。

int qualification=1;  //表明吃飯的權利
//
哲學家進程,num表示哲學家編號,從0到4對應從A到E void philosopher(int num) { while(true) { think(); //思考 down(qualification); //試圖獲取吃飯的權利 //拿筷子,吃飯 takeChopsticks(num); takeChopsticks((num+1)%5); eat(); //放下筷子,中止吃飯 putChopsticks(num); putChopsticks((num+1)%5); up(qualification); //交出吃飯權利,即吃完了 } }

 

  上述解法沒有問題,只是有缺陷:5根筷子明明能夠支持兩個哲學家吃飯,好比A和C或者A和D一塊兒吃,上述解法卻只讓一我的吃。

  能夠解決該缺陷的一種解法是,設置一個mutex做爲進入臨界區的鎖,再令每一個哲學家對應兩個信號量,state和qualification,state表示該哲學家的「狀態」:思考、想吃飯、在吃飯;qualification表示該哲學家的「資格」:如今有沒有資格吃飯。每一個哲學家均可以讀、寫任一哲學家的狀態和資格,所以臨界區即讀寫哲學家狀態、資格的代碼。

  當哲學家X準備吃飯即想拿筷子時,先進入臨界區(從而能夠讀寫任一哲學家的state和qualification),而後將本身的狀態改成想吃飯,接着檢查本身左右兩邊的哲學家是否在吃飯,若是均不在吃飯,則本身有資格吃飯,因而經過up()使本身的qualification+1,而後退出臨界區,再經過down()使用掉本身的資格;若是左右兩邊有哲學家在吃飯,則不使本身的qualification+1,退出臨界區,再經過down()使用本身的資格,可是由於沒有資格,X將阻塞於此,直到正在吃飯的旁邊哲學家吃完飯,而後給予本身資格。

  當哲學家Y吃完飯即放下筷子時,先進入臨界區,將本身的狀態改成思考,接着檢查本身左右兩邊的哲學家是否想吃飯且有資格吃飯,如果則令其qualification+1從而使其得以吃飯,左右哲學家均處理完畢後Y退出臨界區。

#define THINKING 0                 //在思考
#define HUNGRY 1                   //想吃飯
#define EATING 2                   //在吃飯
int state[5]={THINKING};           //表示各個哲學家的狀態
int mutex=1;                       //臨界區的鎖,爲0表示鎖上
int qualification[5]={0};          //哲學家的資格,爲1時表示能夠吃飯,0表示不能夠

//檢查哲學家i是否想吃飯且有資格吃飯 void check(int i) { //若哲學家i想吃飯,且其左右哲學家均不在吃飯,則i的吃飯資格+1,而且將i的狀態改成正在吃飯 if(state[i]==HUNGRY && state[(i+4)%5]!=EATING && state[(i+1)%5]!=EATING) {
    state[i]=EATING;
   up(qualification[i]);   }
}
void takeChopsticks(int i) { down(mutex); //進入臨界區(臨界區內可讀、寫哲學家的狀態和資格) state[i]=HUNGRY; //代表i想吃飯 check(i); //檢查本身是否有資格吃飯 up(mutex); //離開臨界區 down(qualification[i]); //若check(i)時確認本身有資格吃飯,則此處用去吃飯資格,不然阻塞直至被給予吃飯資格 } void putChopsticks(int i) { down(mutex); //進入臨界區(臨界區內可讀、寫哲學家的狀態和資格) state[i]=THINKING; //代表本身不在吃飯也不想吃飯 check((i+4)%5); //檢查左邊的哲學家是否想且有資格吃飯,如果則給予他資格 check((i+1)%5); //檢查右邊的哲學家是否想且有資格吃飯,如果則給予他資格 up(mutex); //離開臨界區 } void philosopher(int i) { while(true) { think(); takeChopsticks(i); putChopsticks(i); } }

   哲學家進餐問題比生產者-消費者問題要更復雜,由於進程須要的資源不是一種而是兩種,並且這兩種資源的競爭對象不同。解決這類問題的關鍵點就是:若是進程X須要x個資源,則X要麼一次性佔用這x個資源,要麼一個都不佔用直到能夠一次性佔用着x個資源,不能出現佔用一部分資源而後等待的狀況。

 

 

   最後提出的問題是最複雜的,叫讀者-寫者問題,在哲學家進餐問題中,資源是「獨享」式的:一根筷子若是被一個哲學家取走了,則這根筷子只能屬於該哲學家,除非他放下筷子。可是在讀者-寫者問題中,資源是既「獨享」又「共享」的,咱們先看看其背景:

  假設存在大量進程共享一個數據庫(或文件),爲了簡化問題,咱們再假設進程要麼是隻會讀取數據庫的「讀者」,要麼是隻會寫入數據庫的「寫者」,同一時間數據庫要麼有一個寫者在寫、要麼有不限量個讀者在讀、要麼沒有進程訪問。

  根據上述要求,該數據庫做爲一種資源,在讀者與寫者之間、寫者與寫者之間是「獨享」的:有讀者在數據庫則寫者得等,有寫者在數據庫則讀者、其餘寫者得等。可是在讀者與讀者之間又是「共享」的:有讀者在數據庫則其餘後到的讀者能夠進去讀。

  若是不容許讀者-讀者共享,那麼問題就變得很簡單,只要給數據庫上一把「鎖」便可,有進程在數據庫,其餘想進去的進程就得沉睡。因此讀者-寫者問題的關鍵難點就是:如何令已有讀者在讀的狀況下,後來的讀者能夠進去?

  根據關鍵難點的描述,一種被稱爲「讀者優先」的解決思路被提出來:

  設置共享變量rd_count,表示當前數據庫中讀者的數量,設置一把鎖rdc_mutex,進程要想操做rd_count,必須利用鎖rdc_mutex進出「rd_count的臨界區」。再設置一把鎖db_mutex,表示數據庫的鎖。

  寫者想進入數據庫,必須在數據庫內無人的狀況下才行,即db_mutex解開時才能夠進入數據庫。同理,寫者退出數據庫時,必須解開db_mutex鎖。

  若讀者想進入數據庫,則必須知足兩個條件其中一個:

  1.數據庫內無人且鎖打開  2.數據庫內有人可是是讀者。

  同理,讀者退出數據庫時,若數據庫內還有人(讀者)則直接推出,不然退出並解開db_mutex。咱們能夠藉助rd_count來實現對第二點的判斷,詳情見代碼:

//讀者優先解法
int db_mutex=1;   //數據庫的鎖,db即database,1開0閉
int rd_count=0;     //表示數據庫中讀者的數量,rd即reader
int rdc_mutex=1;  //rd_count的鎖,rdc即reader_count,1開0閉

//讀者進程,num即讀者編號
void reader(int num)
{
    while(true)
    {    
        /***準備進入數據庫***/
        //先經過rdc_mutex進入rd_count臨界區
        down(rdc_mutex);

        //判斷數據庫內是否已有讀者,如果,則負責搶數據庫的鎖
        if(rd_count==0)   
            down(db_mutex);
        //若本身是「第一個」讀者,則執行至此時已搶到數據庫並上了鎖,因此令rd_count++
        //若本身不是「第一個」讀者,則直接令rd_count++
        rd_count++;  
        up(rdc_mutex);    //離開rd_count臨界區

        read_data();   //讀取數據庫數據

        /***準備離開數據庫***/
        //經過rdc_mutex進入rd_count臨界區
        down(rdc_mutex);
        //令rd_count--後判斷數據庫內是否已無讀者,如果則解開數據庫的鎖,喚醒沉睡進程(如有,必爲寫者)
        rd_count--;
        if(rd_count==0)
            up(db_mutex);
        up(rdc_mutex);    //離開rd_count臨界區

        use_data();
    }
}

//寫者進程,num即編號
void writer(int num)
{
    while(true)
    {
        produce_data();

        //欲進入數據庫,檢查鎖,若鎖上沉睡,不然鎖上並進入
        down(db_mutex);
        write_data();
        up(db_mutex);   //離開數據庫,喚醒沉睡進程(多是讀者也多是寫者)
    }
}/

 

  顯然,上述代碼能夠知足讀者-寫者問題的問題,只是存在一點「缺陷」:若是不斷地有讀者到來,以至於數據庫內老是至少有一個讀者,那麼寫者將永遠沒有機會進入數據庫。這也是該解法被稱爲「讀者優先」的緣由。由於只要數據庫內有讀者,那麼後面來的讀者就能夠進入數據庫,而不須要在乎是否有先到的寫者想進入數據庫。

  可是在某些狀況下,咱們但願算法能知足:即便數據庫內有讀者,若是有寫者先到達(在等待),那麼後到達的讀者也不能進入數據庫,必須讓先到達的、等待中的寫者使用完數據庫後才能夠進入數據庫。

  要想知足該要求,被稱爲「讀寫平等」的解決思路被提了出來:在數據庫「門前」設立一個「候選人」位置,每一個進程必須先成爲候選人,再判斷可否進入數據庫。利用候選人機制,即便數據庫內有讀者(數量不定),只要寫者搶佔了候選人位置,後到達的讀者就不能進入數據庫,同理後到達的寫者也需等待。若是令因沒搶到候選人而沉睡的進程按到達時間順序排成隊列,而且喚醒時按隊列順序進行,那麼進程訪問數據庫的順序就是時間順序的,所以這個算法也被稱爲「讀寫平等」算法。舉例來講,數據庫內有進程,而後寫者A到達、成爲候選人、沉睡,多個讀者到達、等待候選人位置、沉睡,寫者B到達、等待候選人位置、沉睡,那麼最後這些進程必定是按「寫者A」、「讀者羣」、「寫者B」的順序進入數據庫,也即按時間順序進入的數據庫,從而實現「讀寫平等」。

 

//讀寫平等
int candidate=1;  //候選人資格鎖,1無候選人0有候選人
int rd_count=0;    //讀者數量
int db_mutex=1;  //數據庫鎖,1開0閉
int rdc_mutex=1;  //rd_count的鎖,須要鎖是由於候選人讀者和欲離開數據庫的讀者都須要讀寫rd_count

void reader()
{
    while(true)
    {
        //欲進入數據庫,先爭奪候選人
        down(candidate);
        //成爲候選人後,進入rd_count臨界區
        down(rd_count);
        //若數據庫內無讀者,則本身是第一個讀者,負責給數據庫上鎖,以防止寫者進入
        if(rd_count==0)      
            down(db_mutex);
        //修改讀者數量,離開rd_count臨界區,解開候選人鎖(由於本身進入數據庫)
        rd_count++;
        up(rd_count);
        up(candidate);

        read_data();//數據庫內操做

        //欲離開數據庫,進入rd_count臨界區,若本身是最後一個讀者,解開數據庫鎖
        down(rd_count);
        rd_count--;
        if(rd_count==0)
            up(db_mutex);
        up(rd_count);

        use_data();  //數據庫外操做
    }
}    

void writer()
{
    while(true)
    {
        produce_data();  //數據庫外操做

        //欲進入數據庫,先奪得候選人資格
        down(candidate);
        //成爲候選人後,給數據庫上鎖,以保證只有本身在內
        down(db_mutex);
        up(candidate);   //進入數據庫,解開候選人資格鎖

        write_data();   //數據庫內操做

        //離開數據庫,解開數據庫鎖
        up(db_mutex);   
     }
}

 

 

   在某些特殊狀況下,咱們可能須要一個更加極端的讀者-寫者算法,那就是「寫者優先」

  1.只要有寫者在等待,想進入數據庫的讀者就必須等待

  2.數據庫鎖由鎖上變爲打開時,優先喚醒寫者進程,不管是否有先於其到達的讀者(從而打破了時間順序,令寫者有了優先權)

  回顧讀寫平等算法,能夠發現當數據庫鎖解開時,離開數據庫的要麼是寫者,要麼是最後的讀者。

  若是離開的是讀者,並且有沉睡候選人,那麼沉睡候選人必定是寫者(讀者不會由於數據庫內有讀者而沉睡),因此最後的讀者只須要喚醒候選人便可保證「寫者優先」。

  問題出在離開的是寫者的狀況,寫者離開時的沉睡候選人既多是寫者,也多是讀者,但寫者離開時並無考慮這一點。所以要想實現寫者優先,須要下手的是寫者進程,讓它們變得更爲本身人考慮。

  在讀者優先算法中,讀者能「給本身人優先權」的根本緣由在於讀者掌控着數據庫鎖,只要讀者不解開這把鎖,寫者就沒法進入,但其它讀者經過rd_count,獲得了必定狀況下無視數據庫鎖的「特權」。

  所以,一種相似的解法被提了出來:設置變量wt_count表示數據庫內以及想進入數據庫的寫者總數,想進入數據庫的寫者先經過wt_count判斷數據庫內是否有寫者,若無則競爭候選人,再等待數據庫鎖,如有則直接等待數據庫鎖;想離開數據庫的寫者直接釋放數據庫鎖(如有等待中的寫者,此後便可進入),再經過wt_count判斷是否還有其餘寫者,若無則解開候選人鎖,如有則直接走人,由「最後一個」寫者負責解開候選人鎖。這個想法就是利用候選人鎖,使得寫者得以「卡住」數據庫、保證如有其餘寫者則將數據庫讓給其餘寫者。從而實現了「寫者優先」

//寫者優先
int candidate=1;  //候選人資格鎖,1無候選人0有候選人
int wt_count=0;   //數據庫內及想進入數據庫的寫者數量
int rd_count=0;    //數據庫內的讀者數量
int db_mutex=1;  //數據庫鎖,1開0閉
int rdc_mutex=1;  //rd_count的鎖,須要鎖是由於候選人讀者和欲離開數據庫的讀者都須要讀寫rd_count
int wtc_mutex=1; //wt_count的鎖,須要鎖是由於欲進入數據庫的寫者和欲離開數據庫的寫者都須要讀寫wt_count


//reader進程與讀寫平等時相同
void reader()
{
    while(true)
    {
        //欲進入數據庫,先爭奪候選人
        down(candidate);
        //成爲候選人後,進入rd_count臨界區
        down(rd_count);
        //若數據庫內無讀者,則本身是第一個讀者,負責給數據庫上鎖,以防止寫者進入
        if(rd_count==0)      
            down(db_mutex);
        //修改讀者數量,離開rd_count臨界區,解開候選人鎖(由於本身進入數據庫)
        rd_count++;
        up(rd_count);
        up(candidate);

        read_data();//數據庫內操做

        //欲離開數據庫,進入rd_count臨界區,若本身是最後一個讀者,解開數據庫鎖
        down(rd_count);
        rd_count--;
        if(rd_count==0)
            up(db_mutex);
        up(rd_count);

        use_data();  //數據庫外操做
    }
}    

void writer()
{
    while(true)
    {
        produce_data();  //數據庫外操做

        //欲進入數據庫,先進入wt_count臨界區,判斷本身是不是「第一個」寫者
        down(wtc_mutex);
        wt_count++;
        if(wt_count==1)   //若本身是「第一個」寫者,則須要搶奪候選人
            down(candidate);
        down(db_mutex);  //不論本身是不是「第一個」寫者,都須要等待數據庫鎖
        
        write_data();  //數據庫內操做

        //欲離開數據庫,直接解開數據庫鎖,再進入wt_count臨界區,判斷本身是不是「最後一個」寫者
        up(db_mutex);
        down(wtc_mutex);
        wt_count--;
        if(wt_count==0)   //若本身是「最後一個」寫者,則解開候選人鎖,從而令讀者有機會搶奪候選人、進入數據庫
            up(candidate);
        up(wtc_mutex);
    }
}

 

 

 

   ⑤避免編程失誤的「管程」

  回顧三個經典進程通訊問題,能夠發現,信號量機制的確能夠解決進程通訊的問題,可是編程較爲麻煩且容易出錯形成死鎖,以生產者-消費者問題爲例,若是由於編程時的失誤,某個生產者進程對信號量的操做順序從

down(empty);//減小一個空位置,若已爲0則沉睡
down(mutex);//進入臨界區

  變成了

down(mutex);//進入臨界區
down(empty);//減小一個空位置,若已爲0則沉睡

  那麼生產者就可能由於進入臨界區後發現已無空位置而沉睡,而且沒有解開mutex從而致使消費者無法取走產品,形成進程間的死鎖。也就是說,經過直接對進程編程來使用信號量是「比較危險」的作法,一不當心就可能形成死鎖等異常狀況。所以,一種新的利用信號量實現進程通訊的思想被提了出來:管程。

  經過對進程通訊的分析,能夠發現,同一類進程對信號量的操做是相同的,好比生-消問題中的生產者,都是執行以下代碼

down(empty);
down(mutex);
put_product();
up(mutex);
up(count);

  而消費者都是執行以下代碼

down(count);
down(mutex);
take_product();
up(mutex);
up(empty);

  那麼,咱們是否能夠作出以下的一個獨立的「模塊」

int empty=N;
int count=0;

void put_product(productType x)
{
    down(empty);
    put(x);
    up(count);
}

void take_product(productType &x)
{
    down(count);
    x=take();
    up(empty);
}

  而後作出以下限制(爲簡便,put_product()簡記爲p(),take_product()簡記爲t()):

  1.同一時間只能有一個進程在執行p()或t(),其它調用了p()或t()的進程排隊等待

  2.一個進程若在執行p()或t()時由於down()某個信號量而阻塞,則掛起,讓等待執行p()或t()的另外一個進程執行其調用的p()或t()

  3.一個進程若在執行p()或t()時由於up()某個信號量而喚醒了某沉睡進程,則在當前進程退出p()或t()後,令被喚醒進程執行其以前調用的p()或t()

  若是能實現這一限制,那麼生產者和消費者就能夠簡單的完成本身想作的事,避免編程失誤或者說方便進程通訊出錯時debug

void producer()
{
    productType x;
    while(true)
    {
        x=produce();  //生產產品
        put_product(x);  //放置產品
    }
}

void consumer()
{
    productType x;
    while(true)
    {
        take_product(&x);  //取得產品
        consume(x);   //消費產品
    }
}

  上述的所謂「模塊」就是所謂的管程,習慣面向對象編程的人也能夠將其視爲一個類。之因此將這種實現進程通訊的技術稱之爲管程,是由於在旁人看來,管程就是一個管理員,其負責保證同一時間只能有一個進程調用某些方式,而且負責這些進程的沉睡與喚醒。

  須要注意的是,就像信號量機制須要計算機底層的支持同樣,管程也不是任意狀況下均能實現。好比C語言就不可能實現管程,由於C語言沒法知足管程須要的條件。可是有一些語言是能夠實現管程的,好比JAVA,利用關鍵詞synchronized,可使同一個類中的某些方法不能被「同時」執行,藉助此支持,再將生產者、消費者、管程寫在同一個類中,就能夠實現(線程級別的)管程思想。

 

 

 

  有關進程通訊的基礎知識就是上面這些,可是進程通訊問題引出了另外一個問題——死鎖。雖然本文提到過死鎖,但一直是在避免死鎖的出現。那麼死鎖萬一出現了,該如何令操做系統知曉呢?操做系統知道有死鎖發生後,能不能解開死鎖呢?這類問題與進程通訊有關,但又自成一派,所以將其留做往後單獨討論。

相關文章
相關標籤/搜索