简 述: 对于有血缘关系的进程间通信,如父子进程、兄弟子进程子之间的通信,可以采用(匿名)管道的 pipe 方式。 而进程间通信一共有四种方式:管道、信号、共享映射区、套接字。且说一个概念,进程间通信(Inter Process Communication),字母首写即为 IPC。

[TOC]


本文初发于 “偕臧的小站“,同步转载于此。


编程环境:

  💻: uos20 📎 gcc/g++ 8.3 📎 gdb8.0

  💻: MacOS 10.14 📎 gcc/g++ 9.2 📎 gdb8.3


进程间通信 IPC:

进程间通信(Inter Process Communication),字母首写即为 IPC。

  • 进程间常用的 4 种方式:
    • 管道 (简单)
    • 信号 (系统开销小)
    • 共享映射区 (有无血缘关系的进程间通信都可以)
    • 本地 socket 套接字 (稳定)

匿名管道(pipe):

int pipe(int fildes[2]);

一般都是指创建匿名管道,其中传出 int fildes[2] 参数是固定的;fildes[0] 代表读端,fildes[1] 代表写端。适用于有血缘关系的进程。通常父子进程之间是不要使用 sleep() 函数的,因为管道默认就是堵塞的。虽然实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上,它占用的是内存空间。所以,Linux上的管道就是一个操作方式为文件的内存缓冲区。


  • 本质:
    • 是内核缓冲区。也是伪文件(不占用磁盘空间)
  • 特点:
    • 其读端和写端,对应两个文件描述符;数据写端流入,读端流出
    • 操作管道的进程被销毁之后,管道自动被释放了
    • 管道默认是阻塞的(读写端)
  • 实现原理:
    • 内部实现方式:环形队列。只有一次读写的机会。先进先出。
    • 其缓冲区默认是 4K 大小;但是大小会随着实际情况适当 调整。
  • 局限性:
    • 队列:数据只能够读取一次,不能够重复读取
    • 半全工方式工作

父子进程间通信:

  • 写一个父子进程间,使用管道通信的例子:

    比如说,写一个例子,实现父子进程间的通信,实现 ps aux | grep bash 的实现。你可以看下面的思路图解析,然后自己尝试敲写一遍,没有出来的话,再来看我的。学习之事,不能急于求成,始终是那句话:纸上学来终觉浅,觉知此事要躬行。 当你开始写下第一行代码的时候,你会感受到创造的快乐。

  • 代码实现:

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

int main(int argc, char *argv[])
{
    int fd[2];   //用来标记管道的两端的文件描述符
    int ret = pipe(fd);  //创建管道 pipe. fd[2] 是固定的输出参数

    if (ret == -1) {
        perror("[pipe create file] ");
        return 0;
    }
        
    int pipeRead = fd[0];   
    int pipeWrite = fd[1];

    pid_t pid = fork();

    if (pid > 0) {                         //parent process
        dup2(pipeWrite, STDOUT_FILENO);    //重定向
        close(pipeRead);
        execlp("ps", "ps", "aux", NULL);
    } else if (pid == 0) {                 //child process
        dup2(pipeRead, STDIN_FILENO);      //重定向
        close(pipeWrite);
        execlp("grep", "grep", "bash", "--color=auto", NULL);
    }
    
    return 0;
}
  • 代码分析:

    • 父进程的 3 号文件描述符表,指向 pipe 管道的写端,要记得此刻把它的读端关闭

    • 子进程的 4 号文件描述符表,指向 pipe 管道的读端,要记得此刻把它的写端关闭

    • 因为管道只能够一次机会读写机会。如果要父子进程都能够读写,那么还得加一个管道

    • 管道是默认是阻塞的

    • 其中对代码进程中的 20-28行的理解,和文件描述表中的文件描述符 0-4号 的对应关系如下:

  • 运行结果:

    • 在 Ubuntu 18.4 上面的效果如图:

    • 在 Mac 10.14.6 上面效果(后面那一串多余的,都是它自带的一些命令参数)

      • 当你故意开多个终端的时候,也会把其他的终端搜索出来

    • 由上面是可以看到 在 Linux 和 Unix 系统之间是有着一些区别的,第1、2 张截图,其进程的所有者,Linux 中两个都是 muli 用户;而 Unix 一个是 root 系统 ,一个是 muli 用户。但是这里有一段困惑,怎么 Unix 中显示 root 的那个进程, 后面是有显示为我的一个虚拟机软件y?怎么和他扯上关系的???这里暂时有一个小的困惑。在下一个例子中,会看到更有其他的不同。

兄弟子进程间通信:

这里实现两个兄弟子进程(无孙子进程)之间,利用管道进程 pipe 进程通信的例子

  • 先上代码: 这是由上面一个例子改写的,思路可以参考前面那个图

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main(int argc, char *argv[])
    {
        int fd[2];   //用来标记管道的两端的文件描述符
        int ret = pipe(fd);  //创建管道 pipe. fd[2] 是固定的输出参数
    
        if (ret == -1) {
            perror("[pipe create file] ");
            return 0;
        }
            
        int pipeRead = fd[0];   
        int pipeWrite = fd[1];
    
        int i = 0;
        for ( ; i < 2; i++) {
            pid_t pid = fork();
            
            if (pid == 0)
                break;
    
            if (pid == -1)
                perror("[creator process file:]");
    
        }
    
        if (i == 0) {                         //child process 1
            dup2(pipeWrite, STDOUT_FILENO);   
            close(pipeRead);
            execlp("ps", "ps", "aux", NULL);
        } else if (i == 1) {                 //child process 2
            dup2(pipeRead, STDIN_FILENO);    
            close(pipeWrite);
            execlp("grep", "grep", "bash", "--color=auto", NULL);
        } else if (i == 2) {                 //parent process
            close(pipeWrite);
            close(pipeRead);
    
            // sleep(2);
            int wpid;
            while ( wpid = waitpid(-1, NULL, WNOHANG) != -1) {  //回收子进程
                if (wpid == 1) ///sbin/init splash 进程    /sbin/launchd
                    continue;
    
                if (wpid == 0) 
                    continue;
    
                printf("child dide pid = %d\n", wpid);
            }    
        }
        
        printf("pipeWrite = %d,  pipeRead = %d\n", pipeWrite, pipeRead);
        return 0;
    }
  • 附上该例子的思路图:

    • 要注意关闭父进程和不必要的进程的对应对管道的读写的文件描述符。且注意到管道默认是阻塞的。
  • 运行效果:

    • 在 Ubuntu 18.4 中的效果:

    • 在 Mac 10.14。6 中的效果:

  • 运行结果以及代码分析:

    • 在 Linux 和 Unix 下,均可看到,是 三行打印输出的结果
      • 第一行是查询到 子进程 1 的信息,往管道里面写入 ps aux 的那个
      • 第二行是查询到 子进程 2 的信息,往管道里面写入 grep “bash” --color=auto 的那个
      • 第三行是父进程,打印的输出信息。
    • 思考,如果去掉代码42,45,46 行,会发生什么???会发现在 Linux 和 Unix 上面,都会答应批量的 child dide pid = 1 信息。查看了一下,该进程号 pid == 1,在 Linux 下是 /sbin/init splash ;在 Unix 下是 /sbin/launchd 进程。 也就是书上和一些博客说的 init 进程,专门用来领养孤儿进程的,用来释放 子(孤儿)进程的 系统空间的 PCB

管道的读写行为:

此部分为理解,但是归总结纳一下:

读操作:

  • 有数据:
    • read(fd), -正常读,返回读出的字节数
  • 无数据:
    • 写端全部关闭
      • read 解除阻塞,返回 0
      • 相当于读文件到了尾部
    • 没有全部关闭
      • read 阻塞(比如设置了一个 sleep(10))

写操作:

  • 读端全部关闭:
    • 管道破裂,进程被终止
      • 内核给当前进程发送 SIGPIPE 信号(13 号信号)
  • 读端没有全部关闭
    • 缓冲区写满了
      • write 阻塞
    • 缓冲区没有写满
      • write 继续写

查看管道缓冲区大小:

  • 命令: ulimix -a 现在在 Linux 和 Unix 上面,都没有 pipe size 这一栏详细信息了

  • 函数: fpathconf()

    • 一个例子:

      int fd[2];   //用来标记管道的两端的文件描述符
      int ret = pipe(fd);  //创建管道 pipe. fd[2] 是固定的输出参数
      
      int pipeSize = fpathconf(fd[0], _PC_PIPE_BUF);  //fd[1] 也行
      printf("pipeSize = %d\n", pipeSize);
    • 输出结果:Unix 下是 512 大小


设置管道的非阻塞属性:

  • 默认管道是读写两端都是阻塞

  • fcntl() –变参函数

    • 作用一:复制文件描述符 -dup
    • 作用二:修改文件属性 -open 的时候对应 flag 属性
  • 例如实现读端为非阻塞 pipe(fd)

    int flags = fcntl(fd[0], F_GETFL);  //获取原来的 flags, F_GET FL (get flags 的缩写)
    flags |= O_NONBLOCK;                //添加非阻塞属性
    fcntl(fd[0], F_SETFL, flages);      //设置新的属性

下载地址:

11_pipe

欢迎 star 和 fork 这个系列的 linux 学习,附学习由浅入深的目录。