Linux中父子進程、兄弟子進程之間通信方式--匿名管道pipe(適用於有血緣關係的進程)



簡 述: 對於有血緣關係的進程間通信,如父子進程、兄弟子進程子之間的通信,可以採用(匿名)管道的 pipe 方式。 而進程間通信一共有四種方式:管道、信號、共享映射區、套接字。且說一個概念,進程間通信(Inter Process Communication),字母首寫即爲 IPC。


編程環境:

💻: uos20 📎 gcc/g++ 8.3 📎 gdb8.0

💻: MacOS 10.14 📎 gcc/g++ 9.2 📎 gdb8.3


進程間通信 IPC:

進程間通信(Inter Process Communication),字母首寫即爲 IPC。

  • 進程間常用的 4 種方式:
    • 管道 (簡單)
    • 信號 (系統開銷小)
    • 共享映射區 (有無血緣關係的進程間通信都可以)
    • 本地 socket 套接字 (穩定)

匿名管道(pipe):

int pipe(int fildes[2]);

一般都是指創建匿名管道,其中傳出 int fildes[2] 參數是固定的;fildes[0] 代表讀端,fildes[1] 代表寫端。適用於有血緣關係的進程。通常父子進程之間是不要使用 sleep() 函數的,因爲管道默認就是堵塞的。雖然實現形態上是文件,但是管道本身並不佔用磁盤或者其他外部存儲的空間。在Linux的實現上,它佔用的是內存空間。所以,Linux上的管道就是一個操作方式爲文件的內存緩衝區。


  • 本質:
    • 是內核緩衝區。也是僞文件(不佔用磁盤空間)
  • 特點:
    • 其讀端和寫端,對應兩個文件描述符;數據寫端流入,讀端流出
    • 操作管道的進程被銷燬之後,管道自動被釋放了
    • 管道默認是阻塞的(讀寫端)
  • 實現原理:
    • 內部實現方式:環形隊列。只有一次讀寫的機會。先進先出。
    • 其緩衝區默認是 4K 大小;但是大小會隨着實際情況適當 調整。
  • 侷限性:
    • 隊列:數據只能夠讀取一次,不能夠重複讀取
    • 半全工方式工作

父子進程間通信:

  • 寫一個父子進程間,使用管道通信的例子:

    比如說,寫一個例子,實現父子進程間的通信,實現 ps aux | grep bash 的實現。你可以看下面的思路圖解析,然後自己嘗試敲寫一遍,沒有出來的話,再來看我的。學習之事,不能急於求成,始終是那句話:紙上學來終覺淺,覺知此事要躬行。 當你開始寫下第一行代碼的時候,你會感受到創造的快樂。

  • 代碼實現:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
    int fd[2];   //用來標記管道的兩端的文件描述符
    int ret = pipe(fd);  //創建管道 pipe. fd[2] 是固定的輸出參數

    if (ret == -1) {
        perror("[pipe create file] ");
        return 0;
    }
        
    int pipeRead = fd[0];   
    int pipeWrite = fd[1];

    pid_t pid = fork();

    if (pid > 0) {                         //parent process
        dup2(pipeWrite, STDOUT_FILENO);    //重定向
        close(pipeRead);
        execlp("ps", "ps", "aux", NULL);
    } else if (pid == 0) {                 //child process
        dup2(pipeRead, STDIN_FILENO);      //重定向
        close(pipeWrite);
        execlp("grep", "grep", "bash", "--color=auto", NULL);
    }
    
    return 0;
}
  • 代碼分析:

    • 父進程的 3 號文件描述符表,指向 pipe 管道的寫端,要記得此刻把它的讀端關閉

    • 子進程的 4 號文件描述符表,指向 pipe 管道的讀端,要記得此刻把它的寫端關閉

    • 因爲管道只能夠一次機會讀寫機會。如果要父子進程都能夠讀寫,那麼還得加一個管道

    • 管道是默認是阻塞的

    • 其中對代碼進程中的 20-28行的理解,和文件描述表中的文件描述符 0-4號 的對應關係如下:

  • 運行結果:

    • 在 Ubuntu 18.4 上面的效果如圖:

    • 在 Mac 10.14.6 上面效果(後面那一串多餘的,都是它自帶的一些命令參數)

      • 當你故意開多個終端的時候,也會把其他的終端搜索出來

    • 由上面是可以看到 在 Linux 和 Unix 系統之間是有着一些區別的,第1、2 張截圖,其進程的所有者,Linux 中兩個都是 muli 用戶;而 Unix 一個是 root 系統 ,一個是 muli 用戶。但是這裏有一段困惑,怎麼 Unix 中顯示 root 的那個進程, 後面是有顯示爲我的一個虛擬機軟件y?怎麼和他扯上關係的???這裏暫時有一個小的困惑。在下一個例子中,會看到更有其他的不同。

兄弟子進程間通信:

這裏實現兩個兄弟子進程(無孫子進程)之間,利用管道進程 pipe 進程通信的例子

  • 先上代碼: 這是由上面一個例子改寫的,思路可以參考前面那個圖

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main(int argc, char *argv[])
    {
        int fd[2];   //用來標記管道的兩端的文件描述符
        int ret = pipe(fd);  //創建管道 pipe. fd[2] 是固定的輸出參數
    
        if (ret == -1) {
            perror("[pipe create file] ");
            return 0;
        }
            
        int pipeRead = fd[0];   
        int pipeWrite = fd[1];
    
        int i = 0;
        for ( ; i < 2; i++) {
            pid_t pid = fork();
            
            if (pid == 0)
                break;
    
            if (pid == -1)
                perror("[creator process file:]");
    
        }
    
        if (i == 0) {                         //child process 1
            dup2(pipeWrite, STDOUT_FILENO);   
            close(pipeRead);
            execlp("ps", "ps", "aux", NULL);
        } else if (i == 1) {                 //child process 2
            dup2(pipeRead, STDIN_FILENO);    
            close(pipeWrite);
            execlp("grep", "grep", "bash", "--color=auto", NULL);
        } else if (i == 2) {                 //parent process
            close(pipeWrite);
            close(pipeRead);
    
            // sleep(2);
            int wpid;
            while ( wpid = waitpid(-1, NULL, WNOHANG) != -1) {  //回收子進程
                if (wpid == 1) ///sbin/init splash 進程    /sbin/launchd
                    continue;
    
                if (wpid == 0) 
                    continue;
    
                printf("child dide pid = %d\n", wpid);
            }    
        }
        
        printf("pipeWrite = %d,  pipeRead = %d\n", pipeWrite, pipeRead);
        return 0;
    }
    
  • 附上該例子的思路圖:

    • 要注意關閉父進程和不必要的進程的對應對管道的讀寫的文件描述符。且注意到管道默認是阻塞的。
  • 運行效果:

    • 在 Ubuntu 18.4 中的效果:

    • 在 Mac 10.14。6 中的效果:

  • 運行結果以及代碼分析:

    • 在 Linux 和 Unix 下,均可看到,是 三行打印輸出的結果
      • 第一行是查詢到 子進程 1 的信息,往管道里面寫入 ps aux 的那個
      • 第二行是查詢到 子進程 2 的信息,往管道里面寫入 grep “bash” --color=auto 的那個
      • 第三行是父進程,打印的輸出信息。
    • 思考,如果去掉代碼42,45,46 行,會發生什麼???會發現在 Linux 和 Unix 上面,都會答應批量的 child dide pid = 1 信息。查看了一下,該進程號 pid == 1,在 Linux 下是 /sbin/init splash ;在 Unix 下是 /sbin/launchd 進程。 也就是書上和一些博客說的 init 進程,專門用來領養孤兒進程的,用來釋放 子(孤兒)進程的 系統空間的 PCB

管道的讀寫行爲:

此部分爲理解,但是歸總結納一下:

讀操作:

  • 有數據:
    • read(fd), -正常讀,返回讀出的字節數
  • 無數據:
    • 寫端全部關閉
      • read 解除阻塞,返回 0
      • 相當於讀文件到了尾部
    • 沒有全部關閉
      • read 阻塞(比如設置了一個 sleep(10))

寫操作:

  • 讀端全部關閉:
    • 管道破裂,進程被終止
      • 內核給當前進程發送 SIGPIPE 信號(13 號信號)
  • 讀端沒有全部關閉
    • 緩衝區寫滿了
      • write 阻塞
    • 緩衝區沒有寫滿
      • write 繼續寫

查看管道緩衝區大小:

  • 命令: ulimix -a 現在在 Linux 和 Unix 上面,都沒有 pipe size 這一欄詳細信息了

  • 函數: fpathconf()

    • 一個例子:

      int fd[2];   //用來標記管道的兩端的文件描述符
      int ret = pipe(fd);  //創建管道 pipe. fd[2] 是固定的輸出參數
      
      int pipeSize = fpathconf(fd[0], _PC_PIPE_BUF);  //fd[1] 也行
      printf("pipeSize = %d\n", pipeSize);
      
    • 輸出結果:Unix 下是 512 大小


設置管道的非阻塞屬性:

  • 默認管道是讀寫兩端都是阻塞

  • fcntl() --變參函數

    • 作用一:複製文件描述符 -dup
    • 作用二:修改文件屬性 -open 的時候對應 flag 屬性
  • 例如實現讀端爲非阻塞 pipe(fd)

    int flags = fcntl(fd[0], F_GETFL);  //獲取原來的 flags, F_GET FL (get flags 的縮寫)
    flags |= O_NONBLOCK;                //添加非阻塞屬性
    fcntl(fd[0], F_SETFL, flages);      //設置新的屬性
    

下載地址:

11_pipe

歡迎 star 和 fork 這個系列的 linux 學習,附學習由淺入深的目錄。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章