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是一个⽂件描述符数组,其中fd[0]表⽰读端, fd[1]表示写端,这个数组参数是一个输出型参数,我们需要自己创建一个数组然后把这个传进去。

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

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

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

为什么我们要父子进程一个关系写,另外一个就要关闭读呢?这样是防止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)

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


总结:

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

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

相关推荐
这我可不懂8 分钟前
Python 项目快速部署到 Linux 服务器基础教程
linux·服务器·python
车车不吃香菇42 分钟前
java idea 本地debug linux服务
java·linux·intellij-idea
tan77º1 小时前
【Linux网络编程】Socket - TCP
linux·网络·c++·tcp/ip
kfepiza2 小时前
Linux的`if test`和`if [ ]中括号`的取反语法比较 笔记250709
linux·服务器·笔记·bash
CodeWithMe2 小时前
【Note】《深入理解Linux内核》 第十九章:深入理解 Linux 进程通信机制
linux·运维·php
vvw&3 小时前
Linux 中的 .bashrc 是什么?配置详解
linux·运维·服务器·chrome·后端·ubuntu·centos
tao3556674 小时前
树莓派免密登录(vs code/cursor)
linux·嵌入式硬件·ssh
是阿建吖!4 小时前
【Linux | 网络】socket编程 - 使用UDP实现服务端向客户端提供简单的服务
linux·网络·udp
Clownseven5 小时前
SFTP服务器搭建实战:腾讯云 Linux 上的快速安全文件传输方案
linux·服务器·腾讯云
chuanauc5 小时前
记录一次在 centos 虚拟机 中 安装 Java环境
java·linux·centos