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

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