让进程能够“相互沟通”的高级方式一:匿名管道

文章介绍了Linux操作系统中匿名管道作为进程间通信的一种方式,强调其只能用于有血缘关系的进程。匿名管道通过共享内核缓冲区实现数据传输,具有单向性,且在父子进程间传递文件描述符来实现通信。文章还给出了创建和使用匿名管道的示例代码,并讨论了不同速度的读写端情况以及管道关闭的影响。
摘要由CSDN通过智能技术生成

代码运行及测试环境:linux centos7.6
在阅读这篇文章时,需要掌握OS对文件管理的基础知识(文件打开表、文件描述符、索引结点…)

前言

我们都知道进程是具有独立性的,意味着进程之间无法相互通信。但在一些情况下,不得不让进程之间进行通信。通信的作用主要有:

  • 数据的传输。
  • 数据的共享。
  • 控制其他进程。

数据的传输、共享是非常容易理解的。为什么需要一个进程去控制另一个进程呢?做一个虚拟的假设,进程A负责检测地铁进站,进程B负责打开地铁门。进程B一直处于休眠状态,当进程A检测出地铁到站停靠后,进程A可以唤醒进程B,打开地铁门。可见,让一个进程去控制另外一个进程,在日常生活中是非常频繁的, 也是十分重要的。

如果要让两个相互独立的进程进行通信,那它们之间需要一个媒介。OS提供了这个媒介,并且对它们之间的通信进行管理。根据OS提供媒介的不同,产生了许多进程间通信的方式,例如:共享内存、消息队列、管道等等。我在这篇文章里只谈论管道通信——匿名管道。

匿名管道的底层原理

首先强调匿名管道只能用于 具有“血缘关系”的进程之间的通信。

当用户请求建立匿名管道时,操作系统会为进程创建一个管道文件,这个文件它并不需要文件名,因为OS建立管道文件后,就会直接把它的文件属性拷贝到打开文件表的一个条目内,用户可以获取到对应的文件描述符,进而读写管道。
在这里插入图片描述
它会让俩个相邻的打开文件表表项同时指向这个管道文件的inode, 是为了读写操作分离,一个文件描述符对应的是写入操作,另一个文件描述符对应的是读取操作。

虽然匿名管道的本质是一个文件,但和普通文件有许多差别。从另外一个角度来看,匿名管道更偏向于解释成一块缓冲区。用户并不是直接将数据写入磁盘上的数据块,也不会直接从磁盘上读取数据出来, 而是以内核缓冲区为媒介。
在这里插入图片描述
由OS再去调用数据从内核缓冲区写入磁盘和读到内核缓冲区的接口。

那么匿名管道是如何实现 具有血缘关系的进程 之间的通信呢?以父子进程间的管道通信为例。
在这里插入图片描述
首先父进程创建一个管道, 再创建子进程,子进程的PCB是以父进程为模板的,父子进程此刻的打开文件表是一样的, 所以父进程文件打开表有两个表项指向管道,子进程打开文件表也有两个表项指向该管道,并且文件描述符此刻是一样的。这样,相互独立的两个父子进程看到了同一份资源——“管道”, 管道自然而然充当了它们通信的媒介。

管道通信是单向通信的,只允许一端进行写入,另一端进行读取。如果要实现管道通信,需要根据情况,关闭对应的读写端。

匿名管道创建过程

我们将上述的过程简化一下
🍍第一步:父进程创建匿名管道
在这里插入图片描述
🥑第二步:父进程创建出子进程
在这里插入图片描述
🍉第三步:根据需要,关闭对应的读端和写端
(这里以父进程写入,子进程读取为例)
在这里插入图片描述

匿名管道的几种情况

创建匿名管道的系统调用pipe

#include <unistd.h>
int pipe(int pipefd[2]);
//pipefd是一个输出型参数:
//pipefd[0]对应读端的文件描述符 pipefd[1]对应写端
//如果创建管道成功返回0,否则返回-1

简单写一个父进程读取管道、子进程写数据进管道的例子。

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

int main(void)
{
    //1.创建管道
    int pipefd[2];
    if(pipe(pipefd) != 0)
    {
        perror("pipe");
        exit(-1);
    }
    //2.父进程创建出子进程
    if(fork() == 0)
    {
        //child
        //3.1 让子进程写入数据进入管道 ,关闭读端
        close(pipefd[0]);
        const char *str = "i am ydy.";
        while(1)
        {
            write(pipefd[1], str, strlen(str));
            sleep(1);
        }

    }
    else
    {
        //father
        //3.2 父进程从匿名管道种读取数据, 关闭写端
        close(pipefd[1]);
        while(1)
        {
            char buffer[64] = {0};
            ssize_t s = read(pipefd[0], buffer, sizeof(buffer));
            if(s < 0){
                //读取失败
                break;
            }
            else if(s == 0){
                //写端关闭,且数据读取完毕
                printf("child quit...\n");
                break;
            }
            else{
                printf("child say# %s\n", buffer);
            }
        }
    }
    waitpid(-1, NULL, WNOHANG);
    return 0;
}

匿名管道的几种情况:
🌻读端读得慢,写端写得快
🌼读端读得快,写端写得慢
💐写端在写入数据,读端突然关闭
🌷读端在读取数据,写端突然关闭

这几种情况的特点,可以自行检测得出,我在这里直接写结论,当然这些结论的正确性我在此之前都用代码检验过了,毕竟“实践是检验真理的唯一标准”。

🌻读端读得慢,写端写得快

管道内有数据,读端就可以读取。由于写端写得比较快,所以按照趋势下去,管道一定会满。管道满了以后,写端进程会被阻塞,等待读端读取数据。你可能认为,管道满了后,读端读取一份数据,写端就会写入数据,事实并不是这样。实际上,写进程一旦被阻塞,只有管道能够提供一定的空闲空间大小(这个大小具体我并不知道是多少, 在我的机子上跑,测出来的是至少是1kb)后,写进程才会被唤醒。

🌼读端读得快,写端写得慢

由于写的速度赶不上读取的速度, 所以管道极可能某个时刻没有数据。读进程会被阻塞,等待写进程写入数据,直到管道有数据。

💐写端在写入数据,读端突然关闭

管道的作用就是为了让两个进程通信,读端关闭了,写进程又不需要拿取数据,所以读进程不在,管道就没有意义了。所以读端关闭,写进程也会退出。

🌷读端在读取数据,写端突然关闭

写端关闭了,管道内可能还会有数据。读进程是需要拿数据的,管道里的东西还有用,所以读端把数据全部读完后,读进程再退出。

读进程读取得慢,写进程写入得快,会出现这种现象的原因本质上是因为管道是有大小的,在Linux往管道写入64KB数据的时候,写进程就不再写入了,而超过4KB的时候,OS就不再保证管道的写入具有原子性了。对于一端关闭的现象,间接阐述了管道机制必须要有确定对方存在的协调能力。

匿名管道特点

  • 仅限具有血缘关系的进程使用
    根据匿名管道的创建过程可知,匿名管道的本质是一个文件,没有文件名,由用户请求,OS帮助建立后,直接把文件属性拷贝到进程的打开文件表内。血缘关系的进程PCB都是以“父进程”模板创建的,所以子进程以及有血缘关系的进程,打开文件表内都有关于匿名管道的属性。由于没有文件名,其他进程无法自行打开文件,因此非血缘关系的进程间无法使用匿名管道。
  • 匿名管道的生命周期是随进程的
    因为下一次,进程无法打开上一个管道文件(没有文件名)。
  • 管道是单向通信的。
  • 管道是面向字节流的
  • 管道机制必须能够拥有互斥、同步和确定对方存在的协调能力
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小酥诶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值