一、匿名管道

pipe的基本描述和用法

匿名管道是linux中的一种通信方式,为有血缘关系的进程提供数据通信。相关的api函数为:

#include <unistd.h>
int pipe(int pipefd[2]);

pipe函数传入一个长度为2的int数组,执行成功将在内核区域分配一块内存区域,并设置pipefd为管道的文件描述符。

创建子进程后,子进程也会继承这两个描述符,对于同一个文件描述符来说,父端写,子端就可以读,字段写,父端也可以读。

工作流程可以描述为:

但是由于管道内部实现是用的环形队列,因此父子不能同时对一个管道进行读操作或者写操作,队列是单向的,两端写就有问题。

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

#define SEND_BUFF "HelloWorld"

#define CLOSE_FD(fd) \
    do {    \
        if (fd != -1) { \
            close(fd);  \
            fd = -1;    \
        }   \
    } while (0)

int main(){
    int fd[2], ret, n;
    char buf[1025];

    ret = pipe(fd);
    if (ret == -1)
        goto _err;

    memset(buf, 0, 1025);
    pid_t pid = fork();

    if (pid == -1) {
        goto _err;
    } else if (pid == 0) {
        // 子端关闭fd1,打开fd0读
        CLOSE_FD(fd[1]);
        n = read(fd[0], buf, 1024);
        if (-1 == n) goto _err;
        printf("child read %d byte data: %s\n", n, buf);
        CLOSE_FD(fd[0]);
    } else {
        // 父端关闭fd0,打开fd1写
        CLOSE_FD(fd[0]);
        n = write(fd[1], SEND_BUFF, sizeof(SEND_BUFF));
        if (n == -1) goto _err;
        printf("parent send %d bytes data: %s\n", 
                sizeof(SEND_BUFF), SEND_BUFF);
        // 等待子进程退出
        wait(0);
        CLOSE_FD(fd[1]);
    }
    return 0;

_err:
    perror("Error");
    CLOSE_FD(fd[0]);
    CLOSE_FD(fd[1]);
    return -1;
}

1.2 非阻塞pipe

管道还有一个函数pipe2提供了更高级的选项,可以使得管道成为非阻塞读:

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <fcntl.h>              /* Obtain O_* constant definitions */
#include <unistd.h>

int pipe2(int pipefd[2], int flags);

其中,当flags置为O_NONBLOCK时,管道为非阻塞,从非阻塞的管道读取数据时,如果此时没有数据可读,将返回-1,并设置errno为EAGAIN

修改子进程部分代码为:

if (pid == 0){
    CLOSE_FD(fd[1]);
    while (1) {
        n = read(fd[0], buf, 1024);
        if (-1 == n) {
            if (errno == EAGAIN) {
                printf("READ AGAIN\n");
                // 没有数据可读时,休眠一秒,继续读
                sleep(1);
                continue;
            }
            goto _err;
        }
        printf("child read %d byte data: %s\n", n, buf);
        break;
    }
    CLOSE_FD(fd[0]);
}

父进程在写数据前先睡眠5秒:

else {
    CLOSE_FD(fd[0]);
    sleep(5);
    n = write(fd[1], SEND_BUFF, sizeof(SEND_BUFF));
    // ...
}

运行结果:

1.3 管道数据读取的原则

  1. 如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
  2. 如果有指向管道写端的文件描述符没关闭,而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
  3. 如果所有指向管道读端的文件描述符都关闭,这时有进程指向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
  4. 如果有指向管道读端的文件描述符没关闭,而持有管道写端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再write会阻塞,直到管道中有空位置了才写入数据并返回。

二、有名管道fifo

2.1 fifo的基本用法

fifo管道是linux文件系统中的一种文件类型,创建在磁盘上,任何进程都可以打开这个文件进行读写。

它可以解决无血缘关系进程的通信问题,创建管道的函数为:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

当管道文件被创建之后,就可像文件一样打开它进行读写:

// read.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<fcntl.h>
#include <errno.h>
#include<unistd.h>

#define FIFO_FILE "fifo"
#define BUFF_SIZE 1024

int main() {
    int ret, fd = -1;
    char buf[BUFF_SIZE] = { 0 };

    ret = mkfifo(FIFO_FILE, 0755);
    if (ret == -1 && errno != EEXIST)
        goto _err;

    fd = open(FIFO_FILE, O_RDONLY);
    if (fd == -1)
        goto _err;

    bzero(buf, BUFF_SIZE);
    ret = read(fd, buf, sizeof(buf) - 1);
    if (ret == -1)
        goto _err;

    printf("read: %s\n", buf);
    close(fd);
    fd = -1;
    
    return 0;
_err:
    perror("Error");
    if (fd > 0) {
        close(fd);
        fd = -1;
    }
    return -1;
}

写管道程序:

// write.c
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>

#define FIFO_FILE "fifo"

int main(){
    int ret, fd;

    fd = open(FIFO_FILE, O_WRONLY);
    if (fd == -1)
        goto _err;
        
    unlink(FIFO_FILE);

    ret = write(fd, "HelloWorld", 11);
    if (ret == -1)
        goto _err;

    close(fd);
    fd = -1;

    return 0;

_err:
    perror("Error");
    if (fd > 0) {
        close(fd);
        fd = -1;
    }
    return -1;
}

此时运行./read,会创建管道文件,新开一个窗口就可以看到,管道的颜色和普通文件不一样,并且ls的第一个字符是p

默认情况下,读是阻塞的(类似于读文件),直到有数据过来为止,所以./read执行后,会阻塞,直到运行./write之后才会收到数据。

read: HelloWorld

2.2 非阻塞pipe

非阻塞pipe有一个注意事项:当打开的管道没有其他进程也打开的话(即只有当前进程在读,没有其他进程了),直接返回0,不会返回EAGAGIN

// 加上O_NONBLOCK标记
fd = open(FIFO_FILE, O_RDONLY | O_NONBLOCK);
if (fd == -1)
    goto _err;

bzero(buf, BUFF_SIZE);
printf("fd = %d\n", fd);
while (1) {
    ret = read(fd, buf, sizeof(buf) - 1);
    if (ret == -1) {
        if (errno == EAGAIN) {
            printf("READ AGAIN\n");
            sleep(1);
            continue;
        }
        goto _err;
    } else if (ret == 0) {
        printf("read end\n");
        close(fd);
        fd = -1;
        break;
    } else {
        printf("read: %s\n", buf);
    }
}

编译后如果直接执行,程序会直接退出,因为此刻没有其他进程在写,不会返回EAGAIN

最后修改:2019 年 05 月 03 日
如果觉得我的文章对你有用,请随意赞赏