Linux操作系统之进程间通信:管道概念

目录

前言:

一、进程间通信概念

二、什么是管道呢?

三、匿名管道

四、匿名管道的使用

总结:


前言:

本篇文章将会为大家带来进程间通信的基础概念,随后为大家大致了解一下我们古老的进程间通信的方法------管道。

希望通过本篇文章,能够帮助大家掌握进程间通信的基本概念,以及掌握匿名命名管道原理操作!


一、进程间通信概念

我们曾经说过,进程具有独立性,操作系统中的每个进程都拥有自己独立的运行环境和资源,彼此之间相互隔离,互不干扰的特性。这是现代操作系统设计中的一个基本原则。

但是,现代计算机不可能独立完成一项任务,进程与进程之间,也必须要出现交流。

那么如何进行这样的通信呢?我们比如面临着一个前提:得让不同的进程,看到同一份资源。

操作系统提供了打破这种独立性的机制:进程间通信(IPC)机制。

为什么要提供这种机制呢?因为我们面临以下问题:

:数据传输:一个进程将它的数据发送给另一个进程

:资源共享:多个进程之间需要共享一些资源

:进程控制:有些进程希望完全控制另外一个进程的执行(比如说debug进程),此时控制进程希望拦截另外一个进程的所有陷入和异常,并能够及时知道它的状态改变。


所以我们需要设计一套通信的接口,调用系统调用,设定接口标准

于是进程间通信通过时代的发展,主要形成以下三种形式:
1、管道
2、System V进程间通信
3、POSIX进程间通信(这个属于操作系统网络通信,我们后面只会讲到system V)

其中前面两个可以归纳到本地通信的范畴,就是在同一台主机下,同一个操作系统中的不同进程之间实现通信。

我们可以把这三个具体划分为以下内容:
管道 :
匿名管道pipe
命名管道
System V IPC:
System V 消息队列
System V 共享内存
System V 信号量
POSIX IPC:
消息队列
共享内存
信号量
互斥量
条件变量
读写锁


二、什么是管道呢?

管道属于linux系统比较古老的通信方式,最早可以溯源到unix操作系统上。我们把从一个进程连接到另外一个进程上的一个数据流,称为一个管道。、

在linux系统上我们通常通过 | 这个符号来形成两个进程之间的管道:

我们可以看到,我们刚才执行的两个sleep命令变成了两个进程,并且,这两个进程的关系是兄弟进程。(这里的&是让命令后台执行)。他们的本质其实就是进程间通信。
我们以前讲过:我要访问一个文件,需要用到我的文件操作符,找到该下标的指针,找到我要访问的文件的struct file。而一个文件要从磁盘加载到内存,是需要先进行路径解析,确定自己所在分区,找到inode,就找到了自己的属性与内容,从而也能形成struct file被管理起来。而在我们的struct file上,我们可以找到这个结构体对应的inode,以及方法指针集合,还有就是这个文件所独有的内核级文件缓冲区。
那么我想问一下。如果我们的这个进程已经打开了文件访问了,如果此时再创建一个子进程,会出现什么情况呢?

我们之前讲进程创建的时候说过,创建子进程,会把父进程的task_struct复制一份,也就是说,二者的文件描述符表也会被复制。那么父进程打开文件的struct file与文件的inode结构体与这个文件的内核级缓冲区呢?

也会被复制吗?

:只有struct file会被拷贝一份,但是新的struct file所指向的inode与文件缓冲区是一样的指针,也就是说,更靠近文件层面的inode与缓冲区是共享的,不会被复制,而靠近进程方面的struct file等结构体会被子进程拷贝一份。

为什么inode不会被子进程拷贝呢?

:inode 是文件系统层面的数据结构,存储文件的元信息(权限、大小、磁盘块位置等),而不是进程级别的数据。所有进程访问同一个文件时,共享同一个 inode,只是通过各自的 文件描述符来引用它。

为什么缓冲区不会被子进程拷贝呢?

:因为这个内核级缓冲区,是由操作系统提供并管理的,该文件的内核缓冲区由所有打开此文件的进程共用,不属于单个进程。文件缓冲区虽然被进程打开,但是他的整个资源管理属于操作系统,这也就是为什么我们把进程关掉了,文件需要的时候也会自动释放的根本原因。


那么同学们有没有想过一个问题,父子进程,算不算看到了同一份数据呢?

答案肯定是算的。所以父子进程就可以实现我们进程间的通信。

在进程间通信中,当我们将数据放在内核管理的缓冲区而不需要持久化到磁盘时,这种机制本质上创建了一个纯内存的通信通道。操作系统通过专门的数据结构(如环形缓冲区)和特定的系统调用接口(pipe()等)对这种通信方式进行封装和优化,最终形成了一种高效的进程间通信机制------管道。

三、匿名管道

我们有一个专门的系统调用接口来创建管道:

其中 pipefd是一个⽂件描述符数组,其中fd0表⽰读端, fd1表示写端,这个数组参数是一个输出型参数,我们需要自己创建一个数组然后把这个传进去。

没错,管道其实是一种单向通信的东西。

一般来说,我们都使用带有血缘关系的进程实现管道通信(尤其是父子,因为天生具备看到同一份文件的条件)

管道只能进行单向通信,并且我们必须先打开,随后创建子进程,利用的就是这个子进程继承父进程资源的特性。

为什么我们要父子进程一个关系写,另外一个就要关闭读呢?这样是防止fd泄露以及误操作。

而这种管道就不需要名字,所以不需要在操作系统下带路径(文件路径解析),我们把这种管道,称为匿名管道。


四、匿名管道的使用

我们有以下程序及其相应的Makefile:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<sys/wait.h>


int main()
{
    int fd[2];
    int ret=pipe(fd);
    if(ret<0)
    {
        std::cerr<<"pipe error"<<std::endl;
        return 1;
    }

    int id = fork();
    //创建进程后,父子进程需要各自关闭一个读写端
    //我们这里让子进程写,父进程读
    if(id==0)
    {
        //子进程
        ::close(fd[0]);
        int cnt=0;

        while(1)
        {
            std::string message="hello world,";
            message+=std::to_string(getpid());
            message+=",";
            message+=std::to_string(cnt++);
            write(fd[1],message.c_str(),message.size());
            sleep(1);
        }

        exit(0);
    }
    else if(id>0)
    {
        //父进程
        ::close(fd[1]);

        char buffer[1024];
        while(1)
        {
            ssize_t s=read(fd[0],buffer,1024);
            if(s>0)
            {
                buffer[s]=0;//将读取到的内容结尾添加'\0',因为字符串以\0结尾时C语言的规定,系统调用write时可没这个规定
                //write写入的时候是不需要size+1的,我们就没有写入\0,这里需要手动补上
                std::cout<<"parent read:"<<buffer<<std::endl;
            }
        }
        pid_t pid=waitpid(id,nullptr,0);
    }
    return 0;
}

运行之后我们可以看见,这样就实现了一个简单的,父子进程的管道通信。

我们间隔一秒才写入一次,那么这个时候父进程一直在while循环读,没有内容写入时,父进程就处于堵塞状态。

现象1:管道为空&&管道正常,read会阻塞(read本身是系统调用)

因为我们是共享的一份资源,看到的是同一个缓冲区,那么既然是同一份,会不会出现,你读一半时,我就开始写,或者说我写一半时,你就开始读的情况呢?

这个情况就是数据不一致情况。

面对这个问题,管道内部进行了处理,所以才会出现刚刚父进程阻塞的情况(即系统调用read自己会处理)。

那么我们现在改一下代码,每次只输入一个字符,但是不限制写入时间,而读数据确实要等待十秒钟才开始读,我们看看会出现什么情况:

cpp 复制代码
int main()
{
    int fd[2];
    int ret=pipe(fd);
    if(ret<0)
    {
        std::cerr<<"pipe error"<<std::endl;
        return 1;
    }

    int id = fork();
    //创建进程后,父子进程需要各自关闭一个读写端
    //我们这里让子进程写,父进程读
    if(id==0)
    {
        //子进程
        ::close(fd[0]);
        int total = 0;
        int cnt=0;
        while(1)
        {
            std::string message="h\n";

            total += ::write(fd[1], message.c_str(), message.size());
            cnt++;
            std::cout << "total: " << total << std::endl;

        }

        exit(0);
    }
    else if(id>0)
    {
        //父进程
        ::close(fd[1]);

        char buffer[1024];
        sleep(10);
        while(1)
        {
            ssize_t s=read(fd[0],buffer,1024);
            if(s>0)
            {
                buffer[s]=0;
                std::cout<<"parent read:"<<buffer<<std::endl;
            }
        }
        pid_t pid=waitpid(id,nullptr,0);
    }
    return 0;
}

我们一次只打印一个字符,居然发现,写入停留在了65536这个数字上。

如果我们把这个数字除以1024,你会发现,这个刚好等于64kb。

也就是说,我们的管道也会有写入上限的。

现象2:管道为满&&管道正常,write会阻塞(write本身是系统调用)

最后我们可以发现,哪怕我们是一个一个写入的,我要读数据,却不一定要一个一个的读。

管道根本不关心写了什么,也不管你写了多少次,只关心要多少个数据。

这个特性:叫做面向字节流


我们再改变一下代码:

使得子进程写入一次数据后,就break退出循环,关闭写端,我们在父进程新增s==0的检测:

cpp 复制代码
int main()
{
    int fd[2];
    int ret=pipe(fd);
    if(ret<0)
    {
        std::cerr<<"pipe error"<<std::endl;
        return 1;
    }

    int id = fork();
    //创建进程后,父子进程需要各自关闭一个读写端
    //我们这里让子进程写,父进程读
    if(id==0)
    {
        //子进程
        ::close(fd[0]);
        int total = 0;
        int cnt=0;
        while(1)
        {
            std::string message="h\n";

            total += ::write(fd[1], message.c_str(), message.size());
            cnt++;
            std::cout << "total: " << total << std::endl;
            break;//结束循环
        }

        exit(0);
    }
    else if(id>0)
    {
        //父进程
        ::close(fd[1]);

        char buffer[1024];
        //sleep(10);
        while(1)
        {
            ssize_t s=read(fd[0],buffer,1024);
            if(s>0)
            {
                buffer[s]=0;
                std::cout<<"parent read:"<<buffer<<std::endl;
            }
            else if(s==0)
            {
                std::cout<<"child quit"<<std::endl;
            }
             sleep(1);
        }
        pid_t pid=waitpid(id,nullptr,0);
    }
    return 0;
}

我们会发现,当写端关闭时,读段会进入返回值为0的判断语句中:

现象3:写端关闭&&读段正常,读端读到0就表示读到了结尾


那如果反着过来呢?

我们关闭读段只剩下写端:

同学们,操作系统不会做浪费时间浪费空间的事情,而这个情况就是浪费时间与空间的事情,所以操作系统会直接杀掉写端进程。

我们讲进程结束的时候提到,进程结束分为,代码执行完毕,任务完成与不完成,另外一种就是信号异常杀死。

而这个杀进程,就属于异常结束。

我们可以通过waitpid的输出参数status找到子进程退出的信号来验证:

cpp 复制代码
int main()
{
    int fd[2];
    int ret=pipe(fd);
    if(ret<0)
    {
        std::cerr<<"pipe error"<<std::endl;
        return 1;
    }

    int id = fork();
    //创建进程后,父子进程需要各自关闭一个读写端
    //我们这里让子进程写,父进程读
    if(id==0)
    {
        //子进程
        ::close(fd[0]);
        int total = 0;
        int cnt=0;
        while(1)
        {
            std::string message="h\n";

            total += ::write(fd[1], message.c_str(), message.size());
            cnt++;
            std::cout << "total: " << total << std::endl;
            //继续无限循环break;//结束循环
        }

        exit(0);
    }
    else if(id>0)
    {
        //父进程
        ::close(fd[1]);

        // char buffer[1024];
        // //sleep(10);
        // while(1)
        // {
        //     ssize_t s=read(fd[0],buffer,1024);
        //     if(s>0)
        //     {
        //         buffer[s]=0;
        //         std::cout<<"parent read:"<<buffer<<std::endl;
        //     }
        //     else if(s==0)
        //     {
        //         std::cout<<"child quit"<<std::endl;
        //     }
        //     sleep(1);
        // }
        ::close(fd[0]);//关闭读端
        int status=0;
        pid_t pid=waitpid(id,&status,0);

        std::cout<<"child quit signal:"<<((status>>8)&0xFF) << " child quit code:"<<(status&0x7F)<<std::endl;
    }
    return 0;
}

我们可以看见,进程结束的非常快,所以没有进行子进程的无限循环,且子进程的退出信号为13!!

所以:

现象4:写端正常&&读段关闭,操作系统会杀死写端进程

根据以上实现,我们可以总结出匿名管道的五个特性:

1、面向字节流

2、用来进行具有血缘关系的进程,如父子

3、文件的声明周期随进程结束而结束,管道也是(所有相关进程结束时,内核自动回收管道缓冲区,未读取的数据永久丢失)

4、单向数据通信

5、管道自带同步互斥等保护机制 !(现象3、4)

以上就是我们匿名管道的四大现象五大特性!!!!


总结:

我们今天的文章主要就进程间通信进行了一个开篇,并介绍了一下匿名管道的特性与现象,希望对大家有所帮助。下篇文章我们将会为大家介绍的主要使用匿名管道的例子:进程池 ,有兴趣的可以关注一下。

有疑问的大家可以在评论区或者私信我,谢谢大家!!!

相关推荐
律宏阔13 分钟前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔13 分钟前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴14 分钟前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千39 分钟前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei3740 分钟前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei3740 分钟前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei3741 分钟前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW1 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW1 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux
yunwei371 小时前
eBPF 开发者教程: 简单的 XDP 负载均衡器
linux·后端·性能优化