1.進程間通信介紹
1.1 進程通信的基本概念
在之前我們已經學習過進程地址空間。Linux 環境下,進程地址空間相互獨立,每個進程各自有不同的用戶地址空間。任何一個進程的全局變量在另一個進程中都看不到,所以進程和進程之間不能相互訪問,要交換數據必須通過內核,在內核中開辟一塊緩沖區,進程1把數據從用戶空間拷到內核緩沖區,進程2再從內核緩沖區把數據讀走,內核提供的這種機制稱為進程間通信(IPC,Inter Process Communication)。
1.2 為什么要進程間通信
進程通信主要有以下目的:
- 數據傳輸:一個進程需要將它的數據發送給另一個進程。
- 資源共享:多個進程之間共享同樣的資源。
- 通知事件:一個進程需要向另一個或一組進程發送消息,通知它(它們)發生了某種事件(如進程終止時要通知父進程)。
- 進程控制:有些進程希望完全控制另一個進程的執行(如Debug進程),此時控制進程希望能夠攔截另一個進程的所有陷入和異常,並能夠及時知道它的狀態改變。
1.3 常見的進程通信方式
在進程間完成數據傳遞需要借助操作系統提供特殊的方法,如今常見的進程間通信方式有:
① 管道 (分為匿名管道與命名管道)
② 信號 (開銷最小)
③ 共享內存
2.管道
2.1管道簡介
管道是Unix中最古老的進程間通信方式,我們把從一個進程連接到另一個進程的數據流叫做管道。
在Linux中,| 符號被用來代表管道。因為在Linux中,不同的命令,如ps,ls,grep等命令的本質都是可執行程序,| 前面的命令前面的命令通常會輸出大量的結果,這些結果將會交由 | 后面的命令繼續處理。
如下面這個命令就是將ps axj中含有PID的結果輸出:
2.2 管道的創建和應用
管道的本質是內核中一塊供不同進程進行讀寫的緩沖區,而外在的操作形式是通過文件讀寫的方式進行。
#include <unistd.h>
功能:創建一無名管道
原型
int pipe(int fd[2]);
參數
fd:文件描述符數組,這是一個輸出型參數,調用該接口后,將會給fd[2]數組分配兩個文件描述符,兩個文件描述符分別對應管道的讀寫兩端。其中fd[0]表示讀端, fd[1]表示寫端
返回值:成功返回0,失敗返回錯誤代碼
我們先用一個簡單的例子來看一下管道的創建:
#include<iostream>
#include<unistd.h>
int main()
{
int fd[2];
int ret=pipe(fd);
if(-1==ret)
{
std::cout<<"管道創建失敗!"<<std::endl;
}
std::cout<<"fd[0]:"<<fd[0]<<std::endl<<"fd[1]:"<<fd[1]<<std::endl;
return 0;
}
運行后:
可以看到,此時fd[0]和fd[1]返回了兩個文件描述符。這兩個文件描述符分別分別對應管道的讀寫兩端。
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<stdlib.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
int fd[2];
pipe(fd);
pid_t pid = fork();
if(pid < 0)
{
printf("fork error!");
}else if(pid == 0)
{
//child
close(fd[0]);
char str[100];
while(1)
{
printf("child:");
fgets(str, 100, stdin);
ssize_t len = strlen(str);
if(write(fd[1], str, len) != len)
{
perror("write to pipe");
exit(1);
}
memset(str, 0, len);
sleep(1);
}
}
//father
int count = 0;
close(fd[1]);
while(count < 10)
{
char str[100];
ssize_t s = read(fd[0], str, 100);
if(s < 0){
perror("read from pipe");
break;
}else{
printf("father:%s", str);
}
memset(str, 0, strlen(str));
}
return 0;
}
上面這段代碼實現了子進程寫入管道,父進程讀出的過程。
2.3 管道的底層機制
管道是在有血緣關系的進程之間來通信的,如父子進程,兄弟進程等。因此,應用匿名管道時一定會有fork函數的參與。
如下面這個簡化圖可以看到,
-
父進程先使用pipe函數創建管道,得到兩個文件描述符 fd[0]、fd[1]指向管道的讀端和寫端。
-
父進程調用fork創建子進程,此時父子進程有相同的struct files_struct,父子進程指向的struct file又指向了同一片文件緩沖區。(注意:這個表述並不嚴謹,我們下面馬上就會講到)
-
接下來父進程關閉寫端,子進程關閉讀端,就可以實現子進程向管道中寫,父進程讀。注意:管道的通信是單向的!!!!

在 Linux 中,管道的實現並沒有使用專門的數據結構,而是借助了文件系統的file結構和VFS的索引節點inode。通過將兩個 file struct指向同一個臨時的 inode,而這個 VFS 索引節點又指向一個物理頁面而實現的。

如上圖所示,有兩個 file 數據結構,但它們定義文件操作例程地址是不同的,其中一個是向管道中寫入數據的例程地址,而另一個是從管道中讀出數據的例程地址。
這樣,用戶程序的系統調用仍然是通常的文件操作,而內核卻利用這種抽象機制實現了管道這一特殊操作。看待管道,就如同看待文件一樣!管道的使用和文件一致,迎合了“Linux一切皆文件思想”。
2.4 管道讀寫規則
用阻塞的方式打開管道(即默認情況下)
-
如果所有管道寫端對應的文件描述符被關閉(管道寫端引用計數為 0),讀端在將管道中剩余數據讀取后,再次read會返回0。(寫端關閉)
-
如果有指向管道寫端的文件描述符沒關閉,且持有管道寫端的進程也沒有向管道中寫數據,這時有進程從管道讀端讀數據,那么管道中剩余的數據都被讀取后,再次 read 會阻塞。(讀完不寫)
-
如果所有指向管道讀端的文件描述符都關閉了(管道讀端引用計數為 0),進行write操作會產生信號SIGPIPE,進而可能導致write進程退出。(讀端關閉)
-
如果有指向管道讀端的文件描述符沒關閉(管道讀端引用計數大於 0),且讀端進程並沒有向管道中讀進程,則當寫端進程寫滿后,會進入阻塞。(寫滿不讀)
2.5 管道的特點
- 只能用於具有共同祖先的進程(具有親緣關系的進程)之間進行通信。
- 管道提供流式服務。
- 管道的生命周期隨進程,進程退出,管道釋放。
- 內核會對管道操作進行同步與互斥。
- 管道是半雙工的,數據只能向一個方向流動;需要雙方通信時,需要建立起兩個管道
- 管道大小為65536 byte