无名管道的通信原理,父子进程单向通信,SIGPIPE信号,父子进程双向通信(本机IPC)【linux】(zw)

无名管道的通信原理

内核会开辟一个“管道”,通信的进程通过共享这个管道,从而实现通信。
内核的代码也是运行在物理内存上的,内核创建一个“管道”,其实就是在内核自己所在的物理内存空间中开辟出一段缓存空间
比如
char buf[1024];

在这里插入图片描述

进程1向管道写数据,进程2从管道读取数据。或者进程2向管道写数据,进程1从管道读取数据,两个进程共享同一管道并且对同一管道的操作最终实现通信。

如何操作无名管道

以文件的方式来读写管道,以文件方式来操作时
1)有读写用的文件描述符
2)读写时会用write、read等文件Io函数。

为什么叫无名管道

既然可以通过“文件描述符”来操作管道,那么它就是一个文件(管道文件),但是无名管道文件比较特殊,它没有文件名,正是因为没有文件名,所有被称为无名管道。

没有文件名,我们怎么操作这个文件呢?
后面博客内容会说明。

无名管道的API

函数原型

#include <unistd.h>
int pipe(int pipefd[2]);

功能
创建一个用于亲缘进程(父子进程)之间通信的无名管道(缓存),并将管道与两个读写文件描述符关联起来。

参数
缓存地址,缓存用于存放读写管道的文件描述符。
从这个参数的样子可以看出,这个缓存就是一个拥有两个元素的int型数组。

1)元素[0]:里面放的是读管道的读文件描述符
2)元素[1]:里面放的是写管道的写文件描述符。

特别需要注意的是,这里的读和写文件描述符,是两个不同的文件描述符。

这里无名管道的读、写文件描述符,就是直接在创建管道时得到的,与open没有任何关系。

这里也根本没办法使用open函数,因为open函数需要文件路径名,无名管道连文件名都没有,所以说根本就没办法使用open来打开文件,返回文件描述符。我们之后在网络编程编程博客中提到的套接字文件,套接字文件描述符通过socket函数得到,socket函数就会返回套接字文件的描述符。

返回值
成功返回0,失败则返回-1,并且errno被设置。

无名管道特点

为什么无名管道只能用于亲缘进程之间通信?

由于没有文件名,因此进程没办法使用open打开管道文件,从而得到文件描述符,所以只有一种办法,那就是父进程先调用pipe创建出管道,并得到读写管道的文件描述符。然后再fork出子进程,让子进程通过继承父进程打开的文件描述符,父子进程就能操作同一个管道,从而实现通信。

图示:
在这里插入图片描述

父进程调用pipe函数的时候,内核在执行pipe函数的时候就会在内核空间创建一个缓存,这个缓存就是无名管道,文件有一个读端和写端,读端和写端把各自的文件描述符返回给数组,元素1放读文件描述符,文件2放写文件描述符,那么父进程fork出子进程之后,就会继承父进程打开的文件描述符。那么父子进程就可以共享操作同一个管道文件。及就是子进程会继承父进程的读端和写端。那么结果就是父进程和子进程分别由自己的读端和写端只是端口文件描述符号是一样的。

什么样的进程之间,我们可以称为亲缘进程呢?
只要是存在继承关系的进程就是亲缘进程,继承关系分为两种。
(1)直接继承关系
父进程————>子进程
(2)间接继承关系
父进程————>子进程————>子进程————>…

读管道时,如果没有数据的话,读操作会休眠(阻塞)

比如有两个进程,它们是父子进程,需要进行通信,由于父子进程是亲缘进程,此时我就可以使用无名管道通信了。

父子进程单向通信

实现步骤:
(a)父进程在fork之前先调用pipe创建无名管道,并获取读、写文件描述符
(b)fork创建出子进程,子进程继承无名管道读、写文件描述符
(c)父子进程使用各自管道的读写文件描述符进行读写操作,即可实现通信

代码实现:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>

void print_err(char * estr)
{
        perror(estr);
        exit(-1);
}

int main()
{
        int ret = 0;
        //[0]读文件描述符
        //[1]写文件描述符
        int pipefd[2] = {0};     //用于存放管道的读写文件描述符
        ret = pipe(pipefd);
        if(ret == -1)print_err("pipe fail");
        ret = fork();
        if(ret>0)
        {
                while(1)
                {
                write(pipefd[1],"hello world",12);
                sleep(1);
                }
        }
        else if(ret == 0)
        {
                while(1)
                {
                char buffer[20] = {0};
                bzero(buffer,sizeof(buffer));
                read(pipefd[0],buffer,sizeof(buffer) - 1);
                printf("child recv data:%s\n",buffer);
                }
        }
        return 0;
}

运行结果为:

在这里插入图片描述

上面就实现了从父进程向管道文件写入数据,子进程从管道文件读取数据的单向通信机制。

如果管道没有数据的时候,进行的读操作就会进入阻塞(休眠)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>

void print_err(char * estr)
{
        perror(estr);
        exit(-1);
}

int main()
{
        int ret = 0;
        //[0]读文件描述符
        //[1]写文件描述符
        int pipefd[2] = {0};     //用于存放管道的读写文件描述符
        ret = pipe(pipefd);
        if(ret == -1)print_err("pipe fail");
        ret = fork();
        if(ret>0)
        {
                while(1)
                {
                sleep(1);
                }
        }
        else if(ret == 0)
        {
                while(1)
                {
                char buffer[20] = {0};
                bzero(buffer,sizeof(buffer));
                read(pipefd[0],buffer,sizeof(buffer) - 1);
                printf("child recv data:%s\n",buffer);
                }
        }
        return 0;
}

运行结果为:

在这里插入图片描述

那么父子进程都可以进行读写管道文件的操作,如果是单向的数据传输的时候,我们会把用不到的文件描述符进行关闭。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>

void print_err(char * estr)
{
        perror(estr);
        exit(-1);
}

int main()
{
        int ret = 0;
        //[0]读文件描述符
        //[1]写文件描述符
        int pipefd[2] = {0};     //用于存放管道的读写文件描述符
        ret = pipe(pipefd);
        if(ret == -1)print_err("pipe fail");
        ret = fork();
        if(ret>0)
        {
                close(pipefd[0]);
                while(1)
                {
                write(pipefd[1],"hello world",12);
                sleep(1);
                }
        }
        else if(ret == 0)
        {
                close(pipefd[1]);
                while(1)
                {
                char buffer[20] = {0};
                bzero(buffer,sizeof(buffer));
                read(pipefd[0],buffer,sizeof(buffer) - 1);
                printf("child recv data:%s\n",buffer);
                }
        }
        return 0;
}

运行结果为:

在这里插入图片描述

那么就实现了单向通信。

SIGPIPE信号

SIGPIPE信号与管道有关

什么时候会产生在这个信号?
写管道时,如果管道的读端被close了话,向管道“写”数据的进程会被内核发送一个SIGPIPE信号,发这个信号的目的就是想通知你,管道所有的“读”都被关闭了。

这就好比别人把水管的出口(读)给堵住了,结果你还一直往里面灌水(写),别人跟定会警告你,因为你这样可能会对水管造成损害,道理其实是类似的。

由于这个信号的默认动作是终止,所以收到这个信号的进程会被终止,如果你不想被终止的
话,你可以忽略、捕获、或者屏蔽这个信号。

代码演示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>

void print_err(char *estr)
{
        perror(estr);
        exit(-1);
}
int main(void)
{
       int ret = 0;
        int pipefd[2] = {0};

        ret = pipe(pipefd);

        if(ret == -1) print_err("pipe fail");
        ret = fork();
        if(ret > 0)
        {
                signal(SIGPIPE, SIG_IGN);
                close(pipefd[0]);
                while(1)
                {
                        write(pipefd[1], "hello", 5);
                        sleep(1);
                }
        }
        else if(ret == 0)
        {
                close(pipefd[1]);
                close(pipefd[0]);
                while(1)
                {
                        char buf[30] = {0};
                        bzero(buf, sizeof(buf));
                        read(pipefd[0], buf, sizeof(buf));
                        printf("child, recv data:%s\n", buf);
                }
        }
        return 0;
}

运行结果为:

在这里插入图片描述

我们可以看到只有一个子进程在运行,父进程不再运行。

在这里插入图片描述

因为父进程尝试在向管道写入数据的时候,被内核发送了SIGPIPE信号,父进程收到SIGPIPE信号之后就会被终止。如果不想要终止我们就可以进行忽略或者捕获操作。
使用signal函数:
signal(SIGPIPE,SIG_IGN); 就可以把信号设置为忽略。相信使用可以查看signal函数博客。

只有当管道所有的读端都被关闭时,才会产生这个信号,只有还有一个读端开着,就不会产生。

父子进程双向通信

单个无名管道无法实现双向通信,为什么?
因为使用单个无名管道来实现双向通信时,自己发送给对方的数据,就被自己给抢读到。
在这里插入图片描述

上面过程,我们感觉可以实现父子进程的双向通信,但是实际情况是不可以的,如果要实现双向通信,父子进程的读写端都要打开,那么父进程在读数据的时候子进程也在读取数据,父进程在写数据的时候子进程也在写入数据。那么父进程写入的数据父进程就会抢先读取,子进程写入的数据子进程就会抢先读取,就无法实现父子进程之间的双向通信。

如何实现无名管来实现双向通信?
使用两个无名管道,每个管道负责一个方向的通信。
在这里插入图片描述

父进程两次调用pipe函数创建两个管道,那么总共会产生8个文件描述符,父进程4个子进程4个。然后通过上面图示关闭一部分文件描述符。为了实现双向通信,也是为了防止其他进程对于未关闭的文件描述符进行操作。

整理上图如下:

在这里插入图片描述

代码演示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>

void print_err(char *estr)
{
        perror(estr);
        exit(-1);
}
int main(void)
{
        int ret = 0;
        int pipefd1[2] = {0};
        int pipefd2[2] = {0};
        ret = pipe(pipefd1);
        if(ret == -1) print_err("pipe fail");
        ret = pipe(pipefd2);
        if(ret == -1) print_err("pipe fail");
        ret = fork();

        if(ret > 0)
        {
                close(pipefd1[0]);
                close(pipefd2[1]);
                char buf[30] = {0};
                while(1)
                {
                        write(pipefd1[1], "hello", 5);
                        sleep(1);
                        bzero(buf, sizeof(buf));
                        read(pipefd2[0], buf, sizeof(buf));
                        printf("parent, recv data:%s\n", buf);
                }
        }
        else if(ret == 0)
        {
                close(pipefd1[1]);
                close(pipefd2[0]);
                char buf[30] = {0};
                while(1)
                {
                        sleep(1);
                        write(pipefd2[1], "world", 5);

                        bzero(buf, sizeof(buf));
                        read(pipefd1[0], buf, sizeof(buf));
                        printf("child, recv data:%s\n", buf);
                }
        }
        return 0;
}

运行结果为:
在这里插入图片描述

上面代码需要注意的一点:
至少要保证一个进程的write在read之前,否则两个进程都会进入到阻塞状态。

无名管道有两个缺点
(1)无法用于非亲缘进程之间
因为非亲缘进程之间没办法继承管道的文件描述符。

(2)无法实现多进程之间的网状通信

在这里插入图片描述

如果非要使用无名管道实现多进程之间的网状通信的话,文件描述符的继承关系将非常的复杂。所以无名管道基本只适合两个进程间的通信。

什么时候合适使用无名管道

如果通信的进程只有两个,而且还是亲缘进程时,那么可以使用无名管道来通信。
比如:
1)直接继承父子进程之间的通信
在这里插入图片描述

2)间接继承关系的两进程之间的通信
在这里插入图片描述

发布了163 篇原创文章 · 获赞 94 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_43648751/article/details/104680142