1、信號量與P/V操作
- 信號量的構成
1 Struct semaphore { 2 int value; //信號量值 3 pointer_PCB queue; //信號量隊列指針 4 }
-
- 信號量是聯系和控制CR(需求的資源)的數據結構。
- 當信號量的值>0時,s.value表示CR的可用數目
- 當信號量的值=0時,s.value表示無空閑CR
- 當信號量的值<0時,|s.value|表示因CR而阻塞的進程數
- P(Proberen)操作
1 P(s) { 2 s.value = s.value - 1; // s.value減1 3 if (s.value < 0) // 該進程被阻塞,進入相應隊列,然后轉進程調度 4 { 5 該進程狀態置為等待狀態; 6 將該進程加入相應的等待隊列s.queue的末尾; 7 } 8 //若s.value減1后仍大於或等於0,則進程繼續執行 9 }
- V(Verhogen)操作
1 V(s) { 2 s.value = s.value + 1; // s.value加1 3 if (s.value <= 0) // 從隊列中喚醒一等待進程,然后繼續執行或轉進程調度 4 { 5 喚醒相應等待隊列s.queue中等待的一個進程; 6 將其狀態修改為就緒態,並將其插入就緒隊列; 7 } 8 //若相加結果大於0,進程繼續執行 9 }
- 利用信號量和P/V操作實現進程互斥的一般模型:
- 進程P1 進程P2
- ...... ......
- P(S); P(S);
- 臨界區; 臨界區;
- V(S); V(S);
- ...... ......
- 其中信號量S用於互斥,初值為1,當P1進程進入臨界區后S的值就為0,P2執行P(S)后進入阻塞隊列從而實現了對臨界區的互斥訪問。
- 使用P/V操作實現進程互斥的注意點
- 每個程序中用戶實現互斥的P、V操作必須成對出現,先做P操作,進臨界區,后做V操作,出臨界區。若有多個分支,要認真檢查其成對性;
- P、V操作應分別緊靠臨界區的頭尾部,臨界區的代碼應盡可能短,不能有死循環;
- 互斥信號量的初值一般為1。
- 利用信號量和P/V操作實現進程同步的一般模型:
- S:=0
- 進程P1 進程P2
- V(S); P(S);
- ...... ......
- s初值為0,就意味着只能先做v操作,就是先執行p1,然后才能執行p2里的p操作,所以實現了p1 和p2的先后次序同步關系
- 使用P/V操作實現進程同步的注意點
- 分析進程間的制約關系,確定信號量種類。在保持進程間有正確的同步關系情況下,哪個進程先執行,哪些進程后執行,彼此間通過什么資源(信號量)進行協調,從而明確要設置哪些信號量;
- 信號量的初值與相應資源的數量有關,也與P、V操作在程序代碼中出現的位置有關;
- 同一信號量的P、V操作要成對出現,但它們分別在不同的進程代碼中。
- 生產者消費者經典問題分析
- 一個生產者,一個消費者,公用一個緩沖區
- 首先兩進程互斥進入緩沖區,然后是需要同步,先生產再消費
- 但也需要考慮存在的另一種同步,即當緩沖區滿的時候,是先消費再生產
- 一個生產者,一個消費者,公用n個環形緩沖區
- 同步和互斥關系同上
- 與第一種情況的區別在於需要設緩沖區的編號為1~n-1,定義兩個指針in和out,分別是生產者進程和消費者進程使用的指向下一個可用的緩沖區
- 一組生產者,一組消費者,公用n個環形緩沖區
- 互斥關系:消費者和消費者、消費者和生產者、生產者和生產者。
- 同步關系:緩沖區為空,先生產再消費;緩沖區為滿,先消費再生產。
- 每一對同步關系設置一個同步信號量,題目中存在兩對同步關系,就設置兩個同步信號量 empty和full。需要注意同步信號量初值不一定為0,只要根據同步關系保證先后執行次序就可以了。
- 對同一資源使用的一組進程可以設置一個互斥信號量,互斥信號量初值一般為1
- 定義四個信號量:
- empty——表示緩沖區是否為空,初值為n
- full——表示緩沖區中是否為滿,初值為0
- mutex1——生產者之間的互斥信號量,初值為1
- mutex2——消費者之間的互斥信號量,初值為1
- 為了同步生產者和消費者的行為,需要記錄緩沖區中物品的數量。數量可以使用信號量來進行統計,這里需要使用兩個信號量:empty 記錄空緩沖區的數量,full 記錄滿緩沖區的數量。其中,empty 信號量是在生產者進程中使用,當 empty 不為 0 時,生產者才可以放入物品;full 信號量是在消費者進程中使用,當 full 信號量不為 0 時,消費者才可以取走物品。
- 由於需要設緩沖區的編號為1~n-1,所以定義兩個指針in和out,分別是生產者進程和消費者進程使用的指向下一個可用的緩沖區。
- 生產者進程:
- 一個生產者,一個消費者,公用一個緩沖區
1 while(TRUE){ 2 生產一個產品; 3 P(empty); 4 P(mutex1); 5 產品送往buffer(in); 6 in=(in+1)mod n; 7 V(mutex1); 8 V(full); 9 }
-
-
- 消費者進程:
-
1 while(TRUE){ 2 P(full) 3 P(mutex2); 4 從buffer(out)中取出產品; 5 out=(out+1)mod n; 6 V(mutex2); 7 V(empty); 8 消費該產品; 9 }
-
-
- 需要注意,不能先對緩沖區進行加鎖,再測試信號量。也就是說,不能先執行 p(mutex) 再執行 p(empty)。如果這么做了,那么可能會出現這種情況:生產者對緩沖區加鎖后,執行 p(empty) 操作,發現 empty = 0,也就是沒有空的緩沖期可以放產品,此時生產者只能阻塞;但因為mutex的值已經為0,所以消費者不能進入臨界區,消費者就永遠無法執行 p(empty) 操作,empty 永遠都為 0,導致生產者永遠等待下,不會釋放鎖,消費者因此也會永遠等待下去。但兩個v操作順序任意,不影響。
- 3個進程PA,PB和PC合作解決⽂件打印問題
- 問題描述
- PA將⽂件記錄從磁盤讀⼊主存的緩沖區1,每執⾏⼀次讀⼀個記錄;
- PB將緩沖區1的內容復制到緩沖區2,每執⾏⼀次復制⼀個記錄;
- PC將緩沖區2的內容打印出來,每執⾏⼀次打印⼀個記錄。緩沖區的⼤⼩等於⼀個記錄⼤⼩。 請⽤P,V操作來保證⽂件的正確打印。
- 問題描述
-

-
-
- 這是典型的雙緩沖區問題,兩個緩沖區⼤⼩均為1,其中PA為⽣產者,PB既是⽣產者又是消費者,PC為消費者, 故按照⽣產消費者問題的思路解決該題即可。但是需要注意的問題是:如果PA將數據放⼊緩沖區之后,PB沒有及時取的話,如果此時PA進程繼續執⾏,那么會將之前的數據覆蓋掉,緩沖區2也⼀樣,因此這⾥還需要設置兩個 信號量以限制緩沖區1和緩沖區2中記錄的數量。
- 設置信號量
- full1,full2分別表⽰緩沖區1及緩沖區2是否有記錄可供處理,初值均為0;
- empty1,empty2分別表⽰緩沖區1及緩沖區2是否還有空位可放記錄,初值均為1;
- mutex1,mutex2分別表⽰緩沖區1及緩沖區2的訪問控制,初值均為1。
-
1 semaphore full1 = 0; 2 semaphore full2 = 0; 3 semaphore empty1 = 1; 4 semaphore empty2 = 1; 5 semaphore mutex1 = 1; 6 semaphore mutex2 = 1; 7 8 PA() { 9 while (1) { 10 從磁盤讀⼀一個記錄; 11 P(empty1); 12 P(mutex1); 13 將記錄存⼊入緩沖區1; 14 V(mutex1); 15 V(full1); 16 } 17 } 18 19 PB() { 20 while(1) { 21 P(full1); 22 P(mutex1); 23 從緩沖區1中讀出⼀一個⽂文件記錄; 24 V(mutex1); 25 V(empty1); 26 P(empty2); 27 P(mutex2); 28 將⼀一個記錄讀⼊入緩沖區2; 29 V(mutex2); 30 V(full2); 31 } 32 } 33 34 PC() { 35 while (1) { 36 P(full2); 37 P(mutex2); 38 從緩沖區2取⼀一個記錄; 39 V(mutex2); 40 V(empty2); 41 打印記錄; 42 } 43 }
-
- 讀者寫者問題
- 問題描述
- 允許多個讀者同時對文件進行讀操作
- 只允許一個寫者對文件進行寫操作
- 任何寫者在完成寫操作前不允許其他讀者或寫者工作
- 寫者在執行寫操作前,應讓已有的寫者和讀者全部退出
- 分析關系:讀與讀不互斥,讀與寫互斥,寫與寫互斥
- 讀者分類:
- 第一個進入的讀者:打開寫屏蔽,讀入
- 中間進入的讀者:讀入
- 最后一個離開的讀者:關閉寫屏蔽,離開
- 為了區分三類讀者就必須引入計數器rc對讀進程計數,即做加1或減1操作。但大家都知道加1和減1操作在機器內部是三條語句完成的,為了不引起計數器的混亂,就必須對加減定義成原子操作,即需加1或減1一次性完成,mutex就是用於對計數器rc操作的互斥信號量,w則表示是否允許寫的信號量,也就是寫屏蔽開關
- 問題描述
- 讀者寫者問題
1 void reader(void) 2 { 3 while(TRUE) 4 { 5 P(mutex); 6 rc = rc + 1; 7 if(rc == 1) /*第一個讀者*/ 8 { 9 P(W); /*不需要每給讀者都做*/ 10 } 11 V(mutex); 12 讀操作; 13 P(mutex); 14 rc = rc - 1; 15 if (rc == 0)/*最后一個讀者*/ 16 { 17 V(w); 18 } 19 V(mutex); 20 其他操作; 21 } 22 } 23 24 void writer(void) 25 { 26 while(TRUE) 27 { 28 ...... 29 P(W); 30 寫操作; 31 V(w); 32 ...... 33 } 34 }
2、進程通信概念和類型
- 基本概念
- 進程間通信IPC是指進程在系統里同時運行,並相互傳遞、交換信息
- 通過進程通信能夠實現數據傳輸、共享數據、通知事件、資源共享、進程控制
- 進程通過與內核及其他進程之間的互相通信來協調它們的行為
- 通信類型
- 低級通信
- 高級通信
- 通信方式
- 數據格式
- 字節格式:接收方不保留各次發送之間的分界
- 報文格式:接收方保留歌詞發送之間的分界;分為定長報文/不定長報文和可靠報文/不可靠報文
- 同步方式
- 阻塞操作:指操作方要等待操作結束
- 不阻塞操作:指操作提交后立即返回
- 數據格式
3、低級通信中的信號通信
- 機制原理
- 每個信號都要對應正整數常量,即信號編碼;
- 進程之間傳送事先約定的信息的類型,用於通知進程發生了某異常事件;
- 進程通過信號機制來檢查是否有信號。若有,中斷正在執行的程序,轉向的對應處理程序;結束后返回到斷點繼續執行,這是一種軟中斷。
- 信號收發
- 發送信號,發送信號的程序用系統調用kill( )實現;
- 預置信號處理,接收信號的程序用signal( )來實現對處理方式的調用;
- 接收信號的進程按事先規定完成對事件的處理。
- kill( )
- int kill(pid, sig)
- pid是進程的標識符,參數sig是要發送的軟中斷信號
- pid>0時,信號發送給進程pid
- pid=0時,信號發送給與發送進程同組的所有進程
- pid=-1時,信號發送給所有用戶標識符真正等於發送進程的有效用戶標識號的進程
- signal( )
- signal(sig, function)
- 頭文件 #include<signal.h>
- 參數定義 int sig; void (*func)( );
- function的解釋如下
- function=1時,對信號不予理睬,屏蔽該類信號
- function=0時,進程在收到sig信號后終止自己
- function≠0且≠1時,值作為信號處理程序的指針
- 代碼實例
1 #include<stdio.h> 2 #include<signal.h> 3 #include<unistd.h> 4 5 int wait_mark; 6 void waiting() { 7 while (wait_mark != 0); 8 } 9 void stop() { 10 wait_mark = 0; 11 } 12 void main() { 13 int p1, p2; 14 signal(SIGINT, SIG_IGN); //防止control-C鍵盤中斷 15 while ((p1 = fork()) == -1); //創建子進程p1 16 if (p1 > 0) { 17 while ((p2 = fork()) == -1); //創建子進程p2 18 if (p2 > 0) { 19 wait_mark = 1; 20 signal(SIGINT, stop); //接收到^c信號,轉stop 21 waiting(); 22 kill(p1, 16); //向p1發軟中斷信號16 23 kill(p2, 17); //向p2發軟中斷信號17 24 wait(0); //同步 25 wait(0); 26 printf("Parent process is killed!\n"); 27 exit(0); 28 } 29 else { 30 wait_mark = 1; 31 signal(17, stop); //接收到軟中斷信號17,轉stop 32 waiting(); 33 printf("Child process 2 is killed by parent!\n"); 34 exit(0); 35 } 36 } 37 else { 38 wait_mark = 1; 39 signal(16, stop); //接收到軟中斷信號17,轉stop 40 waiting(); 41 printf("Child process 1 is killed by parent!\n"); 42 exit(0); 43 } 44 }
4、高級通信中的共享存儲
- 機制原理
- 共享存儲區是系統中通信速度最高的一種通信機制
- 進程通過對共享存儲區中數據的讀、寫來進行通信

- 函數調用
- tshmget( )
- 創建、獲得一個共享存儲區
- 系統調用格式:shmid = shmget(key, size, flag);
- key是共享存儲區的名字
- size是其大小
- flag是用戶設置的標志
- shmat( )
- 共享存儲區附接,將共享存儲區附接進程虛擬地址空間
- 系統調用格式:virtaddr = shmat(shmid, addr, flag);
- shmid是共享存儲區的標識符
- addr是用戶給定,將共享存儲區附接到進程的虛地址空間
- flag規定讀、寫權限,值為0時,表示可讀、可寫
- 返回值是共享存儲區所附接到的進程虛擬地址virtaddr
- shmdt( )
- 把共享存儲區從進程虛地址空間斷開
- 系統調用格式:shmdt(addr);
- addr是要斷開連接的虛地址,即shmat( )所返回的虛地址
- 調用成功,返回0值,調用不成功,返回-1
- shmctl( )
- 共享存儲區的控制,對其狀態進行讀取和修改
- 系統調用格式:shmctl(shmid,cmd,buf);
- buf是用戶緩沖區地址
- cmd是操作命令:用於查詢共享存儲區的情況,如長度、連接進程數、共享區的創建者標識符等;用於設置或改變共享存儲區的屬性,如共享存儲器的許可權、連接進程計數等;共享存儲區的加鎖和解鎖,刪除共享存儲區標識符等。
- tshmget( )
- 實例
- 進程利用fork( )創建兩個子進程server和client進行通信;
- client端建立或打開 一個key為75的共享區,client端填入9到0,client每發送一次數據后顯示“(client)sent”;
- server端建立或打開一個key為75的共享區,等待其他進程發來的消息,server每接收到一次數據后顯示“(server)received”
1 #include<sys/types.h> 2 #include<sys/shm.h> 3 #include<sys/ipc.h> 4 #define SHMKEY 75 5 int shmid, i; 6 int *addr; 7 void server() { 8 int x; 9 shmid = shmget(SHMKEY, 1024, 0777|IPC_CREAT); //創建共享存儲區 10 addr = shmat(shmid, 0, 0); //獲得首地址 11 do { 12 *addr = -1; 13 while (*addr == -1); 14 x = *addr; 15 printf("(server)received"); 16 } while(*addr); 17 shmctl(shmid, IPC_RMID, 0); //撤銷共享存儲區,歸還資源 18 exit(0); 19 } 20 void client() { 21 int i; 22 shmid = shmget(SHMKEY, 1024, 0777|IPC_CREAT); //打開共享存儲區 23 addr = shmat(shmid, 0, 0); //獲得共享存儲區首地址 24 for (i = 9; i >= 0; i--) { 25 while(*addr != -1); 26 printf("(client)sent"); 27 *addr = i; 28 } 29 exit(0); 30 } 31 void main() { 32 while ((i = fork()) == -1); 33 if (!i) server(); 34 system("ipcs -m"); 35 while ((i = fork()) == -1); 36 if (!i) client(); 37 wait(0); 38 wait(0); 39 }
5、高級通信中的消息通信
- 機制原理
- 消息是一個格式化的可變長信息單元
- 消息通信機制允許由進程給其他進程發送消息
- 進程收到多個消息時,可排成消息隊列
- 消息隊列有消息隊列描述符方便用戶和系統訪問
- 函數調用
- msgget( ):創建一個消息,獲得消息的描述符
- msgsnd( ):向指定的消息隊列發送一個消息,並將該消息鏈接到該消息隊列的尾部
- msgrcv( ):從指定的消息隊列中接收消息
- msgctl( ):讀取消息隊列的狀態並進行修改,如查詢消息隊列描述符、修改許可權及刪除該隊列
6、高級通信中的管道通信
- 機制原理
- 管道是連接寫進程和讀進程的、並允許以生產者消費者方式進行通信的共享文件,稱為pipe文件
- 由寫進程從管道的寫入端將數據寫入管道,而讀進程則從管道的讀出端讀出數據
- 管道類型
- 有名管道:在文件系統中長期存在的、具有路徑名的文件,用系統調用mknod( )建立,其他進程可以利用路徑名來訪問該文件,與訪問其他文件相似
- 無名管道:利用pipe( )建立起來臨時的無名文件,用該系統調用返回的文件描述符來標識該文件,只有調用pipe( ) 的進程及其子孫進程才能識別此文件描述符並利用該管道進行通信
