信號量是一種變量類型,用一個記錄型數據結構表示,有兩個分量:信號量的值和信號量隊列指針
除了賦初值外,信號量僅能通過同步原語PV對其進行操作
s.value為正時,此值為封鎖進程前對s信號量可施行的P操作數,即s代表實際可用的物理資源
s.value為負時,其絕對值為對信號量s實施P操作而被封鎖並進入信號量s等待隊列的進程數,即登記排列在s信號量隊列之中等待的進程個數
s.value為0時,無資源且無進程等待
信號量按其取值可分為二值信號量和一般信號量(計數信號量),記錄型信號量和PV操作定義為如下數據結構和不可中斷過程:
typedef struct semaphore { int value; struct pcb *list; //信號量隊列指針 } void P(semaphore s){ s.value--; if(s.value<0) sleep(s.list); //若信號量值小於0,執行P操作的進程調用sleep(s.list)阻塞自己,被置成等待信號量s狀態並移入s信號量隊列,轉向進程調度程序 } void V(semaphore s){ s.value++; if(s.value<=0) wakeup(s.list); //若信號量小於等於0,則調用wakeup(s.list)從信號量s隊列中釋放一個等待信號量s的進程並轉換成就緒態,進程則繼續運行 }
二值信號量雖然僅能取值0、1,但它和其它記錄型信號量有一樣的表達能力
- 機票問題
注意:(1)P操作與V操作在執行路徑上必須一一對應,有一個P操作就有一個V操作;(2)輸出一張票的操作不應放在臨界區內
int A[m]; Semaphore s = 1; cobegin process Pi { int Xi; Li:按旅客定票要求找到A[j]; P(s); Xi = A[j]; If (Xi>=1) { Xi=Xi-1; A[j]=Xi;V(s); 輸出一張票;} else {V(s); 輸出票已售完;} goto Li; } coend;
只有相同航班的票數才是相關的臨界資源,所以用一個信號量s處理全部機票會影響進程並發度
可以讓每一個航班都有自己的臨界區,把信號量改為s[m]
int A[m]; Semaphore s[m]; //每一個航班都有自己的臨界區 For (int j=0;j<m;i++) s[j] = 1; cobegin process Pi { int Xi; L1:按旅客定票要求找到A[j]; P(s[j]); Xi = A[j]; If (Xi>=1) { Xi=Xi-1; A[j]=Xi;V(s[j]); 輸出一張票; } else {V(s[j]); 輸出票已售完;} goto L1; } coend;
- 生產者消費者問題
(1)1生產者1消費者1緩沖區問題
int B; semaphore sput = 1; /* 可以使用的空緩沖區數 */ semaphore sget = 0; /* 緩沖區內可以使用的產品數 */ process producer { L1: produce a product; P(sput); B = product; V(sget); goto L1; } process consumer { L2: P(sget); product = B; V(sput); consume a product; goto L2; }
(2)1生產者1消費者N緩沖區問題
必須引入放入和取出產品的循環隊列指針putptr指針和getptr指針進行管理,因為只有1生產者1消費者所以它們不需要是共享變量
int B[k]; // 共享緩沖區隊列 semaphore sput = N; //可以使用的空緩沖區數 semaphore sget = 0; //緩沖區內可以使用的產品數 int putptr = getptr = 0; process producer { L1:produce a product; P(sput); B[putptr] = product; putptr = (putptr + 1) mod k; V(sget); goto L1; } process consumer { L2:P(sget); product = B[getptr]; getptr = (getptr + 1) mod k; V(sput); consume a product; goto L2; }
⭐️(3)N生產者N消費者N緩沖區問題
必須引入新的信號量s1和s2對putptr指針和getptr指針進行管理
int B[k]; semaphore sput = N; /* 可以使用的空緩沖區數 */ semaphore sget = 0; /* 緩沖區內可以使用的產品數 */ int putptr = getptr = 0; semaphore s1 = s2 = 1; /* 互斥使用putptr、getptr */ process producer_i { L1:produce a product; P(sput); P(s1); B[putptr] = product; putptr = ( putptr + 1 ) mod k; V(s1); V(sget); goto L1; } process consumer_j { L2:P(sget); P(s2); Product = B[getptr]; getptr = ( getptr + 1 ) mod k; V(s2); V(sput); consume a product; goto L2; }
若要求一個消費者取10次,其它消費者才能開始取,則需要再增加一個信號量mutex1並對消費者進程進行改造,循環10次后再V(mutex1)。
這里通過一個互斥信號量mutex2互斥訪問緩沖區,而不是像前面那樣通過兩個互斥信號量s1和s2分別互斥使用緩沖區存指針putptr、取指針getptr
process consumer_j { P(mutex1); for(int i=0; i<10; i++){ P(full); P(mutex2); 互斥訪問緩沖區; V(mutex2); V(empty); } V(mutex1); }
(4)蘋果橘子問題
父親只放評估、母親只放橙子;兒子只吃橙子、女兒只吃蘋果
同步關系1:有蘋果
同步關系2:有橘子
同步關系3:有空位
Semaphore sp; /* 盤子里可以放幾個水果*/ Semaphore sg1; /* 盤子里有桔子*/ Semaphore sg2; /* 盤子里有蘋果*/ sp = 1; /* 盤子里允許放入一個水果*/ sg1 = 0; /* 盤子里沒有桔子*/ sg2 = 0; /* 盤子里沒有蘋果*/ process father { L1: 削一個蘋果; P(sp); 把蘋果放入plate; V(sg2); goto L1; } process mother { L2: 剝一個桔子; P(sp); 把桔子放入plate; V(sg1); goto L2; } process son { L3: P(sg1); 從plate中取桔子; V(sp); 吃桔子; goto L3; } process daughter { L4: P(sg2); 從plate中取蘋果; V(sp); 吃蘋果; goto L4; }
(5)吸煙者問題
三個消費者各有一種材料、缺另外兩種材料,供應者每次隨機供應兩種不同材料
想法:生產者每次供應兩種不同材料,其實可以理解為有三種不同的生產材料提供方式,分別供三種消費者消費
int random; Semaphore offer1 = offer2 = offer3 = 0; Semaphore finish = 1; process producer(){ while(1){ random = 任意隨機整數; random = random % 3; P(finish); 對應兩種材料放在桌子上; if(random==0) V(offer1); else if(random==1) V(offer2); else V(offer3); } } process consumer_1(){ while(1){ P(offer1); 拿自己缺的那兩種材料; 卷成煙抽掉; V(finish); } }
(6)1生產者2消費者N緩沖區,生產者隨機生成正整數,2個消費者分別取奇數和偶數
思路:這個問題和抽煙者問題很類似,關鍵是要定義緩沖區里有奇數的互斥信號量odd、緩沖區里有偶數的互斥信號量even。每次生產者隨機生成一個數之后,判斷奇偶,分別V(odd)和V(even)
- 哲學家就餐問題
Semaphore fork[5]; for (int i = 0; i < 5; i++) fork[i] = 1; cobegin process philsopher_i() { while (true) { think(); P(fork[i]); P(fork[(i+1)%5]); eat(); V(fork[i]); V(fork[(i+1)%5]); } } coend
如果5位哲學家同時拿起他們左手/右手的叉子,將出現死鎖,可以:
(1)至多允許四個哲學家同時拿叉子
(2)奇數號哲學家先取左邊叉子,再取右邊叉子;偶數號哲學家則相反
(3)每位哲學家取到手邊兩把叉子才開始吃,否則一把也不取:可以通過引入互斥信號量mutex每次只允許一個哲學家拿叉子
Semaphore fork[5]; for (int i = 0; i < 5; i++) fork[i] = 1; Semaphore mutex = 1; cobegin process philsopher_i() { while(true){ P(mutex); P(fork[i]); P(fork[(i+1)%5]); V(mutex); eat(); V(fork[i]); V(fork[(i+1)%5]); } } coend
- 寫者問題
一次只允許一個寫者寫,但可以N個讀者同時讀。寫者完成寫操作前不允許其它寫者、讀者操作
引入表示是否允許寫的信號量writeblock,相當於任何進程在工作的時候都不允許寫。不過單純引入信號量不能解決此問題,還必須引入計數器readcount對讀進程進行計數,讀之前檢查計數器,如果為1(自己是唯一的讀進程)才需要P(writeblock),讀之后檢查計數器,如果為0(當前已無讀進程)則需要V(writeblock)。
mutex是用於對計數器readcount操作的互斥信號量
int readcount = 0; Semaphore writeblock = 1 ; Semaphore mutex = 1; cobegin process read_i() { P(mutex); readcount++; if(readcount==1) P(writeblock); //自己是唯一的讀進程,寫者在寫文件時自己不能開始讀,自己開始讀后不允許寫操作 V(mutex); /*讀文件*/ P(mutex); readcount-—; if(readcount==0) V(writeblock); //自己是唯一的讀進程,讀文件完成后允許寫操作 V(mutex); } process write_j() { P(writeblock); /*寫文件*/ V(writeblock); } coend
此寫法讀者優先,當存在讀者時寫者將被延遲。且只要有一個讀者活躍,隨后而來的讀者都將被允許訪問文件,從而導致寫者長時間等待。
改進方法:增加信號量,確保當一個寫進程聲明想寫時,不允許后面的新讀者訪問共享文件。
- 男女共浴問題、汽車過橋問題
這個問題的關鍵是設置一把性別鎖mutex,第一個到的男人要負責搶這把鎖,因此mutex的PV操作必須放在修改mancount的臨界區內。一旦男人搶到了這把鎖,試圖搶這把鎖的女人就會停留在womancount的臨界區內出不來,而每次又只允許一個女人進入womancount的臨界區。
最后一個走的男人要負責把這把鎖釋放掉。
Semaphore mutex; Semaphore mutex_man = mutex_woman= 1; int mancount = womancount =0; Process man(){ P(mutex_man) mancount++; if(mancount==1) P(mutex); V(mutex_man); 洗澡; P(mutex_man); mancount—; if(mancount==0) V(mutex); V(mutex_man) }
南大18年真題過橋問題:汽車只能單向過橋,最多12輛車同時過橋。
- 理發師問題
引入一個計數器waiting記錄等待理發的顧客坐的椅子數,初值為0最大為N。mutex是用於對計數器waiting操作的互斥信號量
引入信號量customers記錄等候理發的顧客數,並用於阻塞理發師進程,初值為0
引入信號量barbers記錄正在等候顧客的理發師數,並用於阻塞顧客進程,初值為0
想法:其實和N生產者1消費者N緩沖區的生產者消費者問題有些類似,但是多了P(barbers)和V(barbers);此外,椅子數改用了int數waiting再用信號量mutex進行互斥。
int waiting = 0; Semaphore customers = 0; Semaphore barbers = 0; Semaphore mutex = 1; cobegin process barbers() { while(true) { P(customers);//判斷是否有顧客,沒有的話理發師睡眠 P(mutex); waiting--; V(barbers);//理發師准備為顧客理發 V(mutex); cuthair(); //理發師理發,不應放在臨界區 } } process customer_i() { P(mutex); if(waiting<N) { waiting++; V(customers);//喚醒理發師 V(mutex); P(barbers);//如果理發師忙則等待 get_haircut(); } else V(mutex);//人滿了,顧客離開 } coend