版權聲明:本文為 cdeveloper 原創文章,可以隨意轉載,但必須在明確位置注明出處!
Linux 進程間通信
當系統中有了多個進程時,進程之間的通信就顯得格外必要了,進程就相當于現實世界中的人,人跟人之間的交流就相當與進程之間的通信了。Linux 的進程間通信(Inter Process Communication,IPC)主要有 7 種:
- 無名管道
Pipe
- 有名管道
Fifo
- 信號
Signal
- 消息隊列
Message Queue
- 共享內存
Share Memory
- 信號量
Semphone
- 套接字
Socket
這 7 種方式有各自的適用場合。在早期管道和信號是用于單機 IPC 的主要方式,在后來 AT&T
的貝爾實驗室在那之上又拓展了一個 System V IPC
,其中包含了共享內存,消息隊列,信號量這 3 種方法。
之后 BSD(加州大學伯克利分校軟件研發中心)開發了套接字用來進行網絡通信,從這也可以得出網絡通信其實就是不同機器之間的進程相互通信,本質上還是屬于進程間的通信,只不過多了一個網絡的橋梁而已。這就是整個 IPC 的發展過程,IPC 是 Linux 中的一個非常重要的模塊,必須掌握這 7 種方式,這也是面試必問的東西。
這篇文章主要介紹第一種 IPC 的機制:無名管道 Pipe,并且會分析它在 Linux 內核的實現機制,廢話不多說,趕緊上車...
什么是無名管道 Pipe?
shell 管道
管道是 UNIX 系統 IPC 的最古老的形式,所有的 UNIX 系統都提供管道機制,如果你使用過 shell
中的管道,應該不會默認,例如:
ps -aux | grep "xxx"
這個意思是將 ps -aux
的輸出作為 grep xxx
的輸入,通過管道可以將兩個進程連接起來,功能非常強大,但是有名管道與 shell
的管道有些區別。
無名管道
有名管道具有下面 3 個特點:
- 只能用于有親緣關系(父子進程)的進程間通信
- 半雙工通信方式,具有固定的讀寫端
- Pipe 被當作特殊文件來對待(Linux 下一切都是文件)
需要了解下半雙工和全雙工的區別:
- 半雙工:同一時刻,數據只能往一個方向傳輸
- 全雙工:同一時刻,數據可以往兩個方向傳輸
有名管道是半雙工的,每個時刻一個進程只能讀取或者寫入,即只能打開讀端口或者寫端口,不可同時打開。下面的圖可以更好地解釋在父子進程之間使用管道的模型:
這個模型中內核有一個管道的緩沖區,父進程將數據寫入管道寫端(fd[1]),子進程從管道讀端(fd[0])中讀取數據。
例子:test_pipe.c
了解了有名管道的基本原理,下面我們使用 pipe
來創建一個管道,這是 pipe 函數定義:
#include <unistd.h>
/*
* fd[0]:用于讀取
* fd[1]:用于寫入
* return:成功返回 0, 失敗返回 -1,并設置 erron
*/
int pipe(int pipefd[2]);
這個例子中我們在父進程中 fork 了一個子進程,在 fork 之后要做什么取決與我們想要的數據流的方向,這里設置子進程從父進程讀取數據,所以需要關閉子進程的寫端 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);
// 創建管道
if(pipe(pfd) < 0) {
perror("create pipe failed");
exit(1);
} else {
// 創建子進程
if((pid = fork()) < 0) {
perror("create process failed");
} else if(pid > 0) {
// 關閉父進程讀端
close(pfd[0]);
// 父進程像寫端寫入數據
write(pfd[1], w_cont, write_len);
close(pfd[1]);
// 等待子進程結束
wait(&status);
} else {
sleep(2);
// 關閉子進程寫端
close(pfd[1]);
// 子進程從讀端讀取數據
read(pfd[0], r_cont, write_len);
// 子進程輸出讀取的數據
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!
可以看到子進程成功讀取了父進程寫入的數據,整個過程一共分為 6 個步驟:
- 創建管道
- 創建子進程
- 父進程關閉讀端,向寫端寫入數據
- 子進程等待 2s,等父進程寫入完畢
- 子進程關閉寫端,從讀端讀取數據并輸出
- 父進程用 wait 等待子進程結束
這個例子可以很好的解釋管道的使用方法:父進程寫入,子進程讀取,當然你也可以設置子進程寫,父進程讀,只要改變進程的讀寫端口和代碼邏輯即可,代碼參考:test_pipe.c,test_pipe2.c
Pipe 的內核實現
管道的操作比較的簡單,為了更好的理解它的原理,我們看看 Linux 內核中的管道是如何實現的,因為不同版本的 Linux 內核中的修改比較大,這里以 Linux-3.4 版本來分析。
Pipe 注冊過程
內核的 Pipe 的實現原理大體上如下:Pipe 將內存中一片區域映射到虛擬文件系統 VFS,使得上層應用可以像操作文件那樣來操作 Pipe,從而實現 IPC,也就是說 Pipe 是以管道文件系統為基礎的,我們來看看 fs/pipe.c
中的 pipe 文件系統的注冊過程,實際上就是一個驅動程序:
這個過程向內核注冊了 pipe 的文件系統,這個文件系統也受 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;
}
這是具體的執行過程:
這個過程所做的事情主要是向內核申請內存,創建讀寫描述符,以此建立 pipe 文件。其中比較重要的是 create_write_pipe
,這個函數創建一個寫管道,在最后調用 kzalloc
向內核申請內存空間:
這也印證了 pipe 將內存中一片區域映射成虛擬文件系統以及 Linux 的進程間通信實質上就是 IO 操作這兩個概念。
結語
本次,我們了解了 Linux 下進程間通信(IPC)的 7 種方式,并著重學習了第一種方式:無名管道 Pipe。管道是最古老的 IPC 方式,使用起來也比較簡單,并且我們也簡單分析了內核中對 pipe 的實現過程,知道了 pipe 其實也是以文件 IO 的方式來實現 IPC 的,了解些內核的機制可以讓我們對 IPC 有一個更好的理解。
感謝你的閱讀,我們下次再見 :)