进程间通信,pipe匿名管道

Last Updated: 2023-11-05 08:21:23 Sunday

-- TOC --

系统调用pipe接口,用于创建一个匿名管道(即不在文件系统中存在,没有显示出来的pathname),用于父进程和子进程之间的通信。匿名管道是单向的!

// pipe - create pipe
// man 2 pipe
// man 7 pipe
#include <unistd.h>
int pipe(int pipefd[2]);

pipe() creates a pipe, a unidirectional data channel that can be used for interprocess communication. The array pipefd is used to return two file descriptors referring to the ends of the pipe. pipefd[0] refers to the read end of the pipe. pipefd[1] refers to the write end of the pipe. Data written to the write end of the pipe is buffered by the kernel until it is read from the read end of the pipe.

pipe接口的测试代码:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>


#define _PEXIT \
    do {\
        char ebuf[64] = {0};\
        sprintf(ebuf, "%s: %d", __FILE__, __LINE__);\
        perror(ebuf);\
        exit(errno);\
    }while(0)


int main(void) {
    pid_t c1;
    int pipes[2];
    char *msg = "hello pipe";

    /* create a pipe */
    if (pipe(pipes))
        _PEXIT;

    /* fork */
    if ((c1=fork()) == -1)
        _PEXIT;
    else if (c1 == 0) {  // child
        char rbuf[32] = {0};
        close(pipes[1]);  // close write end
        if (read(pipes[0],rbuf,32-1) == -1)
            _PEXIT;
        close(pipes[0]);  // close read end
        printf("in child read: %s\n", rbuf);
        return 0;
    }

    // parent
    close(pipes[0]);  // close read end
    if (write(pipes[1],msg,strlen(msg)) == -1)
        _PEXIT;
    close(pipes[1]);  // close write end

    return 0;
}

通过fork创建子进程,子进程能够继承父进程的所有file descriptor:

The child inherits copies of the parent's set of open file descriptors. Each file descriptor in the child refers to the same open file description (see open(2)) as the corresponding file descriptor in the parent. This means that the two file descriptors share open file status flags, file offset, and signal-driven I/O attributes (see the description of F_SETOWN and F_SETSIG in fcntl(2)).

因此,上面的代码在创建子进程后,子进程就直接有了pipe对应的两个file descriptor。但是,一定要及时关闭不需要的fd,因为只有所有指向同一个pipe的write fd都close后,read才会收到最后的EOF。只有所有指向同一个pipe的read fd都close后,write才会返回错误,或收到SIGPIPE

这段测试代码只用了fork,但没有使用exec系列接口,即父子进程是同一个进程image,只是执行不同的if branch,这样的是OK的!

匿名管道pipe虽然被广泛使用,但也有其局限性:

  1. 匿名管道的最大特点就是要求进程之间具有同源性,即它们必须是最终由同一个进程所派生的子进程。(这个缺点可以通过命名管道解决)
  2. 匿名管道的半双工的工作方式,即只允许单向传输数据。(这一点匿名管道和命名管道是相同的,可以创建两个单向管道来实现数据的双向传输)

pipe,很多技术中都有管道的概念,是否基本逻辑都一样呢...

下面的测试代码,创建2个pipe,实现双向通信:

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


#define _PEXIT \
    do {\
        char ebuf[64] = {0};\
        sprintf(ebuf, "%s: %d", __FILE__, __LINE__);\
        perror(ebuf);\
        exit(errno);\
    }while(0)


int main(void) {
    pid_t c1;
    int pipes[2][2];

    /* create 2 pipe */
    if (pipe(pipes[0]))   // parent write, child read
        _PEXIT;
    if (pipe(pipes[1]))   // child write, parent read
        _PEXIT;

    /* fork */
    if ((c1=fork()) == -1)
        _PEXIT;
    else if (c1 == 0) {  // child
        char rbuf[32] = {0};
        /* child read */
        close(pipes[0][1]);  // close write end
        if (read(pipes[0][0],rbuf,32-1) == -1)
            _PEXIT;
        close(pipes[0][0]);  // close read end
        printf("in child read: %s\n", rbuf);
        /* child write */
        char *msg = "hi papa....";
        close(pipes[1][0]);
        if (write(pipes[1][1],msg,strlen(msg)) == -1)
            _PEXIT;
        close(pipes[1][1]);
        return 0;
    }

    /* parent write */
    close(pipes[0][0]);  // close read end
    char *msg = "hello child...";
    if (write(pipes[0][1],msg,strlen(msg)) == -1)
        _PEXIT;
    close(pipes[0][1]);  // close write end
    /* parent read */
    char rbuf[32] = {0};
    close(pipes[1][1]); 
    if (read(pipes[1][0],rbuf,32-1) == -1)
        _PEXIT;
    close(pipes[1][0]);  // close read end
    printf("parent read: %s\n", rbuf);

    wait(NULL);
    return 0;
}

运行效果:

$ gcc -Wall -Wextra test_pipe.c -o test_pipe
$ ./test_pipe 
in child read: hello child...
parent read: hi papa....

Linux下对fd的读写,默认都是blocking模式,pipe也一样。read时如果pipe空,block。write时如果pipe满,block。可以通过fcntl设置为non-blocking模式。

关于pipe更多详细的工作原理和资源限制,man 7 pipe

close管道的write end,会触发EOF,read end还能读,直到读到EOF。但是要注意,父子进程的write end都要close才有效果...

pipe机制是Linux系统的一个基础设施(从UNIX学来的),它使得小而美的设计哲学成为可能,即一大堆精致的小工具通过管道相互配合实现复杂多变的业务需求。

本文链接:https://cs.pynote.net/sf/linux/prog/202202051/

-- EOF --

-- MORE --