进程间通信(一)匿名管道通信


  

管道介绍及特点

  管道也叫无名(匿名)管道,它是是 UNIX 系统 IPC(进程间通信)的最古老形式,
所有的 UNIX 系统都支持这种通信机制。
  举个例子,统计一个目录中文件的数目命令:ls | wc –l,为了执行该命令, shell 创建了两个进程来分别执行 ls 和 wc。中间的 | 为管道符,ls 进程将得到的结果(默认输出至终端)交给 wc 进程进行统计行数。在传递结果时,使用的就是进程间通信,如下图所示。
ls

  
管道的特点:

  1. 管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。

  2. 管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。

  3. 一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。

  4. 通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的

  5. 在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。

  6. 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用 lseek() 来随机的访问数据。

  7. 匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。

匿名管道一般用于有关系的进程之间,如父子进程。而有名管道由于无关系的进程之间。

管道

为什么可以使用管道来进行进程间通信?

  进程创建子进程后,父子进程的文件描述符表是相同的,如下图中的文件描述符3 指向文件A,文件描述符指向文件B,父进程fork 子进程后,父子进程的文件描述都指向A B 文件,如下图所示。匿名管道的文件描述符也被共享,所以可以在父子进程间使用文件描述进行通信。
why

  

匿名管道使用(pipe系统调用)

  1. 函数原型
#include <unistd.h>

int pipe(int pipefd[2]);
  1. 函数功能:创建一个匿名管道,用于进程通信。

  2. 参数:

  • int pipefd[2] :传出参数。pipefd[0]表示管道读端,pipefd[1] 表示管道写端。如果管道中没有数据,read 阻塞,如果管道满了,write 阻塞
  1. 返回值:成功返回0,失败返回-1,并且设置errorno

匿名管道只用于具有关系的进程之间的通信(父子进程、兄弟进程)

示例1:(发送一次,读取一次)子进程发送数据给父进程,父进程读取数据输出。

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

// 子进程发送数据给父进程,父进程读取数据输出

int main(){
    
    // 在 fork 前创建管道,父子进程就共享管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1){
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid > 0){       // 父进程
         // 从管道读取端,读取数据
        char buf[1024] = {0};
        int len = read(pipefd[0], buf, sizeof(buf));		// 默认阻塞
        printf("parent process receive: %s, pid = %d\n", buf, getpid());
    }
    else if (pid == 0){ // 子进程
        // 子进程写数据
        char* str = "hello, I am child process!";
        write(pipefd[1], str, strlen(str));
    }

    return 0;
}

编译执行,父进程接受到子进程的str 信息,输出如下
nonamepipe

示例2:(一直发送,一直读取)子进程发送数据给父进程,父进程读取数据输出。

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

// 子进程发送数据给父进程,父进程读取数据输出

int main(){
    
    // 在 fork 前创建管道,父子进程就共享管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1){
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid > 0){       // 父进程
        printf("I am parent process, pid: %d\n", getpid());

         // 从管道读取端,读取数据
        char buf[1024] = {0};
        while (1){
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent process(pid = %d) receive: %s\n", getpid(), buf);
        }
        
    }
    else if (pid == 0){ // 子进程
        printf("I am child process, pid: %d\n", getpid());

        while(1){
            // 向管道中写数据
            char* str = "hello, I am child process!";
            write(pipefd[1], str, strlen(str));
            sleep(1);
        }
    }

    return 0;
}

  子进程在管道中写数据,父进程循环读出数据。编译执行示例2,结果如下:
while-pipe
  
示例3:(父子进程交替发送接收),父进程发送信息,子进程接收信息,接着子进程发送信息,父进程接收信息。

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

// 子进程发送数据给父进程,父进程读取数据输出

int main(){
    
    // 在 fork 前创建管道,父子进程就共享管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1){
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid > 0){       // 父进程
        printf("I am parent process, pid: %d\n", getpid());

         // 从管道读取端,读取数据
        char buf[1024] = {0};
        while (1){
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent process(pid = %d) receive: %s\n", getpid(), buf);

            // 向管道中写数据
            char* str = "hello, I am parent process!";
            write(pipefd[1], str, strlen(str));

            sleep(1);
        }
        
    }
    else if (pid == 0){ // 子进程
        printf("I am child process, pid: %d\n", getpid());

        // 从管道读取端,读取数据
        char buf[1024] = {0};
        while(1){
            // 向管道中写数据
            char* str = "hello, I am child process!";
            write(pipefd[1], str, strlen(str));

            sleep(1);  // 必须在管道中写入数据后,睡眠1s,等待父进程读出   

            int len = read(pipefd[0], buf, sizeof(buf));
            printf("child process(pid = %d) receive: %s\n", getpid(), buf);
            
            // sleep(1);       	// sleep() 在这里,父进程会被read阻塞,
                            	// 子进程会循环向管道中写入读出数据

        }
    }

    return 0;
}

  注意父子进程的sleep 必须在 write 后执行,否则,read阻塞,导致子进程会自己发送数据,自己接受数据。编译执行,结果如下
在这里插入图片描述

管道缓冲区大小(ulimit -a)

命令查看:查看管道缓冲区大小 ulimit -a ,可以看到大小为 512 * 8 = 4K 。管道大小 可以通过ulimit -p 进行修改。
pipe size
系统调用接口查看:

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

int main(){
    int pipefd[2];

    int ret = pipe(pipefd);
    if (ret == -1){
        perror("pipe");
        exit(0);
    }

    // 返回管道大小
    long size = fpathconf(pipefd[0], _PC_PIPE_BUF);
    printf("pipe size = %ld\n", size);
    
    return 0;
}

  调用系统接口,查看管道大小为 4096字节。
pipsize

管道通信产生问题及原因分析
问题

解决方案:保证父子进程发送和接受的一个功能。如父进程读取信息,则关闭写端

close(pipefd[1]);		// 写端 为 1

子进程发送信息,则关闭读端

close(pipefd[0]);		// 读端 为 0

匿名管道通信实例 —— 实现 |

  实现管道符的进程通信功能,可以实现 ps aux | grep xxx 来进行ps 进程与 grep 进程的通信。具体过程如下:子进程执行 ps aux ,子进程结束,将数据发送给父进程,父进程获取数据过滤。

  1. 创建匿名管道,用于父子间通信
  2. 使用 execlp 在子进程执行 ps aux
  3. dup2 将子进程标准输出(stdout_fileno)重定向至父进程(管道的写端)
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>

int main(){
    int fd[2];
    int ret = pipe(fd);
    if (ret == -1){
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid > 0){
        close(fd[1]);       // 关闭写端
        // 父进程:从管道中读取数据
        char buf[1024] = {0};
        int length = -1;

        while ( (length = read(fd[0], buf, sizeof(buf) - 1)) > 0){
            // 过滤输出
            printf("%s", buf);
            memset(buf, 0, 1024);
        }
        wait(NULL);
    }
    else if (pid == 0){
        close(fd[0]);       // 关闭读端

        // 子进程:1. 文件描述符重定向 stdout_fileno -> fd[1]  2. 执行 ps aux
        dup2(fd[1], STDOUT_FILENO);
        execlp("ps", "ps", "aux", NULL);
        perror("execlp");
        exit(0);
    }
    else{
        perror("fork");
        exit(0);
    }

    return 0;
}

  

小结

  使用管道时,需要注意以下几种特殊情况(默认阻塞I/O)

case 1: 所有指向管道写端的文件描述符都关闭,即管道写端文件描述符的引用计数为0时,此时,有进程从管道读端读数据,管道中剩余数据被读取以后,再次 read 会返回0,与读到末尾相同。

case 2: 如果有指向管道写端的文件描述符没有关闭,即管道写端引用计数大于0,而持有管道写端没有往管道写数据,此时,进程从管道中读取数据,则管道中剩余数据被读取后,再次 read 会阻塞,直到管道中有数据可以读,才可以读取数据并返回读取字节个数。

case 3:如果所有指向管道读端的文件描述符都关闭了,即管道读端引用计数等于0,此时,有进程向管道中写数据,那么,该进程会收到一个信号SIGPIPE ,通常会导致进程异常终止。

case 4:如果有指向管道读端的文件描述符没有关闭,即管道读端引用计数大于0,而持有管道的进程没有从管道中读数据,此时,写端进程向管道读数据写数据,管道被写满时,再次调用 write 会被阻塞,知道管道中有空位置才能再次写入数据并返回。


读管道:

  1. 当管道中有数据,read 返回实际读到的字节数。(正常情况下
  2. 当管道中无数据
      - 写端被完全关闭,read 返回0(读到文件末尾)(case 1
      - 写端没有完全关闭,read 阻塞等待       (case 2

写管道:

  1. 管道读端被全部关闭,产生信号 SIGPIPE`,进程异常终止。   (case 3
  2. 管道读端没有被全部关闭
     - 管道已满,write 阻塞            (正常情况下
     - 管道没有满,write 将数据写入,并返回实际写入的字节数   (case 4

巨人的肩膀

  1. Linux进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)

一键三连是对我最大的鼓励与支持。欢迎关注编程小镇,每天涨一点新姿势😄。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值