Linux進程間通信—管道(無名管道、pipe)

概述

管道又稱無名管道、匿名管道,是被所有UNIX like系統支持的古老通信方式。

管道是單向字節流,在Linux中管道是通過指向同一個臨時的VFS inode的兩個file數據結構來實現的,此VFS inode指向內存中的同一個物理頁面。這就隱藏了讀寫管道和讀寫普通文件的差別。管道對於管道兩端的進程而言,就是一個文件,但它不是普通的文件,它不屬於某種文件系統,單獨構成一種文件系統,並且只存在與內存中。數據的讀出和寫入:一個進程向管道中寫的內容被管道另一端的進程讀出。寫入的內容每次都添加在管道緩衝區的末尾,並且每次都是從緩衝區的頭部讀出數據。

管道在應用程序中體現爲2個打開的文件描述符:

+--------------------+     +------------------------------+      +--------------------+
|                    |     |                              |      |                    |
|                    |     |     +-----------------+      |      |                    |
|                    |     |     |                 |      |      |                    |
|              fd[0]<------------+     Pipe        +<--------------+fd[1]             |
|                    |     |     +-----------------+      |      |                    |
|                    |     |                              |      |                    |
+--------------------+     +------------------------------+      +--------------------+

   User Application                    Kernel                       User Application

特點

  1. 只有具有親緣關係的進程纔可以通過管道進行通信;
  2. 半雙工,同一時刻,數據只能往一個方向流動;
  3. 寫入管道的順序遵循先入先出(FIFO)原則;
  4. 從管道讀取數據是一次性操作,數據一旦讀取,則從管道中刪除;
+-----------+   +-----------+  +---------------------+   +-----------+   +-------------+
|           |   |           |  |                     |   |           |   |             |
|           |   |           |  |       +------+      |   |           |   |             |
|           | w |           |Y |       |      |      | R |           | Y |             |
|     Write +---> Writable? +--> Write | Pipe | Read +---> Readable? +---> Write       |
|Application|   |(not full) |  | End   |      | End  |   |(not empty)|   | Application |
|           |   |           |  |       +------+      |   |           |   |             |
|           |   |           |  |                     |   |           |   |             |
+-----------+   +------+----+  +---------------------+   +-----+-----+   +-------------+
                       |                                       |
                      N|                                       | N
                       |                                       |
                +------v----+                            +-----v-----+
                |           |                            |           |
                |    Sleep  |                            |   Sleep   |
                |           |                            |           |
                +-----------+                            +-----------+

相關API

#include <unistd.h>
/* 創建無名管道
 * @filedes: 爲 int 型數組的首地址,其存放了管道的文件描述符 filedes[0]、filedes[1]。
 * 當一個管道建立時,它會創建兩個文件描述符 fd[0] 和 fd[1]。其中 fd[0] 固定用於讀管道,而 fd[1] 固定用於寫管道。
 * 一般文件 I/O 的函數都可以用來操作管道( lseek() 除外)。
 * @return 成功:0,失敗:-1 */
int pipe(int filedes[2]);

舉例

一般情況: 子進程寫、父進程讀

一般情況下,在父進程中創建管道,並fork出子進程,這樣在父子進程中分別用fd[0]和fd[1]進行讀寫,例如,父進程中創建fd_pipe[2], 並fork子進程, 子進程向fd[1]中寫入“Hello, Father"字符串,父進程中通過fd_pipe[0]讀取該字符串並打印。(如果需要同時讀寫,爲避免混亂,應使用2個管道)

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
    int fd_pipe[2] = {0};
    pid_t pid;

    if( pipe(fd_pipe) < 0 ){// 創建無名管道
        perror("pipe");
    }

    pid = fork(); // 創建進程
    if( pid < 0 ){ // 出錯
        perror("fork");
        exit(-1);
    }

    if( pid == 0 ){ // 子進程
        char str[] = "Hello, father";

        write(fd_pipe[1], str, strlen(str));
        printf("[child] write done. \n");

        _exit(0);
    }else if( pid > 0){// 父進程

        wait(NULL);	// 等待子進程結束,回收其資源

        char str[50] = {0};

        printf("[father] before read\n");

        read(fd_pipe[0], str, sizeof(str));

        printf("[father] after read\n");

        printf("[father] str=[%s]\n", str); // 打印數據
    }

    return 0;
}

管道空: read會阻塞,直到有數據

管道滿 : write會阻塞,直到可寫

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <time.h>

#define debug(fmt, args...) do { now(); printf(fmt, ##args); } while(0)

char* now()
{
    time_t rawtime;
    struct tm * timeinfo;
    char *t;

    time( &rawtime );
    timeinfo = localtime( &rawtime );
    t = asctime(timeinfo);
    t[strlen(t) - 1] = 0;
    printf("[%s] ", t);
}

int main(int argc, char *argv[])
{
    int fd_pipe[2] = {0};
    pid_t pid;
    char str[1024] = "";

    memset(str, 'a', sizeof(str));

    if( pipe(fd_pipe) < 0 ){// 創建無名管道
        perror("pipe");
    }

    pid = fork(); // 創建進程
    if( pid < 0 ){ // 出錯
        perror("fork");
        exit(-1);
    }

    if( pid == 0 ){ // 子進程
        int i = 0;
        int len = 0;
        while(1) {
            i++;
            len = write(fd_pipe[1], str, sizeof(str)); // 寫滿後會阻塞
            debug("[child] i ===%d.  write %d byte\n", i, len);
        }

        _exit(0);
    }else if( pid > 0){// 父進程

        char str[2048] = {0};

        int len = 0;
        while(1) {
            sleep(2);
            len = read(fd_pipe[0], str, sizeof(str));

            debug("[father] read %d byte data\n", len);
        }
    }

    return 0;
}

輸出結果:

[Tue Mar 12 17:14:22 2019] [child] i ===1.  1024
[Tue Mar 12 17:14:22 2019] [child] i ===2.  1024
[Tue Mar 12 17:14:22 2019] [child] i ===3.  1024
... ...
[Tue Mar 12 17:14:22 2019] [child] i ===62.  1024
[Tue Mar 12 17:14:22 2019] [child] i ===63.  1024
[Tue Mar 12 17:14:22 2019] [child] i ===64.  1024  // 此處寫完後,需要等待3s之後,父進程將數據讀出來之後纔可以繼續寫入)
[Tue Mar 12 17:14:24 2019] [father] read 2048
[Tue Mar 12 17:14:26 2019] [father] read 2048
[Tue Mar 12 17:14:26 2019] [child] i ===65.  1024
[Tue Mar 12 17:14:26 2019] [child] i ===66.  1024
[Tue Mar 12 17:14:26 2019] [child] i ===67.  1024
[Tue Mar 12 17:14:26 2019] [child] i ===68.  1024
[Tue Mar 12 17:14:28 2019] [father] read 2048

讀端關閉,導致SIGPIPE

當管道的所有讀端均關閉時,如果再往裏寫入數據時會拋出SIGPIPE信號,其默認處理動作是中斷當前進程。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <time.h>

#include <signal.h>

#define debug(fmt, args...) do { now(); printf(fmt, ##args); } while(0)

char* now()
{
    time_t rawtime;
    struct tm * timeinfo;
    char *t;

    time( &rawtime );
    timeinfo = localtime( &rawtime );
    t = asctime(timeinfo);
    t[strlen(t) - 1] = 0;
    printf("[%s] ", t);
}

static void sig_handler(int signo)
{
    debug("[father] get signal %d\n", signo);
}

int main(int argc, char *argv[])
{
    int fd_pipe[2] = {0};
    pid_t pid;
    char str[16] = "";

    struct sigaction psa;

    psa.sa_handler = sig_handler;
    sigaction(SIGPIPE, &psa, NULL);

    memset(str, 'a', sizeof(str));

    if( pipe(fd_pipe) < 0 ){// 創建無名管道
        perror("pipe");
    }

    pid = fork(); // 創建進程
    if( pid < 0 ){ // 出錯
        perror("fork");
        exit(-1);
    }

    if( pid == 0 ){ // 子進程

        sleep(3);
        close(fd_pipe[0]); // 主動關閉讀端
        sleep(100);
        _exit(0);
    } else if ( pid > 0){// 父進程

        int len = 0;

        close(fd_pipe[0]);
        while(1) {
            len = write(fd_pipe[1], str, sizeof(str)); 
            // 當所有讀端都關閉後,纔會拋出SIGPIPE信號

            sleep(1);
            debug("[father] write %d byte data\n", len);
        }
    }

    return 0;
}

輸出結果:3秒後,子進程關閉讀端,寫入錯誤,拋出SIGPIPE信號。

[Tue Mar 12 18:29:27 2019] [father] write 16 byte data
[Tue Mar 12 18:29:28 2019] [father] write 16 byte data
[Tue Mar 12 18:29:29 2019] [father] write 16 byte data
[Tue Mar 12 18:29:29 2019] [father] get signal 13
[Tue Mar 12 18:29:30 2019] [father] write -1 byte data
[Tue Mar 12 18:29:30 2019] [father] get signal 13
[Tue Mar 12 18:29:31 2019] [father] write -1 byte data
[Tue Mar 12 18:29:31 2019] [father] get signal 13

非阻塞方式使用PIPE

在非阻塞模式下,write/read函數會直接返回-1, 而不是阻塞等待。

fcntl(fd, F_SETFL, 0);	// 設置爲阻塞模式, 默認情況

fcntl(fd, F_SETFL, O_NONBLOCK); // 設置爲非阻塞模式
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <time.h>

#include <fcntl.h>

#define debug(fmt, args...) do { now(); printf(fmt, ##args); } while(0)

char* now()
{
    time_t rawtime;
    struct tm * timeinfo;
    char *t;

    time( &rawtime );
    timeinfo = localtime( &rawtime );
    t = asctime(timeinfo);
    t[strlen(t) - 1] = 0;
    printf("[%s] ", t);
}

int main(int argc, char *argv[])
{
    int fd_pipe[2] = {0};
    pid_t pid;
    char str[1024] = "";

    memset(str, 'a', sizeof(str));

    if( pipe(fd_pipe) < 0 ){// 創建無名管道
        perror("pipe");
    }

    // 設置讀寫均爲非阻塞
    fcntl(fd_pipe[0], F_SETFL, O_NONBLOCK);
    fcntl(fd_pipe[1], F_SETFL, O_NONBLOCK);

    pid = fork(); // 創建進程
    if( pid < 0 ){ // 出錯
        perror("fork");
        exit(-1);
    }

    if( pid == 0 ){ // 子進程
        int i = 0;
        int len = 0;
        while(1) {
            i++;
            len = write(fd_pipe[1], str, sizeof(str));
            debug("[child] i ===%d.  write %d byte\n", i, len);
            sleep(3); // 確保寫入速度低
        }

        _exit(0);
    }else if( pid > 0){// 父進程

        char str[2048] = {0};

        int len = 0;
        while(1) {
            // 非阻塞,len爲-1
            len = read(fd_pipe[0], str, sizeof(str));

            debug("[father] read %d byte data\n", len);
            sleep(1);
        }
    }

    return 0;
}

輸出結果:

每隔3s,才能讀出1024字節數據,其他時候返回-1, 並不會阻塞。

[Tue Mar 12 18:38:46 2019] [father] read -1 byte data
[Tue Mar 12 18:38:46 2019] [child] i ===1.  write 1024 byte
[Tue Mar 12 18:38:47 2019] [father] read 1024 byte data
[Tue Mar 12 18:38:48 2019] [father] read -1 byte data
[Tue Mar 12 18:38:49 2019] [child] i ===2.  write 1024 byte
[Tue Mar 12 18:38:49 2019] [father] read 1024 byte data
[Tue Mar 12 18:38:50 2019] [father] read -1 byte data
[Tue Mar 12 18:38:51 2019] [father] read -1 byte data
[Tue Mar 12 18:38:52 2019] [child] i ===3.  write 1024 byte

參考資料

https://blog.csdn.net/tennysonsky/article/details/46315517

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