Linux-进程间通信1

**摘要:**本文围绕 Linux 进程间通信(IPC)展开,首先从进程独立性与协作需求出发,说明为什么需要进程间通信,并介绍其四大目的(数据传输、资源共享、通知事件、进程控制)以及发展历程(管道、System V、POSIX)。随后重点讲解管道这一最基础的通信方式,包括其底层原理(父子进程通过 fork 继承文件描述符建立通信信道)、五大特性(血缘关系、单向通信、生命周期随进程、面向字节流、自带同步机制)以及四种典型读写情况,最后给出管道写入规则与进程池的引出。

目录

1.进程间通信介绍

1.1摘要

1.2VsCode

1.2.1VsCode配置

1.2.2在VSCode写代码

1.3进程间通信目的

1.4进程间通信发展

1.5进程间通信分类

2.管道

2.1概念

2.1.1回顾之前

2.1.2管道的底层原理

2.2管道的demo代码

2.3管道的四种情况和五大特性

2.3.1五大特性

2.3.2四种情况(除正常情况以外)

2.3.2.1读端不关,写端不写

2.3.2.2读端不读,但不关闭

2.3.2.3写端写一会关闭,读端不关

2.3.2.4读端关闭,写端正常

2.4管道写入规则


1.进程间通信介绍

1.1摘要

为什么要有进程间通信,什么是进程间通信?

我们之前讲过很多进程相关的话题,但是它们都只是停留在单个进程的东西,虽然我们讲过父子进程,但是这两个关联性并不强,后续子进程执行的代码可能和父进程完全不一样。

如果未来进程之间要协同呢?如:A进程把数据处理完后要把数据交给B进程加工(也有可能B进程把数据再交给C进程去输出),这就叫做协作关系,一个进程负责把数据(从外设)读取,另一个进程负责对数据的加工处理......因此,未来的进程之间可能要基于数据进行协同。

怎么进行协同呢?一个进程要把自己的数据交给另一个进程才能进行协同!

但是我们之前讲进程的特性的时候,进程具有独立性,因此就决定了如果要把一个进程的数据交给另一个进程 ------ 基本不可能(从目前来看)!哪怕是父子进程,遇到数据改变就会发生写时拷贝!

因此要完成进程间通信,就必须要在两个通信的进程之间有另一个人参与!这个人就必须是操作系统!进程A把数据交给操作系统,然后进程B再向操作系统要数据,此时就完成了进程间的通信!

①进程间通信的前提是先让不同的进程看到同一份资源,而这份资源大部分是由操作系统提供的!因此此时的资源既不属于进程A也不属于进程B ,而是属于操作系统的资源,这就不会破坏进程的独立性!

②猜想:同一份资源一定是某种形式的内存空间!

此外,我们还需要换编辑环境,之前我们是用vim+makefile+C语言+CentOS来完成学习的,后续我们需要使用vscode+makefile+g+++C+++ubuntu20.04+ssh+cgdb来开发!

1.2VsCode

vscode是一款文本编辑器,只不过这个文本编辑器是基于插件的,vscode针对C/C++开发是特别简单的,即下即用,最好使用vscode本地化配置,后续要使用的是vscode+云服务器。

1.2.1VsCode配置

vscode官网:https://code.visualstudio.com/Download

我们是安装在自己电脑上的Windows下的。

我们需要下载并配置MinGW-w64编译器套件,下载地址:https://sourceforge.net/projects/mingw-w64/files/

要安装的话根据箭头指引:

点击即有:

下载成功后我们到下载的目录去解压:

然后我们就能看到这个文件夹了:

此时我们再点开:

我们复制这个文件到D盘根目录或你想要的磁盘根目录即可!

完成之后我们点开就有:

这些就是我们之前在Linux下的很多工具!

接下来我们就需要配置了,我们需要按键盘上的Win+S或者打开搜索框搜索环境变量:

双击:

我们需要把刚刚的那个bin给复制到里面:

此时我们就可以通过以命令行的方式去使用这些指令了,我们要验证的话打开命令行提示符:

输入gcc --version即可:

这个就代表配置好了!

然后我们就需要在VSCode下载C/C++插件:

搜索C/C++即可,下载这个版本,由于我已经安装完成,如果没有安装的直接安装即可,安装后需要重启VSCode!

1.2.2在VSCode写代码

VsCode是本地的代码编辑器,我们可以在桌面新建一个文件夹:

我们把这个文件夹命名为code方便后续写代码:

然后我们打开VsCode,点开上面的文件:

选择打开文件夹选到自己刚刚的那个code:

然后我们就可以新建文件了:

此时创建一个文件写下代码:

因此未来我们就可以用VsCode自由写了(但是此时是不能运行的)。

我们需要安装一个ssh插件:

一旦我们把这个插件安装好后左边有一个图标:

然后我们点击有:

然后我们有云服务器就用云服务器登录XShell,有虚拟机就用虚拟机,选择用户登录复制这些:

然后复制这段地址,并且在VsCode搜索框输入:

bash 复制代码
ssh 你登录的用户名@你复制的东西

即可:

回车选择第一个:

确认后右下角出现:

打开配置有:

此时我们就添加成功了

选择Linux输入密码有:

并且我们点击之前的那个电视图标有:

打开文件夹有:

但是此时我们想要切换账号怎么办,依旧之前的操作即可,只是需要改一下登录用户名!

我们打开8.8目录有:

但是我们现在是看不到这个文件夹的,因为上面提示要输入密码,我们输入密码回车有:

我们想要新建一个文件直接点

然后新建一个hello.cpp:

此时我们新建成功了就会在Xshell看到:

我们写下如下代码:

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
    cout << "hello world" << endl;
    return 0;
}

然后我们在VsCode按Ctrl+S就能保存,并且我们就能在Xshell看到:

因此,从今天开始我们写代码都在VsCode写!

虽然我们可以在那个VsCode配置免密码操作,不然每次切换文件都需要输入密码,如果想要配置免密码的可以自己去查找AI大模型,这里就不做过多阐述了!

其实我们安装的插件默认会安装到自己的云服务器上(Linux),且是Linux版本。头文件本质是函数和变量的声明的文本集合。

1.3进程间通信目的

进程间通信目的其实就是问为什么要去进程间通信?

主要有以下四个原因(目的):

①数据传输:⼀个进程需要将它的数据发送给另⼀个进程;

②资源共享:多个进程之间共享同样的资源,如果多进程之间可能有一部分公共数据,假设多个进程要同时共享同一个配置文件;

③通知事件:⼀个进程需要向另⼀个或⼀组进程发送消息,通知它(它们)发⽣了某种事件(如进 程终⽌时要通知⽗进程),比如,我们想让一个进程听另一个进程的号令,A进程控制BCD三个进程,让BCD运行或者不运行都是由进程A决定的,因此结合下面的进程控制就能完成控制进程的开始结束;

④进程控制:有些进程希望完全控制另⼀个进程的执⾏(如Debug进程),此时控制进程希望能够 拦截另⼀个进程的所有陷⼊和异常,并能够及时知道它的状态改变。比如我们之前学过用gdb指令去控制某个进程(控制另一个进程完成调试功能)。

实际上,进程间通信就和每个人之间的通信一样,没必要死记硬背,我们只需要抽象化记忆即可!

1.4进程间通信发展

实际上,写操作系统的程序员也觉得如果一直开发新功能来实现某种技术,那样效率太低了,因此就可以在老的代码里直接实现新的功能,即我们采用现成的技术去实现其他功能。

第一阶段,进程间通信用到的是管道!管道是基于文件的通信方式,即因为早期程序员比较"懒",所以就复用了文件相关的代码。

第二阶段,人们发现复用文件的代码这种方式可能不太支持通信间的另一些场景,因此人们想到能不能在操作系统内单独设计通信模块呢?因此就设计了这个进程间通信的模块,它不属于操作系统核心大功能模块,属于操作系统内小模块,就有了System V进程间通信。

第三阶段,随着网络的出现,就需要进程间通信有网络功能,因此就设计出网络间进程通信:POSIX进程间通信。

System V:由于操作系统早期是某个小组来实现一个功能最后再组成起来,当时是有很多操作系统的,如果一个操作系统的程序员小组实现了这个功能,那么其他的操作系统也需要这个功能,因此就需要定标准来保证每个操作系统都能够使用,如果不定标准,那么操作系统只能在某个区域使用,跨区域就没办法使用了,因此需要所有实现这个功能的操作系统选择出一个实现该功能最优的操作系统,其他操作系统统一使用它的标准,因此只要定了标准,地球上每个地区的进程都可以进行通信了(相当于把实现这个功能的代码每个操作系统都拷贝一份),此时对于每个操作系统来说,使用的接口都大差不差,可以减少程序员的学习成本!而这个标准命名成为System V我们叫做系统V版本!(具体的历史可以去网上搜)

但是随着后期网络的发展,这个System V标准只能进行本地通信即只能在你自己一台机器内部进行通信,因此就需要进行网络间通信了,它也需要定标准,但是具体的标准需要后面再讲!实际上本地通信就代表通信在Linux/MacOS等操作系统都支持这个功能!

实际上,定标准并不是随便定的,往往定标准的都是具有一定的话语权或具有绝对统治地位的组织或者公司甚至是个人由这些人来定标准,但是这些定标准的人并不写代码,而是由其他人来实现!

1.5进程间通信分类

管道:匿名管道pipe,命名管道;System V IPC:System V消息队列,System V共享内存,System V信号量;POSIX IPC:消息队列,共享内存,信号量,互斥量,条件变量,读写锁。

以后我们讲解进程间通信就按照这样的顺序来讲(Linux下System V技术已经淘汰了,只会讲解一小部分)。

2.管道

管道通信是基于文件系统的!

2.1概念

2.1.1回顾之前

我们可以在命令行输入:

bash 复制代码
who | wc -l

who能查看系统当中有多少人在使用(Xshell + VMware)登录系统:

wc -l是用来统计是输出的文本里面有多少行,而|代表管道,结合起来who | wc -l是统计当前有几个人在访问Linux系统,因为我是虚拟机,我在虚拟机上用root登录了,所以就有2个人。

我们知道,Linux命令行输入指令后回车就会把指令变成一个进程,who回车之后就是一个进程,wc回车之后也是一个进程,之前我们讲过|是Linux里的管道,因此我们的命令who | wc -l启动之后,who是一个进程,wc -l也是一个进程,|会被操作系统解释成一个叫做管道的东西,所以这个就相当于基于管道进行进程间通信的一种场景!

我们虽然不咋理解管道是什么,但是我们可以把|两端的视为进程。那么这两个进程之间有什么关系呢?

sleep指令可以让系统休眠(单位:s),因此我们输入:

bash 复制代码
sleep 1000 | sleep 2000 | sleep 3000

新开一台机器输入以下命令:

bash 复制代码
ps axj | grep sleep

然后看结果:

然后我们发现此时进程的ppid都是1760,因此我们输入:

bash 复制代码
ps axj | grep 1760

有:

因此我们可以认为,用管道连接的几个进程,它们的父进程相同(都是bash),它们之间是兄弟进程!

2.1.2管道的底层原理

我们站在文件角度来理解一下,我们知道一个进程都要有自己的文件描述符表struct files_struct,文件描述符表里最核心的是数组,这个数组叫做struct file* fd_array,里面存放每个被打开文件的文件描述符地址,当进程打开一个文件,它会在内核层面上为它创建一个struct file。那么如果一个进程创建一个子进程,那么它会不会为子进程创建一个struct files_struct呢,肯定是要的,而且是基于父进程进行浅拷贝,那么父进程打开的文件这部分要不要给子进程拷贝一份呢?

不用拷贝,因为父进程用fork创建子进程只是完成进程创建,并不是文件创建,只需要拷贝进程相关的结构即可,这也就是为什么父子进程打印都会往同一个显示器打印的根本原因,本质是显示器文件被共享了!

进程间本质是让两个进程看到同一份资源,现在父子进程有没有看到同一份资源呢?

可以看到了,因为父进程可以往内存块里写,子进程从内存块里读,因此它们两个构建了一个通信信道,这种通信信道我们叫做管道!管道的本质是父子进程通过fork继承打开的文件实现建立通信信道的过程!

但是这样会有很多问题,如:

①我们打开的文件一般都是普通文件,它往往需要我们把写入的数据加载到磁盘里,而进程间通信,如果父子进程间通信,它通过文件描述符往管道写入,写入的消息再通过文件描述符让另一个进程去读取,父进程要把自己的数据交给子进程,本质上并不需要把数据进行IO刷新,因为做IO刷新会让通信速度变慢了,我们本质是让一个进程把数据交给另一个进程,而普通文件需要把数据刷新到外设的,这样效率太低了!

②文件的读写位置其实只有一个,即父进程往管道缓冲区里写的时候,如果写了一百个数据,那么此时下标值应该为101(文件的读写位置变为101),而子进程在读的时候,如果两者共用一个struct file,读写位置只有一个的话,父进程写到101了,子进程再从101读它是读不出数据的,我们期望的是父子进程有独立的写入/读取位置,因此它们两个进程缓冲区的位置的读取位置应当是分开的,此时双方就能更好的进行通信,因此我们一般不以可读可写的方式打开文件,因为读写位置只有一个,所以如果要切换操作需要把写入的下标切换到文件开头,因此真实情况下管道是做了特殊处理的,它会给父子进程都拷贝一份struct file,此时就能把父子进程的读写位置分开。

管道的基本原理是让子进程继承父进程打开的文件,但是之后管道会给子进程struct file拷贝一份,只不过它们两个的struct file指向相同的文件对应的内容!

①父进程打开文件时,它以读和写方式打开同一个管道文件;

②父进程创建子进程,子进程继承父进程文件描述符表结构,此时子进程也以读写方式打开管道文件了;

③父子进程关闭自己不需要的读写端,如:父进程写入,子进程读取,我们把得到的数据交给子进程,然后让父子进程关闭掉不需要的文件描述符,如:父进程只想写就保留4,子进程只想读,就保留3,此时就形成了一个进行单向通信的信道,这个单向通信是基于文件的通信方式叫做管道!

可能有几个疑问:

Ⅰ为什么父进程要用读和写同时打开一个管道文件呢?

如果父进程只以读的方式打开,那么将来让子进程继承下去的文件打开方式也只能是读;同理,如果父进程只以写的方式打开,那么将来让子进程继承下去的文件打开方式也只能是写,此时这两个进程就没办法完成通信。本质:让子进程继承读写方式!

Ⅱ为什么要关闭读写端呢?

主要是管道只需要进行单向通信。其实不关闭也可以通信,但建议关闭,因为这样可以防止误操作,因为管道只能进行单向通信,万一使用者本来只能给父进程去写,但是却用父进程去读,那么写入的数据就相当于给父进程了,而本来就是父进程写入的准备交给子进程,这样就会导致父进程自己在通信,并没有交给子进程!

Ⅲ为什么要让父进程写?

这个是基于自己的需求和应用场景的,我们完全可以让子进程来写,父进程来读,没有一定要让父进程来写!

Ⅳ如果我想让父子进程双向通信呢?

我们可以创建两个管道,一个叫做写管道,一个叫做读管道!

Ⅴ管道为什么要单向通信?

管道通信本身定位就是简单快速易上手,设计成单向通信的就是因为这样简单快速易上手!因此我们才把这种方式叫做管道!管道叫做管道是因为设计之初它是单向通信的,所以名字叫管道,而不是它是管道所以才设计成单向通信!

Ⅵ现实生活中有没有管道的例子?

现实生活中,管道都是单向的,而不是双向流动!

2.2管道的demo代码

管道相关的系统调用:

cpp 复制代码
int pipe(int pipefd[2]);

pipe函数,参数:含有两个元素的数组(输出型参数);返回值:成功就返回0,失败就返回-1。因为012被标准输入输出流占用,所以一般会返回两个文件描述符为3和4!

我们可以在Makefile写下如下代码:

bash 复制代码
testpipe:testpipe.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f testpipe

在testpipe.cc写下如下代码:

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<unistd.h>
int main()
{
    //创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    //验证是否创建管道成功
    if(n == 0)
    {
        //应该是3、4
        printf("create pipe success:pipefd[0]:%d,pipefd[1]:%d\n",pipefd[0],pipefd[1]);
    }
    return 0;
}

我们在VSCode里按Ctrl+~能调出VSCode的命令行:

然后我们就可以make编译运行:

因此,pipefd的第一个元素保存的是读文件描述符,第二个元素保存的是写文件描述符,这个是固定的(抽象记忆:0相当于o,用嘴巴读,就是读;1相当于|,用笔写,就是写)!

我们打开了文件了吗?有路径吗?有文件名吗?

打开了!但是没有路径连文件名都没有,我们之前打开文件需要fopen,关闭文件要fclose,这里都没有啊,因此没有指明文件名,也没有指明路径,它是内存级文件,没有名字我们叫做匿名管道文件,相当于操作系统模拟的文件,因此就不存在往磁盘里刷新!

因此管道定义:管道是基于文件系统的一个内存级的单向通信,主要用来进行进程间通信(IPC)的!IPC:Inner-Process Communication (进程内部的交流)。

2.3管道的四种情况和五大特性

2.3.1五大特性

匿名管道特性:①常用于具有"血缘"关系进程间进行通信,除了父子进程之外,兄弟进程,爷孙进程之间也能用匿名管道进行通信,常用于父子进程!②单向通信。

但是我们好像只有父进程给子进程通信,没有反过来啊!

如果想让子进程写,我们需要了解一下read函数和write函数:

然后把代码改为:

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

int main()
{
    //1.父进程创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    (void)n;//防止编译器报警
    //2.fork创建子进程
    pid_t id = fork();
    if(id < 0)
    {
        //子进程创建失败
        perror("fork");
        exit(1);
    }
    //3.父子关闭不需要的fd
    else if(id == 0)
    {
        //子进程
        //关闭不用的文件描述符(用close)
        close(pipefd[0]);
        //给父进程发送一堆数据
        std::string str = "我是子进程";
        std::string self = std::to_string(getpid());
        int cnt = 1;
        std::string message = str + "," + self + ",";
        while(true)
        {
            //发送不一样的信息
            message += std::to_string(cnt++);
            //往管道里写
            write(pipefd[1],message.c_str(),message.size());
            sleep(1);
        }
    }
    else
    {
        //子进程给父进程发送的是字符串
        //父进程
        //关闭不用的文件描述符
        close(pipefd[1]);
        while(true)
        {
            //文件缓冲区
            char inbuffer[1024] = {0};
            //传入的数据并没有\0,因此我们还需要留一个位置存放\0(防止读取超过1023个)
            ssize_t n = read(pipefd[0],inbuffer,sizeof(inbuffer)-1);
            if(n > 0)
            {
                inbuffer[n] = 0;
                //只有父进程会往显示器打印数据
                std::cout << "client->father# " << inbuffer << std::endl;
            }
        }
        //等待子进程结束
        pid_t rid = waitpid(id,nullptr,0);
        (void)rid;
    }
    //前面都是在做建立(单向)通信的信道,还没有通信
    //4.传递数据
    return 0;
}

然后观察一下:

子进程的pid为2500,父进程的pid为2499,且子进程能给父进程传输信息!

但是父进程定义全局数据,本来就可以被子进程看到啊,为什么还需要通信呢?

①子进程的数据并不能被父进程看到;②父子通信的时候如果要对数据进行修改,如果是用全局的方案那么就会发生写时拷贝,没办法让另外一方看到修改后的数据,此时的数据就能实时被对方看到!

如果父子进程都退出了,管道怎么办?

它们两个创建的管道会被操作系统自动回收,因此第三大特点:③管道的生命周期随进程!

如果我们在原来的基础上让父进程每隔5秒读一次信息会发生什么情况?

子进程会把数据写到缓冲区里,可能写了5次,但是父进程去读取的时候,就可以一次性从缓冲区把所有信息全部读取,站在父进程角度,它并不区分子进程曾经写的是不是多少个字符串,而是在自己读取的范围内,把你写的数据全部读取上来,即:如果我写了1000个字符,分10次写,但是我们读取可能一次就能读取完毕这1000个字符!即通信双方并没有关于读写的格式的问题,因此,管道的第四个特性:④面向字节流。(文件也是基于字节流的,往文件写入时很简单,但是从文件读的时候比较难)我们可以暂时理解成读写次数并没有强相关,读取数据是按照自己的需求来,想怎么读取就怎么读取,这个记住即可!

运行代码的时候我们发现,我们并没有让父进程休眠啊,为什么父进程还是一条一条的打印?

因为子进程写一条父进程才能读一条,只要子进程不写,那么父进程就读不到,因此在读不到的时候,管道里面是空的,因此操作系统不让父进程去读,因此管道的第五个特点:⑤管道自带同步机制。类似学校里如果有书到货了,需要喊同学去搬书,搬书的过程中我们是上不了课的,只有把书搬回来,人和书都齐了符合上课条件了,才能继续上课。相当于在等待子进程写数据,在等同学回来的过程中相当于做同步,得等同学把书搬回来才行;如果是同学搬书的过程中继续上课,上课和同学搬书的两个事情同步推进,这叫做异步!因为子进程还没有写数据,当前的父进程就需要在read这里阻塞住,需要等子进程再把数据写进来才能读,因此就是管道内部实现了同步机制(多线程会深入讲解)。

2.3.2四种情况(除正常情况以外)

2.3.2.1读端不关,写端不写

当父进程发现管道没有数据了就阻塞住了,父进程阻塞的本质是什么?

我们调用read是系统调用,它会让操作系统去查看管道里是否有数据,相当于操作系统发现管道文件缓冲区大小为0,因此操作系统在内部会把当前进程设置为阻塞状态,直到管道里有数据才会继续运行。

第一种情况结论:Ⅰ管道里面没有数据,读端就会被阻塞;(读端不关,写端不写)

2.3.2.2读端不读,但不关闭

把代码改为:

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

int main()
{
    //1.父进程创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    (void)n;//防止编译器报警
    //2.fork创建子进程
    pid_t id = fork();
    if(id < 0)
    {
        //子进程创建失败
        perror("fork");
        exit(1);
    }
    //3.父子关闭不需要的fd
    else if(id == 0)
    {
        //子进程
        //关闭不用的文件描述符(用close)
        close(pipefd[0]);
        //给父进程发送一堆数据
        std::string str = "我是子进程";
        std::string self = std::to_string(getpid());
        int cnt = 0;
        std::string message = str + "," + self + ",";
        while(true)
        {
            char c = 'A';
            write(pipefd[1],&c,1);
            std::cout << "count: " << cnt++ << std::endl;
        }
    }
    else
    {
        //子进程给父进程发送的是字符串
        //父进程
        //关闭不用的文件描述符
        close(pipefd[1]);
        while(true)
        {
            sleep(1);
        }
        //等待子进程结束
        pid_t rid = waitpid(id,nullptr,0);
        (void)rid;
    }
    //前面都是在做建立(单向)通信的信道,还没有通信
    //4.传递数据
    return 0;
}

此时子进程会一直写,直到把管道文件写满为止:

我们发现它写到65535就结束了,实际上就相当于2^16-1即64KB,因此结论:

读端不读,但不关闭,把管道文件写满了就不再写入了!

2.3.2.3写端写一会关闭,读端不关

把代码改为:

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

int main()
{
    //1.父进程创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    (void)n;//防止编译器报警
    //2.fork创建子进程
    pid_t id = fork();
    if(id < 0)
    {
        //子进程创建失败
        perror("fork");
        exit(1);
    }
    //3.父子关闭不需要的fd
    else if(id == 0)
    {
        //子进程
        //关闭不用的文件描述符(用close)
        close(pipefd[0]);
        //给父进程发送一堆数据
        std::string str = "我是子进程";
        std::string self = std::to_string(getpid());
        int cnt = 10;
        while(cnt)
        {
            sleep(1);
            char c = 'A' + cnt;
            write(pipefd[1],&c,1);
            cnt--;
        }
        //关闭写端
        close(pipefd[1]);
        std::cout << "写端关闭" << std::endl;
        sleep(5);
        exit(0);
    }
    else
    {
        //子进程给父进程发送的是字符串
        //父进程
        //关闭不用的文件描述符
        close(pipefd[1]);
        while(true)
        {
            sleep(1);
            char c;
            int n = read(pipefd[0],&c,1);
            if(n>0)
            {
                std::cout << "读到数据: " << c << std::endl;
            }
            else
            {
                std::cout << "return val : " << n << std::endl;
            }
        }
        //等待子进程结束
        pid_t rid = waitpid(id,nullptr,0);
        (void)rid;
    }
    //前面都是在做建立(单向)通信的信道,还没有通信
    //4.传递数据
    return 0;
}

有以下的结果:

我们发现,写端写完关闭后,并没有让读端继续阻塞,而是让read返回了,如果多加个else if(n==0)执行打印return val的语句那么和这个一样的结果,因此:写端写完后关闭,读端会读到返回值为0表示读到文件结尾!

2.3.2.4读端关闭,写端正常

如果只往管道文件里写,不读取,那么写的内容一点意义都没有,因此操作系统会自动杀掉这个进程。

为什么?

操作系统不会做无效操作,因此发现你在浪费时间和空间的事情,因此就会发个信号把这个进程杀掉(后面讲)。

我们可以把代码改为:

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

int main()
{
    //1.父进程创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    (void)n;//防止编译器报警
    //2.fork创建子进程
    pid_t id = fork();
    if(id < 0)
    {
        //子进程创建失败
        perror("fork");
        exit(1);
    }
    //3.父子关闭不需要的fd
    else if(id == 0)
    {
        //子进程
        //关闭不用的文件描述符(用close)
        close(pipefd[0]);
        //给父进程发送一堆数据
        std::string str = "我是子进程";
        std::string self = std::to_string(getpid());
        //int cnt = 10;
        //std::string message = str + "," + self + ",";
        while(true)
        {
            char c = 'A';
            write(pipefd[1],&c,1);
        }
        //关闭写端
        close(pipefd[1]);
        std::cout << "写端关闭" << std::endl;
        sleep(5);
        exit(0);
    }
    else
    {
        //子进程给父进程发送的是字符串
        //父进程
        //关闭不用的文件描述符
        close(pipefd[1]);
        //读端关闭
        close(pipefd[0]);
        std::cout << "读端关闭: " << std::endl;
        //等待子进程结束
        //接受子进程的退出码
        int status = 0;
        pid_t rid = waitpid(id,&status,0);
        if(rid > 0)
        {
            //获取子进程退出码
            int exitcode = (status >> 8) & 0xFF;
            //获取退出码
            int exitsig = (status & 0x7F);
            //预期结果exitsig = 13(代表是被操作系统杀掉的)
            std::cout << "wait success : " << rid << "exit code : " << exitcode << ",exitsig : " << exitsig << std::endl;
        }
    }
    //前面都是在做建立(单向)通信的信道,还没有通信
    //4.传递数据
    return 0;
}

此时我们运行一瞬间就会有:

因为退出信号为13,所以代表是被信号杀掉而退出!

2.4管道写入规则

实际上,单次向管道里写入,写入的字节数小于PIPE_BUF写入,写入操作就是原子的,原子具有不可分割性,因此要么不做要么就做完,没有第三状态,当进行原子操作时不会被任何人打扰!一般这个PIPE_BUF为4096,当我们写入的数据小于4096的时候,读端并不会打扰你写入数据;同理,如果读端正在读,写端也不能干扰读端,必须等读端读完才行!因此我们一次性往管道写入的数据量最好不要超过4096!

一个父进程可以创建若干个子进程,只要父进程不往管道里写入数据,那么子进程就会一直在read阻塞住,因此,结论①:父进程可以通过向管道里写入数据,来控制子进程的启停!

只要等到父进程往管道里写入数据,那么子进程会继续执行后续代码,这样可以做到父进程给子进程派发任务,且子进程大部分时间都在等待任务!

结论②:父进程和子进程规定:一个4字节整数,整数不同,表示不同的任务。因此我们可以通过控制父进程给子进程的数字来要控制子进程完成的任务!因此我们可以让父进程给子进程派发对应的任务让子进程去执行,这就是进程池!

进程池的实现放到下一篇博客讲解,进程间通信剩余部分放到之后讲解!

相关推荐
天远Date Lab2 小时前
零信任架构实战:基于天远柠檬查维保构建自动化全国连锁二手车置换上架电子维保报告网关
运维·人工智能·架构·自动化
张宇Joaquin2 小时前
Linux系统下磁盘扇区512n、512e和4kn的区别
linux·运维·数据库
道虽迩,不行不至2 小时前
数据库自动连库执行增量脚本-Linux
linux·数据库
GeW2 小时前
新质生产力发展路线图来了,Linux和数据库人该怎么接
linux·数据库
IDCPDU2 小时前
RS485 总线上的 PDU 总丢包:拓扑、终端电阻和地址冲突怎么查
运维
西索斯coding2 小时前
Grok 4.7 调用一直 401 怎么办?不是 Key 过期,是 Authorization 头格式问题——排查全流程 + 修复示例
服务器·开发语言·ai·php
yunwei372 小时前
eBPF 示例:使用 CUPTI 构建 GPU 火焰图分析器
linux·后端·性能优化
const_qiu2 小时前
第四章 07:4.7 本章小结
java·运维·服务器·人工智能
张宇Joaquin3 小时前
自研SP600系列网卡驱动跟麒麟V10 SP3 2403配套关系
linux·运维·网络