轶想世界 发表于 2023-1-24 19:34:04

linux环境编程(2): 使用pipe完成进程间通信

1. 写在前面

linux系统内核为上层应用程序提供了多种进程间通信(IPC)的手段,适用于不同的场景,有些解决进程间数据传递的问题,另一些则解决进程间的同步问题。对于同样一种IPC机制,又有不同的API供应用程序使用,目前有POSIX IPC以及System V IPC可以为应用程序提供服务。后续的系列文章将逐一介绍消息队列,共享内存,信号量,socket,fifo等进程间通信方法,本篇文章主要总结了管道相关系统调用的使用方式。文中代码可以在这个代码仓库中获取,代码中使用了我自己实现的一个单元测试框架,对测试框架感兴趣的同学可以参考上一篇文章。
2. pipe介绍

在linux环境进行日常开发时,管道是一种经常用到的进程间通信方法。在shell环境下,'|'就是连接两个进程的管道,它可以把一个进程的标准输出通过管道写到另一个进程的标准输入,利用管道以及重定向,各种命令行工具经过组合之后可以实现一个及其复杂的功能,这也是继承自UNIX的一种编程哲学。
除了在shell脚本中使用管道,另一种方式是通过系统调用去操作管道。使用pipe或者pipe2创建管道,得到两个文件描述符,分别是管道的读端和写端,有了文件描述符,进程就可以像读写普通文件一样对管道进行read和write操作,操作完成之后调用close关闭管道的两个文件描述符即可。可以看到,当完成创建之后,管道的使用和普通文件相比没有什么区别。
管道有两个特点: 1) 通常只能在有亲缘关系的进程之间进行通信; 2) 阅后即焚;有亲缘关系是指,通信的两个进程可以是父子进程或者兄弟进程,这里的父子和兄弟是一个广义的概念, 子进程可以是父进程调用了多次fork创建出来的,而不仅局限在只经过一次fork,总之,只要通信双方的进程拿到了管道的文件描述符就可以使用管道了。说”阅后即焚“是因为管道中的数据在被进程读取之后就会被管道清除掉。有一个形象的比喻说,管道就像某个进程家族各个成员之间传递情报的中转站,情报内容阅后即焚。
3. pipe的基本使用

在使用管道时,需要注意管道中数据的流动方向,通常都是把管道作为一个单向的数据通道使用的。虽然通信双方可以都持有管道的读端和写端,然后使用同一个管道实现双向通信,但这种方式实际上很少使用。下面通过几段代码说明几种使用管道的方法:
3.1 自言自语

管道虽然时进程间通信的一种手段,但一个进程自言自语也是可以的,内核并没有限制管道的两端必须由不同的进程操作。下面的代码展示了一个孤独的进程怎样通过管道自言自语,代码中使用了自己实现的测试框架cutest。执行之后它将从管道的另一头收到前一个时刻发给自己的消息。
CUTEST_CASE(basic_pipe, talking_to_myself) {
    int pipefd;
    pipe(pipefd);

    const char *msg = "I'm talking to myself";
    write(pipefd, msg, strlen(msg));

    char buf;
    read(pipefd, buf, 32);
    printf("talking_to_myself: %s\n", buf);

    close(pipefd);
    close(pipefd);
} 3.2 父进程向子进程传递数据

自言自语始终是太过无聊,是时候让父子进程之间聊点什么了。因为fork之后的子进程会继承父进程的文件描述符,fork之前父进程向管道写入的数据,子进程可以在管道的另一端读到。
CUTEST_CASE(basic_pipe, parent2child) {
    int pipefd;
    pipe(pipefd);

    const char *msg = "parent write, child read";
    write(pipefd, msg, strlen(msg));

    if (fork() == 0) {
      close(pipefd);
      char buf;
      memset(buf, 0, 64);
      read(pipefd, buf, 64);
      printf("parent2child: %s\n", buf);
      exit(0);
    }

    close(pipefd);
    close(pipefd);
} 3.2 自进程向父进程传递数据

管道的方向是由通信双方操作的文件描述符决定的,子进程同样可以传递消息给父进程。
CUTEST_CASE(basic_pipe, child2parent) {
    int pipefd;
    pipe(pipefd);

    if (fork() == 0) {
      close(pipefd);
      const char *msg = "parent read, child write";
      write(pipefd, msg, strlen(msg));
      close(pipefd);
      exit(0);
    }

    close(pipefd);

    char buf;
    memset(buf, 0, 64);
    read(pipefd, buf, 64);
    printf("child2parent: %s\n", buf);
    close(pipefd);
} 3.3 父进程向多个子进程传递数据

当有多个子进程时,只要它们持有了管道的文件描述符,就可以利用管道通信,把父进程写进管道的数据读取出来。当然,在具体的应用中需要考虑子进程的读取顺序等因素,下面的例子只是简单的创建了多个子进程,每个进程读取一个int类型的数据,开始阶段由父进程向管道写入数据,需要说明一点,三个子进程并没有将管道内的数据都读完,当所有引用了这个管道的文件描述符都关闭了之后,内核也会在适当的时机销毁自己维护的管道。
void fork_child_read(int id, int pipefd, const char *msg_pregix) {   if (fork() == 0) {         close(pipefd);         int n;         read(pipefd, &n, sizeof(int));         printf("%s: child %d get data %d\n", msg_pregix, id, n);         close(pipefd);         exit(0);   } }CUTEST_CASE(basic_pipe, parent2children) {   int pipefd;   pipe(pipefd);      for (int i = 1; i
页: [1]
查看完整版本: linux环境编程(2): 使用pipe完成进程间通信