
版權聲明:本文為 cdeveloper 原創(chuàng)文章,可以隨意轉載,但必須在明確位置注明出處!
Linux 進程間通信
當系統(tǒng)中有了多個進程時,進程之間的通信就顯得格外必要了,進程就相當于現(xiàn)實世界中的人,人跟人之間的交流就相當與進程之間的通信了。Linux 的進程間通信(Inter Process Communication,IPC)主要有 7 種:
- 無名管道
Pipe - 有名管道
Fifo - 信號
Signal - 消息隊列
Message Queue - 共享內存
Share Memory - 信號量
Semphone - 套接字
Socket
這 7 種方式有各自的適用場合。在早期管道和信號是用于單機 IPC 的主要方式,在后來 AT&T 的貝爾實驗室在那之上又拓展了一個 System V IPC,其中包含了共享內存,消息隊列,信號量這 3 種方法。
之后 BSD(加州大學伯克利分校軟件研發(fā)中心)開發(fā)了套接字用來進行網(wǎng)絡通信,從這也可以得出網(wǎng)絡通信其實就是不同機器之間的進程相互通信,本質上還是屬于進程間的通信,只不過多了一個網(wǎng)絡的橋梁而已。這就是整個 IPC 的發(fā)展過程,IPC 是 Linux 中的一個非常重要的模塊,必須掌握這 7 種方式,這也是面試必問的東西。
這篇文章主要介紹第一種 IPC 的機制:無名管道 Pipe,并且會分析它在 Linux 內核的實現(xiàn)機制,廢話不多說,趕緊上車...
什么是無名管道 Pipe?
shell 管道
管道是 UNIX 系統(tǒng) IPC 的最古老的形式,所有的 UNIX 系統(tǒng)都提供管道機制,如果你使用過 shell 中的管道,應該不會默認,例如:
ps -aux | grep "xxx"
這個意思是將 ps -aux 的輸出作為 grep xxx 的輸入,通過管道可以將兩個進程連接起來,功能非常強大,但是有名管道與 shell 的管道有些區(qū)別。
無名管道
有名管道具有下面 3 個特點:
- 只能用于有親緣關系(父子進程)的進程間通信
- 半雙工通信方式,具有固定的讀寫端
- Pipe 被當作特殊文件來對待(Linux 下一切都是文件)
需要了解下半雙工和全雙工的區(qū)別:
- 半雙工:同一時刻,數(shù)據(jù)只能往一個方向傳輸
- 全雙工:同一時刻,數(shù)據(jù)可以往兩個方向傳輸
有名管道是半雙工的,每個時刻一個進程只能讀取或者寫入,即只能打開讀端口或者寫端口,不可同時打開。下面的圖可以更好地解釋在父子進程之間使用管道的模型:

這個模型中內核有一個管道的緩沖區(qū),父進程將數(shù)據(jù)寫入管道寫端(fd[1]),子進程從管道讀端(fd[0])中讀取數(shù)據(jù)。
例子:test_pipe.c
了解了有名管道的基本原理,下面我們使用 pipe 來創(chuàng)建一個管道,這是 pipe 函數(shù)定義:
#include <unistd.h>
/*
* fd[0]:用于讀取
* fd[1]:用于寫入
* return:成功返回 0, 失敗返回 -1,并設置 erron
*/
int pipe(int pipefd[2]);
這個例子中我們在父進程中 fork 了一個子進程,在 fork 之后要做什么取決與我們想要的數(shù)據(jù)流的方向,這里設置子進程從父進程讀取數(shù)據(jù),所以需要關閉子進程的寫端 fd[1] 和父進程的讀端 fd[0],注意無名管道不能同時讀寫。
// test_pipe.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
int pfd[2];
int pid;
int status = 0;
char w_cont[] = "Hello child, I'm parent!";
char r_cont[255] = { 0 };
int write_len = strlen(w_cont);
// 創(chuàng)建管道
if(pipe(pfd) < 0) {
perror("create pipe failed");
exit(1);
} else {
// 創(chuàng)建子進程
if((pid = fork()) < 0) {
perror("create process failed");
} else if(pid > 0) {
// 關閉父進程讀端
close(pfd[0]);
// 父進程像寫端寫入數(shù)據(jù)
write(pfd[1], w_cont, write_len);
close(pfd[1]);
// 等待子進程結束
wait(&status);
} else {
sleep(2);
// 關閉子進程寫端
close(pfd[1]);
// 子進程從讀端讀取數(shù)據(jù)
read(pfd[0], r_cont, write_len);
// 子進程輸出讀取的數(shù)據(jù)
printf("child process read: %s\n", r_cont);
}
return 0 ;
}
編譯運行看看:
gcc test_pipe.c -o test_pipe
./test_pipe
child process read: Hello child, I'm parent!
可以看到子進程成功讀取了父進程寫入的數(shù)據(jù),整個過程一共分為 6 個步驟:
- 創(chuàng)建管道
- 創(chuàng)建子進程
- 父進程關閉讀端,向寫端寫入數(shù)據(jù)
- 子進程等待 2s,等父進程寫入完畢
- 子進程關閉寫端,從讀端讀取數(shù)據(jù)并輸出
- 父進程用 wait 等待子進程結束
這個例子可以很好的解釋管道的使用方法:父進程寫入,子進程讀取,當然你也可以設置子進程寫,父進程讀,只要改變進程的讀寫端口和代碼邏輯即可,代碼參考:test_pipe.c,test_pipe2.c
Pipe 的內核實現(xiàn)
管道的操作比較的簡單,為了更好的理解它的原理,我們看看 Linux 內核中的管道是如何實現(xiàn)的,因為不同版本的 Linux 內核中的修改比較大,這里以 Linux-3.4 版本來分析。
Pipe 注冊過程
內核的 Pipe 的實現(xiàn)原理大體上如下:Pipe 將內存中一片區(qū)域映射到虛擬文件系統(tǒng) VFS,使得上層應用可以像操作文件那樣來操作 Pipe,從而實現(xiàn) IPC,也就是說 Pipe 是以管道文件系統(tǒng)為基礎的,我們來看看 fs/pipe.c 中的 pipe 文件系統(tǒng)的注冊過程,實際上就是一個驅動程序:

這個過程向內核注冊了 pipe 的文件系統(tǒng),這個文件系統(tǒng)也受 VFS 的控制。
Pipe 的調用過程
再來看看管道的調用過程,上層的 pipe 調用一般都對應底層的 sys_pipe 調用,但是隨著內核的修改,有些名稱會改變,比如 sys_pipe 在 3.4 中就是用宏定義來表示的:
/*
* fs/pipe.c
* sys_pipe() is the normal C calling standard for creating
* a pipe. It's not the way Unix traditionally does this, though.
**/
SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags)
{
int fd[2];
int error;
error = do_pipe_flags(fd, flags);
if (!error) {
if (copy_to_user(fildes, fd, sizeof(fd))) {
sys_close(fd[0]);
sys_close(fd[1]);
error = -EFAULT;
}
}
return error;
}
這是具體的執(zhí)行過程:

這個過程所做的事情主要是向內核申請內存,創(chuàng)建讀寫描述符,以此建立 pipe 文件。其中比較重要的是 create_write_pipe,這個函數(shù)創(chuàng)建一個寫管道,在最后調用 kzalloc 向內核申請內存空間:

這也印證了 pipe 將內存中一片區(qū)域映射成虛擬文件系統(tǒng)以及 Linux 的進程間通信實質上就是 IO 操作這兩個概念。
結語
本次,我們了解了 Linux 下進程間通信(IPC)的 7 種方式,并著重學習了第一種方式:無名管道 Pipe。管道是最古老的 IPC 方式,使用起來也比較簡單,并且我們也簡單分析了內核中對 pipe 的實現(xiàn)過程,知道了 pipe 其實也是以文件 IO 的方式來實現(xiàn) IPC 的,了解些內核的機制可以讓我們對 IPC 有一個更好的理解。
感謝你的閱讀,我們下次再見 :)