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

当程序开始执行,进入 main() 函数后,父进程创建,当调用 fork() 创建子进程后,两个进程间常常需要传输数据,但多进程与多线程不同,进程间是无法共享内存的,所以进程间的通信就尤为重要,无名管道(pipe)就是用来解决进程间通信的其中一种方法。但仅仅用在有亲缘关系的进程中,这又是为什么呢?

无名管道通信原理

具体来说,就是内核开辟一个管道,通过往这个管道里读写内容,从而完成数据的传输;

管道具体是什么

每个进程都运行都有自己的虚拟内存中,而虚拟内存实则都有与之对应的物理内存,操作系统即内核代码也都运行在相应的内存上,而管道,则是内核在自己的内存中开辟出来的一段缓存区(buf),父子进程通过文件描述符,来对这个缓存区进行读写,例如,父进程往里面写 (write),子进程往里面读 (read) ,就完成了父子进程之间的通信。

粗略图

在这里插入图片描述
借助内核中的这个缓存区,进程1可将数据写入到管道,进程2来读取,但具体是怎么实现的呢。

pipe()

#include <unistd.h>

int pipe(int fd[2]);

返回:成功返回0,出错返回-1,error被设置;

该函数只有一个参数,是一个整型类型的数组,数组只有两个元素,由此,可以猜测 pipe() 的用法:

当程序调用 pipe() 后,内核就会在其空间中开辟一个管道,并返回两个文件描述符(这里的返回不是返回值),将这两个文件描述符存储在参数数组中,一个 fd[0] 用于对管道进行读操作,另一个 fd[1] 对管道进行写操作。如图:

在这里插入图片描述

之所以称之为无名管道,是因为我们用来读写的文件描述符,并不是由 open() 函数指定文件名后返回的文件描述符,这是我们通过非 open()
获取的文件描述符(类似的还有socket,accept),我们并不知道这段内存或者文件的名字,所以我们称之为无名管道。

无名管道——单向通信

fork()

单向通信又叫半双工通信;无名管道通信中,必须用到的就是 fork() 函数,因为当我们使用 pipe() 创建管道以后,别的进程是不可见的,就像在一个进程中使用 open() 打开一个文件描述符以后,别的进程并不能使用这个文件描述符一样,所以,只能用到
fork() 函数创建子进程,这是因为子进程在创建后会继承父进程的很多东西,子进程具体继承了父进程哪些东西以及进程如何工作,可参考博客:多进程服务器编程
其中,就包括了已指向管道的文件描述符,这样,出父进程以外,就有其他进程(子进程)也可以读写这个管道了。

单向通信原理

父进程在调用 fork() 创建子进程之前,调用 pipe() 函数,创建了一个管道,函数返回两个文件描述符到我们传入的参数(数组)中,再调用 fork() 创建子进程,子进程继承了这些文件描述符这样,子进程就也有两个文件描述符指向了这个管道:
在这里插入图片描述
通过这个图,看似可以完成双向通信,但是不然,假设父进程往管道里写了内容,很有可能在子进程读到内容之前,被父进程的fd[0]抢读,子进程就读不到数据了。假设是父进程给子进程成发送数据——则父进程需要用到的是写端fd[1],而子进程需要用到读端fd[0],同样为了避免其他函数误操作使用到这些文件描述符,我们需要把没用到的文件描述符 close;这里,我们需要关闭父进程的读端与子进程的写端;
下图就是单向通信管道:

扫描二维码关注公众号,回复: 10145689 查看本文章

在这里插入图片描述

简单的代码实现

int main(int argc, char *argv[])
{
    char        buf[12];
    int        rv = -1;
    int        fd[2];   //Put fd[0] , fd[1]


    bzero(buf,sizeof(buf));
    if((rv = pipe(fd)) < 0)
    {
        printf("Create pipe failure:%s\n",strerror(errno));
        return -1;
    }

    if((rv = fork()) < 0)   //创建子进程
    {
        printf("fork() failure:%s\n",strerror(errno));
        return -2;
    }

    else if(rv > 0)   //父进程会执行这部分代码
    {

        close(fd[0]);   //父进程关闭无用文件描述符
        while(1)
        {
            write(fd[1],"Hello World",12);
            sleep(2);
        }
    }
    else if(0 == rv)
    {

        close(fd[1]);进程关闭无用文件描述符;
        while(1)
        {
            read(fd[0],buf,sizeof(buf));
            printf("Receive form parent:%s\n",buf);
        }
    }
    return 0;
}

执行程序程序:

在这里插入图片描述
父进程每隔两秒向管道中发送数据,子进程在调用 read() 函数后,将会阻塞,直到读到数据为止,我们可以尝试修改代码,观察该现象;

信号 SIGPIPE

我们可以多次使用 fork() 函数来创建子进程,他们之间两两都是亲缘进程,但是,无名管道只支持两个进程间通信;

对于一个管道来说,当他的读端全部被关闭以后,如果程序还试图从其写端写入数据,则内核将会给该进程发送一个信号: SIGPIPE;当程序接收到这个信号后,会执行终止进程的的动作;但是我们可以使用signal函数来捕捉这个信号忽略之;

signal(SIGPIPE,SIG_IGN); 捕捉信号并作忽略处理;

无名管道 —— 双向通信

双向通信又叫全双工通信,因为前面提到的问题,要想使两个亲缘进程完成双向通信,我们就必须创建两个管道(即调用两次 pipe() 函数),一个用来 父 ----- 子,另一个 子 ----- 父;这么来说,一共就有 8 个文件描述符,其中4个需要关闭,图解:
在这里插入图片描述
关闭掉无用的文件描述符,就只剩下4个,两个管道各司其职,互不干扰:
在这里插入图片描述

代码修改——双向通信

我们定义两个整型类型的数组,fd1[2]fd2[2] ,分别用来存储第一个管道和第二个管道的读端和写端,作为第一个管道来说,我们用它来完成父进程 ———> 子进程的数据发送,所以 , 对于父进程来说 ,我们要关掉他的读端:
fd1[0] ; 而子进程则关闭他的写端:fd1[1] ; 同理,close 掉 子进程 ——> 父进程 这个管道的相应文件描述符;

代码:

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

int main(int argc, char *argv[])
{
    char        buf[12];
    int        rv = -1;
    int        fd1[2];   //Put fd[0] , fd[1]
    int        fd2[2];


    bzero(buf,sizeof(buf));
    if((rv = pipe(fd1)) < 0)
    {
        printf("Create pipe failure:%s\n",strerror(errno));
        return -1;
    }

     if((rv = pipe(fd2)) < 0)
    {
        printf("Create pipe failure:%s\n",strerror(errno));
        return -1;
    }


    if((rv = fork()) < 0)
    {
        printf("fork() failure:%s\n",strerror(errno));
        return -2;
    }

    else if(rv > 0)
    {

        close(fd1[0]);
        close(fd2[1]);
        while(1)
        {
            write(fd1[1],"Hello World",12);
            read(fd2[0],buf,sizeof(buf));
            printf("Receive from child:%s\n",buf);
            sleep(2);
        }
    }
    else if(0 == rv)
    {

        close(fd1[1]);
        close(fd2[0]);
        while(1)
        {
            read(fd1[0],buf,sizeof(buf));
            write(fd2[1],"Goodbye",12);
            printf("Receive from parent:%s\n",buf);
        }
    }
    return 0;
}

运行代码:
在这里插入图片描述

发布了18 篇原创文章 · 获赞 37 · 访问量 5003

猜你喜欢

转载自blog.csdn.net/weixin_45121946/article/details/104804133