进程间通信(IPC) --- 管道

什么是管道?

       管道是 UNIX 系统中最古老的 IPC 形式,所有的 UNIX 系统都提供此种通信机制,我们把从一个进程连接到另一个进程的数据流称为一个 " 管道 "。管道有一下两种局限性:

  1. 历史上,管道是半双工的(即数据只能在一个方向上流动)。现在,某些系统提供全双工管道,但是为了最佳的可移植性,我们绝不应预先嘉定系统支持全双工管道。
  2. 管道只能在具有公共祖先的两个进程之间使用。通常,一个管道由一个进程创建,再进程调用 fork 之后,这个管道就能在父子进程之间使用了。

       尽管有这两种局限性,半双工管道仍是最常见的 IPC 形式。但第二种局限性只是针对匿名管道来说的,对于命名管道 (FIFO) 是没有这个局限性的。

匿名管道

       匿名管道实际上是内核中的一段内存,再加上 linux 中一切皆文件的思想,匿名管道可以看做是一个特殊的文件,我们也是通过一对文件描述符来操作管道对应的内存。

       前面我们说了,匿名管道只能在具有亲缘关系的进程中使用,这是因为匿名管道是没有自己唯一的标识符的。在父进程中创建一个管道,父进程就有了可以操作这个管道的文件描述符,当子进程创建时拷贝了父进程的地址空间,当然也就包括了这个管道的文件描述符,既然父子进程都可以对这个管道进行操作,那么当然可以通过这个管道进行通信。

       让我们通过代码来看看管道的使用

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

int main()
{
    int pipe_fd[2];
    if(pipe(pipe_fd) == -1) {    // 管道的创建一定要在 fork 之前
        perror("create pipe failed");
        return 1;
    }

    pid_t pid = fork();
    if(pid == -1) {
        perror("fork failed");
        return 1;
    } else if(pid == 0) {
        close(pipe_fd[0]);   // 子进程关闭读端
        char buf[100] = "hello pipe!";
        write(pipe_fd[1], buf, strlen(buf));
        close(pipe_fd[1]);
        exit(0);
    }
    close(pipe_fd[1]);      // 父进程关闭写端
    char buf[100] = {0};
    read(pipe_fd[0], buf, 100);
    printf("Child say: %s\n", buf);
    close(pipe_fd[0]);

    return 0;
}

       可以看到结果:父进程读到了子进程写入管道的数据:

匿名管道的读写规则:

  • 当没有数据可读时:
  •         O_NONBLOCK disable:read 调用阻塞,即进程暂停执行,一直等到有数据到来为止
  •         O_NONBLOCK enable:read 调用返回 -1,errno 值为 EAGAIN
  • 当管道满时:
  •         O_NONBLOCK disable:write 调用阻塞,直到有进程读走数据
  •         O_NONBLOCK enable:调用返回 -1,errno 值为 EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则 read 返回 0
  • 如果所有管道读端对应的文件描述符被关闭,则 write 操作会产生 SIGPIPE,进而可能导致 write 进程退出
  • 当要写入的数据量不大于 PIPE_BUF 时,linux 将保证写入的原子性
  • 当要写入的数据量大于 PIPE_BUF 时,linux 将不再保证写入的原子性

       正是因为 linux 保证了写操作的原子性,当多个进程同时想要想管道中写入时,才不会导致数据的混乱。下图为多个进程对同一管道进行操作的示意图:

匿名管道的特点

  • 只能用于具有亲缘关系的进程之间进行通信;通常,一个管道由一个进程创建,然后该进程调用 fork,此后父子进程之间就可以使用该管道
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道对的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥(写满了就不写了,读完了就不读了)
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

命名管道(FIFO)

       匿名管道只能在具有亲缘关系的进程之间通信,但大多数情况下,我们需要在两个毫无关系的进程之间进行通信,这个时候我们可以使用 FIFO 文件来做这项工作,它经常被称为命名管道。

       命名管道是一种特殊的文件,和匿名管道不同的是,匿名管道只能说是看做是一种特殊的文件,但它实际上并不是文件,我们也看不到它;而命名管道确确实实是一个我们可以看到的文件,当我们创建一个 FIFO 文件时,就会在文件夹中出现一个 FIFO 文件。

命名管道的创建

       因为命名管道是一种特殊的文件,因此它是可以从命令行上创建的,使用下面的命令可以创建一个命名管道

$ mkfifo filename

       当然命名管道也是可以从程序里创建的,函数原型为

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

       下面让我们来创建一个命名管道

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

int main()
{
    mkfifo("myfifo", 0644);
    return 0;
}

       运行程序后可以看到,文件夹里多了一个名为 "myfifo" 的文件,并且模式最左边的一位 "p" 表示这是一个命名管道。

命名管道的打开规则

  • 如果当前操作是为读而打开 FIFO 时
  •         O_NONBLOCK disable:阻塞直到有相应进程为写而打开该 FIFO
  •         O_NONBLOCK enable:立刻返回成功
  • 如果当前操作是为写而打开 FIFO 时
  •         O_NONBLOCK disable:阻塞直到有相应进程为读而打开该 FIFO
  •         O_NONBLOCK enable:立刻返回失败,错误码为 ENXIO

       因为命名函数是一种文件,因此除创建外,其他的使用规则同普通文件的操作规则是完全一样的。下面我们用 FIFO 来实现一个简单的 server&client 通信:

// server.c

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

int main()
{
    // 创建一个名为 chat 的命名管道
    if(mkfifo("chat", 0644) == -1) {
        perror("mkfifo failed");
        return 1;
    }

    // 服务端以只读方式打开
    int fd = open("chat", O_RDONLY);
    if(fd == -1) {
        perror("open failed");
        return 1;
    }

    while(1) {
       char buf[1024] = {0};
       ssize_t n = read(fd, buf, sizeof(buf));
       if(n == -1) {
           perror("read failed");
           return 1;
       } else if(n == 0) {
           continue;
       }
       buf[n] = 0;
       printf("client say: %s\n", buf);
    }

    close(fd);
    return 0;
}

// client.c

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

int main()
{
    // 服务端已创建好命名管道,客户端以只写方式打开
    int fd = open("chat", O_WRONLY);
    if(fd == -1) {
        perror("open failed");
        return 1;
    }
    while(1) {
        char buf[1024] = {0};
        ssize_t n = read(0, buf, sizeof(buf));
        if(n == -1) {
            perror("read failed");
            return 1;
        }
        buf[n-1] = 0;
        write(fd, buf, strlen(buf));
    }
    close(fd);
    return 0;
}

       完成后可以看到,当客户端发送一条消息服务端就可以接收到

匿名管道与命名管道的区别

  1. 匿名管道由 pipe 函数创建并打开
  2. 命名管道由 mkfifo 函数创建,由 open 函数打开
  3. 匿名管道只能用于具有亲缘关系的进程间通信,而命名管道可用于任意两个进程间的通信
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值