從 0 開始學習 Linux 系列之「19.無名管道 Pipe」

無名管道 Pipe

版權聲明:本文為 cdeveloper 原創文章,可以隨意轉載,但必須在明確位置注明出處!

Linux 進程間通信

當系統中有了多個進程時,進程之間的通信就顯得格外必要了,進程就相當于現實世界中的人,人跟人之間的交流就相當與進程之間的通信了。Linux 的進程間通信Inter Process Communication,IPC)主要有 7 種:

  1. 無名管道 Pipe
  2. 有名管道 Fifo
  3. 信號 Signal
  4. 消息隊列 Message Queue
  5. 共享內存 Share Memory
  6. 信號量 Semphone
  7. 套接字 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 個特點:

  1. 只能用于有親緣關系(父子進程)的進程間通信
  2. 半雙工通信方式,具有固定的讀寫端
  3. Pipe 被當作特殊文件來對待(Linux 下一切都是文件)

需要了解下半雙工和全雙工的區別:

  1. 半雙工:同一時刻,數據只能往一個方向傳輸
  2. 全雙工:同一時刻,數據可以往兩個方向傳輸

有名管道是半雙工的,每個時刻一個進程只能讀取或者寫入,即只能打開讀端口或者寫端口,不可同時打開。下面的圖可以更好地解釋在父子進程之間使用管道的模型:

管道模型

這個模型中內核有一個管道的緩沖區,父進程將數據寫入管道寫端(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 個步驟:

  1. 創建管道
  2. 創建子進程
  3. 父進程關閉讀端,向寫端寫入數據
  4. 子進程等待 2s,等父進程寫入完畢
  5. 子進程關閉寫端,從讀端讀取數據并輸出
  6. 父進程用 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 有一個更好的理解。

感謝你的閱讀,我們下次再見 :)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android跨進程通信IPC整體內容如下 1、Android跨進程通信IPC之1——Linux基礎2、Andro...
    隔壁老李頭閱讀 15,732評論 19 113
  • 1 進程介紹 1.1 進程和程序 所謂進程是由正文段(text)、用戶數據段(user segment)以及系統數...
    瘋狂小王子閱讀 1,263評論 0 7
  • 前言 管道是UNIX環境中歷史最悠久的進程間通信方式,也是最簡單的進程間通信方式,一般用來作為IPC的入門,最合適...
    GeekerLou閱讀 1,190評論 0 6
  • 一.管道機制(pipe) 1.Linux的fork操作 在計算機領域中,尤其是Unix及類Unix系統操作系統中,...
    Geeks_Liu閱讀 3,727評論 1 9
  • 項目崩潰,crashLog可是你的救命稻草,能讓你快速分析出bug的問題所在,不會收集可不行。當然項目在開發時,可...
    怪小喵閱讀 6,871評論 1 15