一、進程間通訊
系統中的進程都是獨立的個體,擁有屬於自己的用戶空間,所以每個進程之間的數據是不共享的
。在前面學習fork時,瞭解到父、子進程可以共享fork之前打開的文件描述符。那麼現在我們思考一個問題:父進程現在想給子進程發送一個“hello world"字符串。可以採取哪種方式,我想大多數人會想到 藉助文件傳輸這種辦法:
- 父、子進程指向同一個文件,先讓父進程對文件進行write操作,子進程等待,讓它sleep睡眠,保證父進程先運行進行數據的寫入;
- 父進程寫完“hello world"後,文件偏移量會偏移到d的位置,故子進程在讀取時需要先把文件偏移量移動到h纔可以保證讀到數據。
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<fcntl.h>
int main()
{
int fd=open("a.txt",O_RDWR);//一定要讀寫打開
assert(fd!=-1);
char buff[128]={0};
pid_t pid=fork();
if(pid==0)
{
sleep(1);
lseek(fd,0,SEEK_SET);
int n=read(fd,buff,20);
printf("buff:%s\n",buff);
}
else
{
int n= write(fd,"Hello,world!",20);
}
close(fd);
exit(0);
}
可以看到這中方法存在以下問題:
無法準確保證子進程在父進程後面運行:
子進程又不知道父進程的處理需要多少秒,所以無法確定睡眠的秒數,容易導致混論。速度效率極低:
每次對文件進行操作,就需要和磁盤進行交互,寫入時進行一次I/O操作,讀出時進行一次I/O操作,只是一個讀取過程就需要耗費大量的時間,所以效率低下。傳送信息對象固定:
只能在父子進程之間進行數據的傳遞,沒有辦法做到任意兩個文件的信息傳遞。
爲了解決上述的問題,達到進程間任一兩個進程進行數據交互通訊的目的,科學家們研究出了進程間通訊(IPC)的幾種方式:有名管道,無名管道,消息隊列,信號量,共享內存,socket套接字。
我們會一一進行講解,socket套接字一般用於網絡上,支持兩個不同主機上的進程間通訊。和其他五個不太一樣,故不和它們進行對比。那其他五個的進程通訊方式比較如下:
進程通訊方式 | 內容 | 效率 | 共享實質 | 進程數量 | 阻塞機制 | 特點 | 使用場景 |
---|---|---|---|---|---|---|---|
有名管道 | 數據 | 低 | 磁盤上文件標識符 | 任意兩個進程 | open,read,write | 1、半雙工通信方式 2、在磁盤上會存在一個管道文件標識,但管道文件並不佔用磁盤block空間,數據交互在內存上 | 應用於同一臺主機上的有權限訪問磁盤的任意幾個進程間通訊 |
無名管道 | 數據 | 低 | 父子進程共享文件描述符 | 父子兩個 | read,write | 1、半雙工通信方式 2、藉助父子進程之間共享fork之前打開的文件描述符 3、字節流服務 | 只能用於父子進程之間 |
消息隊列 | 消息 | 低 | 內核對象 | n | msgrcv(獲取數據) | 1、可以實現信息的隨機查詢,可以按消息的類型讀取 2、生命週期隨內核 3、無優先級時採取FIFO | 發送帶有類型的數據,可以真正實現多進程間通訊 |
信號量 | 同步 | - | 內核對象 | n | P操作 | 1、主要用於進程間同步 2、基於操作系統的PV操作 | 負責進程之間的互斥、同步等功能,和其他進程間通訊方式搭配來實現進程間同步通信 |
共享內存 | 數據 | 高 | 內核對象 | n | P操作 | 1、最快的IPC 2、需要進行同步控制 | 使得多個進程訪問同一塊內存空間 ,通信機制運行效率高 |
進程間通訊必須要有一塊共享的內存,這樣纔可以實現進程間數據的交互和通訊。
什麼場景下需要進程間通訊呢?
- 數據傳輸:一個進程需要將它的數據發送給另一個進程,發送的數據量在一個字節到幾兆字節之間。
- 共享數據:多個進程想要操作共享數據,一個進程對共享數據的修改,別的進程應該立刻看到。
- 通知事件:一個進程需要向另一個或一組進程發送消息,通知它(它們)發生了某種事件(如進程終止時要通知父進程)。
- 資源共享:多個進程之間共享同樣的資源。爲了作到這一點,需要內核提供鎖和同步機制。
- 進程控制:有些進程希望完全控制另一個進程的執行(如Debug進程),此時控制進程希望能夠攔截另一個進程的所有陷入和異常,並能夠及時知道它的狀態改變。
二、管道
每個進程的空間地址是獨立的,因此進程與進程之間是不能相互訪問的,要進行進程間通訊,必須通過內核,內核會開闢一段特殊的內存空間
,進程可以在這塊內存空間進行數據的交換。
管道是一個重要的通信機制,思想是:在內存中創建一個共享文件,從而使通信雙方利用這個共享文件來傳遞信息,由於這種方式具有單向傳遞數據的特點,所以稱爲管道,即在某一時刻只能一端讀數據一端寫數據。
根據使用方式和通信對象將它分爲無名管道,有名管道
兩類:
1. 有名管道
:
- 含義:在磁盤有一個管道文件標識,這個管道文件會佔據一個inode結點,但是不會佔用block塊,數據在傳遞過程中會緩存到內核開闢的內存上。
- 作用:磁盤上的空間,只要進程有執行權限都可以訪問,這塊區域在有權限的進程之間是共享的,這個就保證了任何進程都可以訪問到這塊磁盤,這就是利用管道進行進程間通訊的共享內存空間。故管道文件僅僅是爲了使得不同進程(對磁盤有權限)能共享。
2. 無名管道
:
沒有管道文件的創建,藉助父子進程共享fork之前打開的文件描述符,共同指向內核開闢的內存空間,來實現進程間通訊。
3. 管道的特點
:
- 無論有名無名,寫入管道的數據都在內存中。
- 管道的生命週期隨着進程結束而結束。
- 管道是一種半雙工的通信方式,即某一時刻只能A-B,B-A。
- 有名管道可以在任意進程間使用,而無名主要在父子進程間。
二、有名管道FIFO
有名管道需要創建管道文件,創建成功後可以使用系統調用open,read等函數對文件進行操作。
之所以叫FIFO,因爲管道本質是一個先進先出的隊列數據結構,最早放入的數據最先被讀出來
(一)基本概念
我們可以使用命令創建管道文件:
mkfifo filename
也可以使用庫函數創建管道文件:
# include<sys/stat.h>
int mkfifo(const char* pathname,mode_t mode);//文件名,指定創建文件的權限
返回值:若成功返回0,失敗返回-1
一旦用mkfifo創建一個FIFO管道文件,就可以用open打開它,用read,write等可以對文件進行操作。
我們來看一下有名管道是如何實現進程間的通信,假設現在又A,B兩個進程,A進程向管道文件寫數據,B進程從管道文件讀數據,那麼就有下面這張圖:
我們需要清楚這幾個點:
- 在磁盤上會有一塊FIFO文件標識符,佔據inode區域,inode結點會指向內核開闢的內存上的一片空間,兩個進程對於磁盤上的文件是共享的。
進程之間通訊的所有數據都會在內存交互,而不會存儲到FIFO文件,因爲它只是一個標識,所以進程通訊結束後,可以通過ls -a查看文件詳細信息,文件大小爲0
。 - A,B兩個進程通過文件inode區域可以訪問到這一塊內存區域,都指向內存上這一塊空間的起始位置,就可以開始進行半雙工通信了。
- A,B進程只能一個寫打開,一個讀打開。
不能以讀寫的方式打開,那樣就會造成混亂,A寫給B的,A自己讀了,所以管道是一個半雙工的通信機制,某一時刻只能A寫B讀(A-B);B寫A讀(B-A)。
- 內核對管道這塊的內存空間的管理是以循環方式管理,即寫到末尾時會從頭開始寫,覆蓋原來的數據;在讀取數據時會將讀過的數據刪除,當讀到末尾時,會從頭開始讀。故
這一塊區域是循環的使用的,類似循環鏈表。
也正是因爲這種工作方式:只有當有進程往管道中寫數據,同時有進程從管道讀數據這種情況下,管道纔會有意義,如果一個進程是以只寫或只讀方式操作管道文件,那就沒有意義,會阻塞,浪費空間。
(二)實例
使用一下管道進行兩個進程間的通訊:有A.c,B.c兩個進程:
- A進程負責打開管道,循環獲取用戶從鍵盤上輸入的信息,存儲到buff,再將數據寫入管道中,最後關閉管道。
- B進程負責打開管道,循環從管道中讀取數據,將數據輸出到屏幕上。
那麼我們代碼如下:
A.c
# include<stdio.h>
# include<stdlib.h>
# include<assert.h>
# include<unistd.h>
# include<sys/stat.h>
# include<fcntl.h>
# include<string.h>
int main()
{
int fd=open("./FIFO",O_WRONLY);
assert(fd!=-1);
printf("fifo1 write succes!\n");
while(1)
{
char buff[128]={0};
printf("input:");
fgets(buff,127,stdin);
if(strncmp(buff,"end",3)==0)
{
break;
}
int n=write(fd,buff,strlen(buff)-1);
}
close(fd);
}
B.c
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<sys/stat.h>
# include<assert.h>
# include<fcntl.h>
int main()
{
int fd=open("./FIFO",O_RDONLY);
assert(fd!=-1);
printf("fifo2 Read success!\n");
while(1)
{
char buff[128]={0};
int n=read(fd,buff,127);
if(n<=0)
{
break;
}
printf("read:%s\n",buff);
}
close(fd);
}
我們對代碼進行下面幾種方法的測試,分析出現的情況:
- 先運行A進程,B進程不運行出現的情況,出現的結果:
可以看到A進程阻塞,不能輸出A進程的提示信息,這就表示open函數阻塞,
因爲我們是以只寫的方式打開管道,B進程不運行表示沒有進程來讀取管道數據,管道只寫不讀,就沒有意義,所以會一直阻塞。
- 先運行B進程,A進程不運行,會出現的情況:
和(1)的情況是一樣的,會阻塞,直到A進程運行才解除阻塞。
通過這兩個測試,我們直到任何一個進程先運行都不能正常運行,會出現阻塞,但我們要搞清楚一點,阻塞並不是說open函數會阻塞,而是操作的對象會阻塞,因爲操作的是管道文件,只讀/只寫會導致無意義,阻塞,如果換成普通文件就不會阻塞。
3.同時運行兩個進程,出現的情況:
可以看到兩個進程運行,一個對管道寫,一個從管道讀,可以立即輸出提示信息,open不會阻塞,輸入一個數據,讀出一個數據,不會存在B進程read空讀的問題,它會一直阻塞着,直到A進程往管道里面寫入一個數據,它纔讀一個數據,而A進程的write操作,也不會一直的讓你寫,而是你讀一個我寫一個。所以read,write會阻塞,這樣就解決了讀寫混亂的問題。
close關閉管道文件不會阻塞。
我們可以看到一下管道文件的詳細信息:
可以看到文件大小爲0。
表明數據的交互都是在內存中,它只是一個標識符。
(三)注意
我們可以總結出有名管道需要注意的點:
- 以open方式打開管道文件會阻塞,直到有進程以另一種方式打開管道文件(讀或寫)。
- 如果管道對應的內存空間沒有數據,則read會阻塞,直到內存中有數據或寫端關閉返回0。
- 如果管道對應的內存空間已滿,則write就會阻塞,直到內存中有空間或讀端關閉返回0.
瞭解一個命令:
ulimit -a可以顯示當前的各種用戶進程限制,包括塊大小,創建進程數等。
三、無名管道
無名管道的使用存在限制,它只能用於父,子進程之間,但是它卻最常用,原因很簡單:現在的項目都是由父進程創建子進程,替換爲新代碼來實現不同的操作。這樣可以只創建一個進程,通過不斷fork,execl進行不同功能的實現,所以無名管道最常用。
因爲是父子之間,所以不用單獨創建管道文件和打開文件,創建打開是一起的,故不用open操作,打開後就可以進行write,read等操作了。
無名管道沒有文件名,存儲在內核題和的一段內存中,通過藉助這段內存完成進程間的通信,進程結束,管道也隨之結束。
(一)基本概念
創建和打開管道文件是一起的,是通過pipe函數實現的:
# include<unistd.h>
int pipe(int filedes[2]);//數組2位就夠了,一個表示寫,一個表示讀
返回值:成功返回0,出錯返回-1
參數filedes返回兩個文件描述符,filedes[0]爲讀打開,filedes[1]爲寫打開,
filedes[1]的輸出是filedes[0]的輸入;即filedes[1]->filedes[0];
無名管道只能在具有公共祖先的進程之間使用,通常,一個管道由一個進程創建,然後該進程調用fork,此後父、子進程之間就可以對管道進行操作,我們來具體看一下過程:
首先父進程pipe創建和打開管道,內核開闢一段內存,稱爲管道,用於通信
,它有一個讀端,一個寫端,通過fds參數傳給用戶兩個文件描述符,fds[0]指向管道的讀端,fds[1]指向管道的寫端。此時管道的兩端通過進程相互連接,fds[1]寫端指向fds[0]讀端,數據通過內核開闢的管道流動:
父進程fork之後。子進程複製父進程的所有文件描述符,父進程有fds[1]->fds[0],子進程也有fd[1]->fds[0],都通過管道進行連接:
因爲管道是半雙工通信,所以只能一端讀,一端寫。那麼根據需求,將多餘的連接關閉,如果我們讓父進程寫,子進程讀,那麼需要父進程關閉fd[0]讀端,子進程關閉fd[1]寫端
:
當管道的一端關閉後,另一端打開時,會有兩種情況:
- 當管道的寫端被關閉,讀端打開時,在所有數據都被讀取後,read返回0,指示達到了文件結束處。
- 當管道的讀端被關閉,寫端打開時,則產生SIGPIPE信號,如果忽略該信號或者捕捉該信號並從處理程序返回,則write返回-1,error設置爲EPIPE。
表示如果同一時刻只打開管道的一端,如父進程打開寫端,子進程將寫端,讀端都關閉,那就會出現上述的的問題,或父進程關閉讀,寫端,子進程打開讀端,就產生第一種情況。
(二)實例
因爲是用於父子進程,所以一個.c文件即可。我們實現:父子進程的數據交互,父進程給子進程發送信息,即在父進程種關閉讀端,向管道中寫入數據,子進程關閉寫端,從管道中讀取數據。
# include<stdio.h>
# include<string.h>
# include<unistd.h>
# include<assert.h>
# include<string.h>
# include<fcntl.h>
int main()
{
int fds[2];
int res=pipe(fds);//fds[0]讀端fds[1]寫端
pid_t pid=fork();
assert(pid!=-1);
if(pid==0)//子進程
{
close(fds[1]);//關閉寫端
while(1)
{
char buff[128]={0};
int n=read(fds[0],buff,127);
if(n<=0)
{
break;
}
printf("read:%s\n",buff);
}
close(fds[0]);
}
else
{
close(fds[0]);
while(1)
{
char buff[128]={0};
printf("input:");
fgets(buff,127,stdin);
if(strncmp(buff,"end",3)==0)
{
break;
}
int n=write(fds[1],buff,strlen(buff)-1);
}
close(fds[1]);
}
}
將其運行:
可以看到,在子進程輸出之前會打出input,原因是它們是兩個獨立的進程,都在併發運行,即父進程運行自己的,子進程也在運行自己的,子進程是buff中有數據打印,比父進程慢一點,所以每次都會輸出input.
read,write一樣會阻塞,父進程不輸入,子進程就不會輸出。注意pipe函數不會阻塞,因爲調用它創建和打開管道文件,就有了讀端和寫端。
加油哦!💪。