歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> Linux進程間通信(IPC)編程實踐(十)System V信號量---PV操作經典題目

Linux進程間通信(IPC)編程實踐(十)System V信號量---PV操作經典題目

日期:2017/3/1 12:20:18   编辑:關於Linux

//P原語 //P(semaphore *S)
wait(semaphore *S) {
-- S->value; if (S->value < 0)
{ //將當前進程設置為阻塞狀態
//將當前進程的PCB插入相應的阻塞隊列S->list末尾 block(S->list);
} } [

//V原語 //V(semaphore *S)
signal(semaphore *S) {
++ S->value; if (S->value <= 0) //表示有進程處於阻塞狀態
{ //喚醒阻塞隊列S->list中等待的一個進程,將其置為就緒態;
//將其插入就緒隊列; wakeup (S->list);
} } p操作(wait):申請一個單位資源,進程進入

v操作(signal):釋放一個單位資源,進程出來

使用PV操作實現進程互斥時應該注意的是:

(1)每個程序中用戶實現互斥的P、V操作必須成對出現,先做P操作,進臨界區,後做V操作,出臨界區。若有多個分支,要認真檢查其成對性。

(2)P、V操作應分別緊靠臨界區的頭尾部,臨界區的代碼應盡可能短,不能有死循環。

(3)互斥信號量的初值一般為1。

在接下來的經典題目的分析中,我們可以 將P操作看作是判斷型操作,也就是if;將V操作看作是通知型的操作,這樣更容易寫偽代碼。

(一)生產者消費者問題

生產者一消費者問題(producer-consumerproblem)是指若干進程通過有限的共享緩沖區交換數據時的緩沖區資源使用問題。假設“生產者”進程不斷向共享緩沖區寫人數據(即生產數據),而“消費者”進程不斷從共享緩沖區讀出數據(即消費數據);共享緩沖區共有n個;任何時刻只能有一個進程可對共享緩沖區進行操作。所有生產者和消費者之間要協調,以完成對共享緩沖區的操作。

生產者進程結構: do{
wait(empty) ; wait(mutex) ;
add nextp to buffer
signal(mutex) ;
signal(full) ; }while(1) ;
消費者進程結構:
do{ wait(full) ;
wait(mutex) ;

remove an item from buffer to nextp

signal(mutex) ; signal(empty) ;
}while(1) ;

我們可把共享緩沖區中的n個緩沖塊視為共享資源,生產者寫人數據的緩沖塊成為消費者可用資源,而消費者讀出數據後的緩沖塊成為生產者的可用資源。為此,可設置三個信號量:full、empty和mutex。其中:full表示有數據的緩沖塊數目,初值是0;empty表示空的緩沖塊數初值是n;mutex用於訪問緩沖區時的互斥,初值是1。實際上,full和empty間存在如下關系:full + empty = N注意:這裡每個進程中各個P操作的次序是重要的。各進程必須先檢查自己對應的資源數在確信有可用資源後再申請對整個緩沖區的互斥操作;否則,先申請對整個緩沖區的互斥操後申請自己對應的緩沖塊資源,就可能死鎖。出現死鎖的條件是,申請到對整個緩沖區的互斥操作後,才發現自己對應的緩沖塊資源,這時已不可能放棄對整個緩沖區的占用。如果采用AND信號量集,相應的進入區和退出區都很簡單。如生產者的進入區為Swait(empty,mutex),退出區為Ssignal(full,mutex)。
(二)讀者寫者問題 讀者—寫者問題(Readers-Writers problem)也是一個經典的並發程序設計問題,是經常出現的一種同步問題。計算機系統中的數據(文件、記錄)常被多個進程共享,但其中某些進程可能只要求讀數據(稱為讀者Reader);另一些進程則要求修改數據(稱為寫者Writer)。就共享數據而言,Reader和Writer是兩組並發進程共享一組數據區,要求: (1)允許多個讀者同時執行讀操作; (2)不允許讀者、寫者同時操作; (3)不允許多個寫者同時操作。 我們使用讀者優先策略解決: int rc=0; //用於記錄當前的讀者數量 semaphore rc_mutex=1;//用於對共享變量rc 操作的互斥信號量 semaphore write=1; //用於保證讀者和寫者互斥地訪問的信號量 void reader() do{ P(rc_mutex); //開始對rc共享變量進行互斥訪問 rc ++; //來了一個讀進程,讀進程數加1 if (rc==1) P(write);//如是第一個讀進程,判斷是否有寫進程在臨界區, //若有,讀進程等待,若無,阻塞寫進程 V(rc_mutex); //結束對rc共享變量的互斥訪問 讀文件; P(rc_mutex); //開始對rc共享變量的互斥訪問 rc--; //一個讀進程讀完,讀進程數減1 if (rc == 0) V(write); //最後一個離開臨界區的讀進程需要判斷是否有寫進程//需要進入臨界區,若有,喚醒一個寫進程進臨界區 V(rc_mutex);//結束對rc共享變量的互斥訪問 } while(1) void writer() do{ P(write); //無讀進程,進入寫進程;若有讀進程,寫進程等待 寫文件; V(write);//寫進程完成;判斷是否有讀進程需要進入臨界區, //若有,喚醒一個讀進程進臨界區 } while(1) 讀者優先的設計思想是讀進程只要看到有其它讀進程正在讀,就可以繼續進行讀;寫進程必須等待所有讀進程都不讀時才能寫,即使寫進程可能比一些讀進程更早提出申請。該算法只要還有一個讀者在活動,就允許後續的讀者進來,該策略的結果是,如果有一個穩定的讀者流存在,那麼這些讀者將在到達後被允許進入。而寫者就始終被掛起,直到沒有讀者為止。 (三)哲學家就餐問題 假設有五位哲學家圍坐在一張圓形餐桌旁,做以下兩件事情之一:吃飯,或者思考。吃東西的時候,他們就停止思考,思考的時候也停止吃東西。每兩個哲學家之間有一只餐叉。因為用一只餐叉很難吃飯,所以假設哲學家必須用兩只餐叉吃東西, 而且他們只能使用自己左右手邊的那兩只餐叉。 [cpp] view plaincopy void philosopher(int i) /*i:哲學家編號,從0 到4*/ { while (TRUE) { think( ); /*哲學家正在思考*/ take_fork(i); /*取左側的筷子*/ take_fork((i+1) % N); /*取左側筷子;%為取模運算*/ eat( ); /*吃飯*/ put_fork(i); /*把左側筷子放回桌子*/ put_fork((i+1) % N); /*把右側筷子放回桌子*/ } } 分析:假如所有的哲學家都同時拿起左側筷子,看到右側筷子不可用,又都放下左側筷子, 等一會兒,又同時拿起左側筷子,如此這般,永遠重復。對於這種情況,即所有的程序都在 無限期地運行,但是都無法取得任何進展,即出現饑餓,所有哲學家都吃不上飯。 問題算法描述: 規定在拿到左側的筷子後,先檢查右面的筷子是否可用。如果不可用,則先放下左側筷子, 等一段時間再重復整個過程。 分析:當出現以下情形,在某一個瞬間,所有的哲學家都同時啟動這個算法,拿起左側的筷子,而看到右側筷子不可用,又都放下左側筷子,等一會兒,又同時拿起左側筷子……如此這樣永遠重復下去。對於這種情況,所有的程序都在運行,但卻無法取得進展,即出現饑餓,所有的哲學家都吃不上飯。 2) 描述一種沒有人餓死(永遠拿不到筷子)算法。 考慮了四種實現的方式(A、B、C、D): A: 原理:至多只允許四個哲學家同時進餐,以保證至少有一個哲學家能夠進餐,最終總會釋放出他所使用過的兩支筷子,從而可使更多的哲學家進餐。以下將room 作為信號量,只允許4 個哲學家同時進入餐廳就餐,這樣就能保證至少有一個哲學家可以就餐,而申請進入餐廳的哲學家進入room 的等待隊列,根據FIFO 的原則,總會進入到餐廳就餐,因此不會出現餓死和死鎖的現象。 偽碼: [cpp] view plaincopy semaphore chopstick[5]={1,1,1,1,1}; semaphore room=4; void philosopher(int i) { while(true) { think(); wait(room); //請求進入房間進餐 wait(chopstick[i]); //請求左手邊的筷子 wait(chopstick[(i+1)%5]); //請求右手邊的筷子 eat(); signal(chopstick[(i+1)%5]); //釋放右手邊的筷子 signal(chopstick[i]); //釋放左手邊的筷子 signal(room); //退出房間釋放信號量room } } B: 原理:僅當哲學家的左右兩支筷子都可用時,才允許他拿起筷子進餐。 方法1:利用AND 型信號量機制實現:根據課程講述,在一個原語中,將一段代碼同時需要的多個臨界資源,要麼全部分配給它,要麼一個都不分配,因此不會出現死鎖的情形。當某些資源不夠時阻塞調用進程;由於等待隊列的存在,使得對資源的請求滿足FIFO 的要求,因此不會出現饑餓的情形。 偽碼: semaphore chopstick[5]={1,1,1,1,1}; void philosopher(int I) { while(true) { think(); Swait(chopstick[(I+1)]%5,chopstick[I]); eat(); Ssignal(chopstick[(I+1)]%5,chopstick[I]); } } 方法2:利用信號量的保護機制實現。通過信號量mutex對eat()之前的取左側和右側筷子的操作進行保護,使之成為一個原子操作,這樣可以防止死鎖的出現。 偽碼: semaphore mutex = 1 ; semaphore chopstick[5]={1,1,1,1,1}; void philosopher(int I) { while(true) { think(); wait(mutex); wait(chopstick[(I+1)]%5); wait(chopstick[I]); signal(mutex); eat(); signal(chopstick[(I+1)]%5); signal(chopstick[I]); } } C: 原理:規定奇數號的哲學家先拿起他左邊的筷子,然後再去拿他右邊的筷子;而偶數號的哲學家則相反.按此規定,將是1,2號哲學家競爭1號筷子,3,4號哲學家競爭3號筷子.即五個哲學家都競爭奇數號筷子,獲得後,再去競爭偶數號筷子,最後總會有一個哲學家能獲得兩支筷子而進餐。而申請不到的哲學家進入阻塞等待隊列,根FIFO原則,則先申請的哲學家會較先可以吃飯,因此不會出現餓死的哲學家。 偽碼: semaphore chopstick[5]={1,1,1,1,1}; void philosopher(int i) { while(true) { think(); if(i%2 == 0) //偶數哲學家,先右後左。 { wait (chopstick[ i + 1 ] mod 5) ; wait (chopstick[ i]) ; eat(); signal (chopstick[ i + 1 ] mod 5) ; signal (chopstick[ i]) ; } Else //奇數哲學家,先左後右。 { wait (chopstick[ i]) ; wait (chopstick[ i + 1 ] mod 5) ; eat(); signal (chopstick[ i]) ; signal (chopstick[ i + 1 ] mod 5) ; } } } D: 利用管程機制實現(最終該實現是失敗的,見以下分析): 原理:不是對每只筷子設置信號量,而是對每個哲學家設置信號量。test()函數有以下作用: a. 如果當前處理的哲學家處於饑餓狀態且兩側哲學家不在吃飯狀態,則當前哲學家通過test()函數試圖進入吃飯狀態。 b. 如果通過test()進入吃飯狀態不成功,那麼當前哲學家就在該信號量阻塞等待,直到其他的哲學家進程通過test()將該哲學家的狀態設置為EATING。 c. 當一個哲學家進程調用put_forks()放下筷子的時候,會通過test()測試它的鄰居,如果鄰居處於饑餓狀態,且該鄰居的鄰居不在吃飯狀態,則該鄰居進入吃飯狀態。 由上所述,該算法不會出現死鎖,因為一個哲學家只有在兩個鄰座都不在進餐時,才允許轉換到進餐狀態。 該算法會出現某個哲學家適終無法吃飯的情況,即當該哲學家的左右兩個哲學家交替處在吃飯的狀態的時候,則該哲學家始終無法進入吃飯的狀態,因此不滿足題目的要求。 但是該算法能夠實現對於任意多位哲學家的情況都能獲得最大的並行度,因此具有重要的意義。 偽碼: #define N 5 /* 哲學家人數*/ #define LEFT (i-1+N)%N /* i的左鄰號碼 */ #define RIGHT (i+1)%N /* i的右鄰號碼 */ typedef enum { THINKING, HUNGRY, EATING } phil_state; /*哲學家狀態*/ monitor dp /*管程*/ { phil_state state[N]; semaphore mutex =1; semaphore s[N]; /*每個哲學家一個信號量,初始值為0*/ void test(int i) { if ( state[i] == HUNGRY &&state[LEFT(i)] != EATING && state[RIGHT(i)] != EATING ) { state[i] = EATING; V(s[i]); } } void get_forks(int i) { P(mutex); state[i] = HUNGRY; test(i); /*試圖得到兩支筷子*/ V(mutex); P(s[i]); /*得不到筷子則阻塞*/ } void put_forks(int i) { P(mutex); state[i]= THINKING; test(LEFT(i)); /*看左鄰是否進餐*/ test(RIGHT(i)); /*看右鄰是否進餐*/ V(mutex); } } 哲學家進程如下: void philosopher(int process) { while(true) { think(); get_forks(process); eat(); put_forks(process); } }
Copyright © Linux教程網 All Rights Reserved