一、進程間通信
進程間通信的本質
它的本質就是讓不同的進程看到一份公共的資源(內存的一段內存區域),該資源只能由第三方提供,即操作系統直接或者間接提供。
進程間通信的目的
- 數據傳輸:一個進程需要將它的數據發送給另一個進程
- 資源共享:多個進程之間共享同樣的資源。
- 通知事件:一個進程需要向另一個或一組進程發送消息,通知它(它們)發生了某種事件(如進程終止時要通知父進程)。
- 進程控制:有些進程希望完全控制另一個進程的執行(如Debug進程),此時控制進程希望能夠攔截另一個進程的所有陷入和異常,並能夠及時知道它的狀態改變。
二、管道
1.什麼是管道
我們把從一個進程連接到另一個進程的一個數據流稱爲一個“管道”。
2.管道的兩種類型
- 匿名管道pipe
- 命名管道FIFO
三、匿名管道
#include <unistd.h>
功能:創建無名管道
int pipe(int fd[2]);
參數:
fd:文件描述符數組,其中fd[0]表示讀端, fd[1]表示寫端
返回值:成功返回0,失敗返回錯誤代碼
1.用fork來共享管道原理
父進程創建一個管道,然後fork出子進程,接下來關閉管道兩頭各自不用的讀端或者寫端。如圖所示:
關掉不用的描述符後,一個保留寫端,一個保留讀端,這樣就實現了父進程與子進程之間的通信。
2.站在文件描述符角度-深度理解管道
Linux下一切皆文件,所以管道也是文件。父進程調用pipe創建管道時,其實就是創建了一個文件。此時文件描述符表除了默認的三個標準輸入、標準輸出、標準錯誤之外,又多了兩個文件描述符。它們是管道的讀端fd[0]和寫端fd[1]。(讀端對應0,像一個張開的嘴巴,寫端對應1,像一支筆。這樣就不會搞混了)
父進程fork出子進程,子進程與父進程共享代碼,數據各自私有一份。所以父進程打開的文件子進程同樣看得到。這樣一來,二者的文件描述符表中也都有了讀端fd[0]和寫端fd[1],也保證了父子進程看到了一份公共的資源---->管道。
接下來我們可以根據自己的需要關閉相應的描述符就可以了。假如此時我們讓子進程來讀,父進程去寫,那麼我們關閉父進程的fd[0],關閉子進程的fd[1],這樣就可以實現父進程和子進程之間的通信了。
3.匿名管道的五個特點.
- 只允許單向通信(父-->子或者子-->父),若想雙向通信,可以利用兩個管道。
- 只能用於具有血緣關係的進程間通信,常用於父子進程。
- 管道的生命週期隨通信雙方的進程。打開的文件在進程結束時自動關閉,管道也是文件,生命週期隨進程。
- 自帶同步與互斥機制
- 面向字節流,提供流式服務
這裏還需要引入五個概念:
1>臨界資源:多個進程看到的一份公共資源
2>臨界區:訪問臨界資源的那部分區域
3>互斥:在任何一個時刻,只允許有一個進程進入臨界資源進行資源訪問,在其訪問期間,其他進程不得進入訪問。
4>同步:在保證安全的條件前提,進程按照特定的順序訪問臨界資源。
5>原子性:一件事情,要麼做了,要麼沒做,不會有第三態。
4.匿名管道的四種情況
前提條件:父子進程進行通信,父進程讀,子進程寫。
① 父進程的讀端不讀,但是也不關,那麼子進程的寫端就會一直寫,子進程一直寫一直寫,最後就會把管道寫滿。此時如果再繼續寫,就會把以前的數據覆蓋掉,而這些數據可能還沒有讀取。所以基於安全考慮,write調用阻塞,子進程會進行阻塞式等待。其實阻塞式等待就是操作系統不會調度子進程,把子進程的R狀態變爲非R狀態,然後把子進程的PCB放置特定的等待隊列中進行等待,等到讀端開始讀了,操作系統再把子進程喚醒,即把子進程的狀態從非R變爲R,再將子進程的PCB從等待隊列拿到運行隊列中。所以當讀端不讀並且不關的時候,寫端寫滿的時候,寫端進程就會阻塞式等待。
② 子進程的寫端不寫,但是也不關,此時父進程的讀端如果檢測到管道里還有有效數據,就會繼續讀。但是因爲寫端沒有關,所以當有效數據被讀完的時候,寫端隨時有可能來寫,那麼讀端就會等寫端進行有效數據的寫入,也就意味着讀端會阻塞式等待。所以當寫端不寫並且不關的時候,讀端讀完的時候,讀端進程就會阻塞式等待。
③ 子進程的寫端寫完了不再寫入,然後把自己的寫端關閉了。那麼如果管道里有數據,讀端就會將數據讀出來。因爲寫端已經關閉了,那麼已經不會再有數據寫入了,所以讀端讀完之後繼續等就沒有任何意義了,而操作系統絕對不會做浪費資源的事情,所以此時操作系統就會把讀端返回。
④ 父進程的讀端關閉了,那麼寫端寫也就沒有任何意義了,同上,操作系統絕對不會做浪費資源的事情,所以操作系統就會給寫端發送13號SIGPIPE信號然後立即kill掉寫端。
5.匿名管道的代碼實現
輸出的結果爲:
四、命名管道
命名管道解決了匿名管道的一個最大的問題就是可以實現兩個毫不相干的進程間的通信,命名管道是一種特殊類型的文件,我們可以通過命令行來創建一個命名管道,也可以使用mkfifo系統調用函數來創建命名管道,創建出來的是一個管道文件。讓一個進程往管道文件裏寫入數據,另一個進程從該管道文件裏讀取數據,即可完成兩個毫無關係的進程之間的通信。
1.創建命名管道
- 從命令行創建:
$ mkfifo filename
- 從程序裏創建:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo (const char *filename,mode_t mode);
創建命名管道:
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc , char *argc[])
{
mkfifo("p2",0644);
return 0;
}
2.server&client代碼實例
server.c(讀端)
1 #include <stdio.h>
2 #include <fcntl.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 int main()
6 {
7 if(mkfifo("./fifo",0644)<0){//調用系統調用接口創建命名管道,若創建失敗,輸出信息!
8 printf("mkfifo error!\n");
9 return 1;
10 }
11 int fd = open("./fifo",O_RDONLY);//用只讀權限打開這個管道文件
12 if(fd<0){ //打開失敗
13 perror("read");
14 return 2;
15 }
16
17 char buf[1024];
18 while(1){
19 ssize_t s = read(fd,buf,sizeof(buf)-1);//讀文件
20 if(s>0){
21 buf[s]=0;//讀取成功
22 printf("client# %s\n",buf);//輸出
23 }else if(s==0){
24 printf("client quit,server quit too!\n");//讀取結束
25 break;
26 }else{//讀取出錯
27 }
28 }
29 close(fd);//使用結束,關閉文件
30 return 0;
31 }
~
client.c(寫端)
1 #include <stdio.h>
2 #include <string.h>
3 #include <fcntl.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 int main()
7 {
8 int fd = open("./fifo",O_WRONLY);//打開這個管道文件
9 if(fd<0){
10 perror("read");
11 return 2;
12 }
13
14 char buf[1024];
15 while(1){
16 printf("Please Enter:");//從標準輸入中寫內容
17 scanf("%s",buf);
18 write(fd,buf,strlen(buf));//寫文件
19 }
20 close(fd);//使用完畢,關閉文件
21 return 0;
22 }
輸出結果:我們可以看到,讀端發出的消息,寫端全部收到了,寫端寫什麼,讀端就讀什麼!!!
並且當我的讀端關閉不在讀數據時,寫端同時退出也有信息:
五、匿名管道和命名管道的區別
- 匿名管道由pipe函數創建並打開。
- 命名管道由mkfifo函數創建,打開用open
- 匿名管道適合具有血緣關係的進程間通信,常用於父子進程
- 命名管道可以用於兩個毫無關係的進程之間