POSIX管道

参考:《UNIX网络编程 · 卷2 : 进程间通信》

PIPE(无名管道)

管道是最初的 Unix IPC 形式,其局限在于其没有名字,只能由有亲缘关系的进程使用。PIPE 使用常用的 read 和 write 函数访问。所有的 Unix 都提供无名管道,提供一个单向数据流(半双工),但是有些版本的 Unix 提供全双工管道。

#include <unistd.h>
int pipe(int fd[2]); //返回值:成功为0,出错为-1

如下展示了单个进程中管道的模样,从图中可以看到,fd[0] 用于读,称为读端,fd[1] 用于写,称为写端,如下图所示:
单个进程中管道
管道由单个进程创建,却很少在单个进程中使用。最经典的用途就是在父子进程中提供通信手段。父进程调用 fork 派生出一个自身副本,如下图:
父子进程中管道1
如果关闭父进程管道的读段,关闭子进程管道的写入端,就会在父子进程间提供了一个单向的数据流。
父子进程中管道2
如果我们需要一个双向的数据流时,必须创建两个管道,每个方向上一个。如下步骤:

  1. 创建管道1(fd[0] 和 fd[1])和管道2(fd[0] 和 fd[1])。
  2. 调用 fork 函数,创建子进程。
  3. 父进程关闭管道 1 的读端 fd1[0]
  4. 父进程关闭管道 2 的写端 fd2[1]
  5. 子进程关闭管道 1 的写端 fd1[1]
  6. 子进程关闭管道 2 的读端 fd2[0]

那么最总的管道布局图如下:
父子进程中管道3
那么,我们可以通过这种模型,实现父进程和子进程之间的通信的典型实例,主要功能是:客户端输入一个路径名,把它写入 IPC 通道,服务器从该通道读出路径名,并尝试打开文件读取,读取成功并写入IPC通道,否则响应一个出错消息。客户端再将消息打印出来,实例代码如下:

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

#define MAXLINE 128

void client(int readfd, int writefd)
{
    size_t len;
    ssize_t n;
    char buff[MAXLINE];
    fgets(buff, MAXLINE, stdin);
    len = strlen(buff);
    if (buff[len - 1] == '\n')
        len--;
    write(writefd, buff, len);
    while ((n = read(readfd, buff, MAXLINE)) > 0) //阻塞读
    {
        write(STDOUT_FILENO, buff, n);
    }
}

void server(int readfd, int writefd)
{
    int fd;
    ssize_t n;
    char buff[MAXLINE + 1];
    if ((n = read(readfd, buff, MAXLINE)) == 0)
    {
        printf("EOF read pathname");
        exit(1);
    }
    buff[n] = '\0';
    if ((fd = open(buff, O_RDONLY)) < 0) //出错
    {
        snprintf(buff + n, sizeof(buff) - n, ":cant't open\n");
        n = strlen(buff);
        write(writefd, buff, n);
    }
    else
    {
        while ((n = read(fd, buff, MAXLINE)) > 0)
        {
            write(writefd, buff, n);
        }
        close(fd);
    }
}

int main()
{
    int pipe1[2];
    int pipe2[2];
    pid_t childpid;
    //TODO:返回值判断
    pipe(pipe1);
    pipe(pipe2);
    if ((childpid = fork()) == 0) //子进程
    {
        close(pipe1[1]);
        close(pipe2[0]);
        server(pipe1[0], pipe2[1]); //充当服务器
        exit(0);
    }
    //父进程
    close(pipe1[0]);
    close(pipe2[1]);
    client(pipe2[0], pipe1[1]); //充当客户端

    waitpid(childpid, NULL, 0);

    exit(0);
}

shell 中的管道原理

在上面已经对管道做了详细解释,那么 shell 中的管道是如何运作的呢?
假如在 shell 中输入一个像下面这种命令:

$ who | sort | lp

该 shell 将执行上述步骤创建三个进程和其间的两个管道。还把每个管道的读出端复制到相应的进程的标准输入,把每个管道的写入端复制到相应进程的标准输出,就有了如下这样的管道线:
shell中的管道原理

全双工管道

上面提到,某些 Unix 的 pipe 是提供全双工的,除此之外,许多内核都提供的 socketpair 函数也是全双工的。那么全双工管道到底提供什么?
首先看一下半双工管道:
半双工管道
再看全双工管道:
全双工管道
这里的全双工管道是由两个半双工管道构成的。写入 fd[1] 的数据只能从 fd[0] 读出,写 入 fd[0] 的数据只能从 fd[1] 读出。
可使用单个全双工管道完成双向通信,如下程序:

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

int main()
{
    int fd[2];
    int n;
    char c;
    pid_t childpid;
    pipe(fd);
    if ((childpid = fork()) == 0) //子进程
    {
        sleep(1);
        if ((n = read(fd[0], &c, 1)) != 1)
        {
            printf("child:read returned %d", n);
            exit(1);
        }

        printf("child read:%c\n", c);
        write(fd[0], "c", 1);
        exit(0);
    }
    //父进程
    write(fd[1], "p", 1);
    sleep(2);
    if ((n = read(fd[1], &c, 1)) != 1)
    {
        printf("parent:read returned %d", n);
        exit(1);
    }
    printf("parent read:%c\n", c);
    waitpid(childpid, NULL, 0);

    exit(0);
}

在某些 Unix 如 Solaris 2.6 上是全双工的,这份代码可以运行。
然而,遗憾的是,这份代码并不能在 Linux 上正常运行,因为 Linux 上的 pipe 只是半双工的。父进程试图在 fd[1] 端 read 会中止,子进程在 fd[0] 端 write 时中止,并出现错误。

popen/pclode 函数

popen 主要用于创建一个管道并启动另外一个进程,这个进程要么从该管道读出标准输入,要么从该管道写入标准输出。
pclose 主要用于关闭由 popen 创建的标准 I/O 流,等待其中的命令终止,然后返回 shell 的终止状态。

#include <stdio.h>
FILE *popen(const char *command, const char *type); //返回值:成功文件指针,出错NULL
int pclose(FILE *stream); //返回成功则为 shell 终止状态,出错为-1

参数:
command:是一个 shell 命令。popen 在调用进程和所指定的命令之间建立一个管道,由 popen 返回的值是一个标准 I/O FILE 指针,该指针或者用于输入,或者用于输出,取决于 type。
type:如果为 r,调用进程读取 command 的标准输出;如果是 w,调用进程写到 command 的标准输入。
如下使用 popen 函数和 shell 的 cat 命令实例,cat 命令的输出被复制到标准输出。

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

#define MAXLINE 128

int main()
{
    size_t n;
    char buff[MAXLINE];
    char command[MAXLINE];
    FILE *fp;
    fgets(buff, MAXLINE, stdin); //read path name
    n = strlen(buff);
    if (buff[n - 1] == '\n')
        n--;
    snprintf(command, sizeof(command), "cat %s", buff);
    fp = popen(command, "r");
    while (fgets(buff, MAXLINE, fp) != NULL)
    {
        fputs(buff, stdout);
    }
    pclose(fp);
    exit(0);
}

在 Linux 下运行结果:

 ./test 
/etc/shadow
cat: /etc/shadow: Permission denied

虽然得到一个出错的信息,但是调用是成功的,因为 cat 将出错消息写到标准错误输出,调用进程读取到该输出,并打印出来。

FIFO(有名管道)

上面说到 pipe 没有名字,无法用于无亲缘关系的两个进程之间。而 FIFO 类似于管道,其全称 First in first out,是一个单向(半双工)数据流,但是每个 FIFO 都有一个路径名与之关联,也就支持了无亲缘关系的进程访问同一个 FIFO,而 FIFO 也随之被称为又名管道。
FIFO 由 makfifo 函数创建:

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

int mkfifo(const char *pathname, mode_t mode); //返回值:成功0,出错-1

参数:
pathname:一个普通的 Unix 路径名,作为该 FIFO 的名字。
mode:指定文件权限位,类似于 open 的第二个参数,在 IPC 概念中已经提到过。FIFO 函数已经默认指定 O_CREAT | O_EXCL。所以如果创建一个所指定名字的 FIFO 已存在,返回一个 EEXIST 的错误。如果不希望创建一个新的 FIFO,就改为调用 open 而不是 mkfifo。
所以正确的做法就是:要打开一个已存在的 FIFO 或 创建一个新的 FIFO,应先调用 mkfifo,再检查它是否返回 EEXIST 错误,若返回该错误则改为调用 open。
我们也可以从 shell 命令创建 FIFO,使用 mkfifo 命令即可。
创建出一个 FIFO 后,必须打开来读或者写,所用的可以是 open 函数或者标准 I/O 函数,如 fopen。FIFO 不能打开来即读又写,因为它是半双工的。
对 pipe 或 FIFO 的 write 总是向末尾添加数据,read 总是从头返回数据。如果对 pipe 或 FIFO 调用 lseek,将返回 ESPIPE 错误。
现在重新使用 FIFO 编写上面的客户端-服务器的程序。使用两个 FIFO代替两个 PIPE。

#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/wait.h>

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define FIFO1 "/tmp/fifo.1"
#define FIFO2 "/tmp/fifo.2"
#define MAXLINE 128

void client(int readfd, int writefd)
{
    size_t len;
    ssize_t n;
    char buff[MAXLINE];
    fgets(buff, MAXLINE, stdin);	//读取输入的文件名
    len = strlen(buff);
    if (buff[len - 1] == '\n')
        len--;
    write(writefd, buff, len);	//写入到管道中
    while ((n = read(readfd, buff, MAXLINE)) > 0) //从管道中读
    {
        write(STDOUT_FILENO, buff, n);
    }
}

void server(int readfd, int writefd)
{
    int fd;
    ssize_t n;
    char buff[MAXLINE + 1];
    if ((n = read(readfd, buff, MAXLINE)) == 0) //服务器读取客户端管道中发来的文件名
    {
        printf("EOF read pathname");
        exit(1);
    }
    buff[n] = '\0';
    if ((fd = open(buff, O_RDONLY)) < 0) //打开文件
    {
        snprintf(buff + n, sizeof(buff) - n, ":cant't open\n");
        n = strlen(buff);
        write(writefd, buff, n);
    }
    else
    {
        while ((n = read(fd, buff, MAXLINE)) > 0)
        {
            write(writefd, buff, n);	//读取内容写入管道中
        }
        close(fd);
    }
}

int main()
{
    int readfd, writefd;
    pid_t childpid;
    if ((mkfifo(FIFO1, FILE_MODE) < 0) && (errno != EEXIST))
        printf("can't create %s\n", FIFO1);
    if ((mkfifo(FIFO2, FILE_MODE) < 0) && (errno != EEXIST))
    {
        unlink(FIFO1);
        printf("can't create %s\n", FIFO2);
    }

    if ((childpid = fork()) == 0) //子进程
    {
        readfd = open(FIFO1, O_RDONLY, 0);
        writefd = open(FIFO2, O_WRONLY, 0);

        server(readfd, writefd);
        exit(0);
    }
    //父进程
    writefd = open(FIFO1, O_WRONLY, 0);
    readfd = open(FIFO2, O_RDONLY, 0);

    client(readfd, writefd);

    waitpid(childpid, NULL, 0);

    close(readfd);
    close(writefd);

    unlink(FIFO1);
    unlink(FIFO2);
    exit(0);
}

需要注意的是:创建打开一个 FIFO 则需要在调用 mkfifo 后再调用 open。管道在所有进程都关闭它之后自动消息。FIFO 的名字只有通过调用 unlink 才能从文件系统中删除。这样的好处就是:FIFO 在文件系统中有一个名字,该名字允许某个进程创建一个 FIFO,与它无关的另一个进程来打开这个 FIFO。
有名管道FIFO
如果没有正确的使用 FIFO 会发生问题。比如我们将上面的父进程中两个 open 调用顺序互换,程序就不能工作,因为当没有任何进程打开某个 FIFO 来写,那么打开该 FIFO 来读的进程将阻塞。父子进程都将打开一个 FIFO 来读,但是当时并没有任何进程已打开该文件来写,父子进程将阻塞,形成死锁。
因为 FIFO 的 open 使用 O_RDONLY 标志将阻塞到另一个只写 O_WRONLY 标志打开该 FIFO 为止。
再来看一个无亲缘关系的进程的例子:
客户端:

#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/wait.h>

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define FIFO1 "/tmp/fifo.1"
#define FIFO2 "/tmp/fifo.2"
#define MAXLINE 128

void client(int readfd, int writefd)
{
    size_t len;
    ssize_t n;
    char buff[MAXLINE];
    fgets(buff, MAXLINE, stdin);
    len = strlen(buff);
    if (buff[len - 1] == '\n')
        len--;
    write(writefd, buff, len);
    while ((n = read(readfd, buff, MAXLINE)) > 0) //阻塞读
    {
        write(STDOUT_FILENO, buff, n);
    }
}

int main()
{
    int readfd, writefd;
    writefd = open(FIFO1, O_WRONLY, 0);
    readfd = open(FIFO2, O_RDONLY, 0);

    client(readfd, writefd);

    close(readfd);
    close(writefd);

    unlink(FIFO1);
    unlink(FIFO2);

    return 0;
}

服务器:

#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/wait.h>

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define FIFO1 "/tmp/fifo.1"
#define FIFO2 "/tmp/fifo.2"
#define MAXLINE 128

void server(int readfd, int writefd)
{
    int fd;
    ssize_t n;
    char buff[MAXLINE + 1];
    if ((n = read(readfd, buff, MAXLINE)) == 0)
    {
        printf("EOF read pathname");
        exit(1);
    }
    buff[n] = '\0';
    if ((fd = open(buff, O_RDONLY)) < 0) //出错
    {
        snprintf(buff + n, sizeof(buff) - n, ":cant't open\n");
        n = strlen(buff);
        write(writefd, buff, n);
    }
    else
    {
        while ((n = read(fd, buff, MAXLINE)) > 0)
        {
            write(writefd, buff, n);
        }
        close(fd);
    }
}

int main()
{
    int readfd, writefd;
    if ((mkfifo(FIFO1, FILE_MODE) < 0) && (errno != EEXIST))
        printf("can't create %s\n", FIFO1);
    if ((mkfifo(FIFO2, FILE_MODE) < 0) && (errno != EEXIST))
    {
        unlink(FIFO1);
        printf("can't create %s\n", FIFO2);
    }

    readfd = open(FIFO1, O_RDONLY, 0);
    writefd = open(FIFO2, O_WRONLY, 0);

    server(readfd, writefd);

    return 0;
}

PIPE 和 FIFO 的非阻塞属性

PIPE 和 FIFO 的打开、读入和写入还有一些属性,比如将管道设置为非阻塞。
(1)调用 open 时可指定 O_NONBLOCK 标志。如下:

writefd = open(FIFO1, O_WRONLY | O_NONBLOCK, 0);

(2)若描述符已经打开,可以调用 fcntl 以启用 O_NONBLOCK 标志,在 PIPE 中来说,必须使用这种技术,因为其没有 open 调用。在使用 fcntl 时,先使用 F_GETFL 命令取得当前文件状态标志,将它和 O_NONBLOCK 标志按位或后,再使用 F_SETFL 命令存储这些文件标志。

int flags;
if ((flags = fcntl(fd, F_GETFL, 0)) < 0)
	err_sys("F_GETFL error.");
flags |= O_NNONBLOCK;
if (fcntl(fd, F_SETFL, flags) < 0)
    err_sys("F_SETFL error");

非阻塞标志对管道 PIPE 和 FIFO 的影响:
非阻塞标志对管道PIPE和FIFO的影响
write 操作是原子性的,若有两个进程差不多同时向一个 PIPE 或者 FIFO 写,那么要么先写入来自第一个进程的所有数据,再写第二个进程的所有数据,要么颠倒,系统不会混杂写。O_NONBLOCK 标志对对于其原子性没有影响。
如果向一个没有为读打开着的 PIPE 或者 FIFO 写入,那么内核将产生一个 SIGPIPE 信号,若进程没有捕捉也没有忽略,默认行为将是终止进程。

PIPE 和 FIFO 的限制

系统加于 PIPE 和 FIFO 的唯一限制为:
OPEN_MAX 一个进程在任意时刻打开的最大描述符数(Posix 要求至少为 16)
PIPE_BUF 可原子地写往一个 PIEP 或 FIFO 的最大数据量(Posix 要求至少为512)。
OPEN_MAX 的值可通过调用 sysconf 函数查询。它通常可通过执行 ulimit 命令或 limit 命令从 shell 中修改。也可通过调用 setrlimit 函数从一个进程中修改。
PIPE_BUF 的值通常定义在头文件中,但是 Posix 认为它是一个路径名变量。意味着它的值可以随所指定的路径名而变化(只对 FIFO 而言, 因为 PIPE 没有名字),因为不同的路径名可以落在不同的文件系统上,而这些文件系统可能有不同的特征。于是 PIPE_BUF 的值可在运行时通过调用 pathconf 或 fpathconf 取得。如下实例:

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        printf("usage: pipeconf <pathname>");
        exit(1);
    }

    printf("PIPE_BUF = %ld, OPEN_MAX = %ld\n",
           pathconf(argv[1], _PC_PIPE_BUF), sysconf(_SC_OPEN_MAX));
    exit(0);
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值