进程间通信

进程间通信

是什么?

两个或者多个进程实现数据层面的交互

在这里插入图片描述

为什么?

在这里插入图片描述

怎么办?

a.进程间通信的本质:必须让不同的进程看到同一份"资源"
在这里插入图片描述

b."资源"就是特定形式的内存空间

c.这个"资源"谁提供?一般是操作系统,
为什么不是我们两个进程中的一个呢?
假设一个进程提供,这个资源属于谁?
这个进程独有,破坏进程独立性。所以一定得是第三方空间由OS提供!

d.我们进程访问这个空间,进行通信,本质就是访问操作系统!
进程代表的就是用户,一般而言“资源”从创建,使用,释放–OS不允许用户直接访问内部的资源,所以都是调用–系统调用接口!
从底层设计,从接口设计,都要由操作系统独立设计
一般操作系统,会有一个独立的通信模块—隶属于文件系统—IPC通信模块
定制标准—-进程间通信是有标准的
system V(本机内部) && posix(网络)

e.基于文件级别的通信方式–管道(既不属于system V也不属于posix)

1、直接原理
OS创建一个文件,有struct file, inode , 操作方法file_operators , 缓冲区
但是不需要打开磁盘文件,让这个文件成为内存级文件,也就是按照打开文件这套流程,并不是在磁盘中真正存在对应的文件,和磁盘没关系
操作方法匹配缓冲区读写,我这个文件不需要向磁盘刷新,这个内存级文件只存在内存中
在这里插入图片描述
打开这个文件时,在创建一个子进程,子进程重新创建task_struct ,也要拷贝files_struct文件描述符表里面的内容,因为这个表属于进程的数据结构,但是打开的文件struct file对象不会再拷贝一份,因为我们这是创建子进程,进程和文件是两码事,文件属于文件系统,由OS打开的

子进程也指向同样的struct file文件资源,尤其是新打开的内存级文件
在这里插入图片描述
此时父子进程看到同一份资源,这不就是通信的前提条件吗
管道就是文件
问题
如果此时父进程把对应的内存级文件关了,此时会不会影响到子进程呢?
不会,因为struct file维护了引用计数,当没人引用它时才会释放结构体
在这里插入图片描述
但现在还是有坑的,当父进程只读打开文件的,子进程继承后打开也是只读,那怎么通信呢?
所以父进程就不能这么草率 的打开文件了
在这里插入图片描述

系统中父进程打开对应管道文件时,并不是单方面打开或读或写,而是既以读方式打开,又以写方式打开
打开同一个管道文件

子进程拷贝父进程的文件描述符表,那子进程也以读写方式指向同一个文件

接着我们需要结合具体场景,你到底让父进程读子进程写,还是反过来,此时要父进程子进程各自关闭一个读写端来形成单向通信的信道


不管读还是写,文件都必须先加载到内存,也就会有文件页缓冲区,今天重点就是这个缓冲区

以读方式打开一个文件,创建了一个struct file r ,今天把同一个文件在进程里再打开一次,在OS层面上还是要再创建一个struct file w,两个文件对应的读写方式不一样,每一个文件里都有自己的读写位置,如果读写混用很容易出问题的,只不过这两个struct file指向同一个文件的inode,同一个文件的file_operators,同一个文件的缓冲区
今天管道是要通信的,不能让两个父子进程访问同一个读写位置,我需要两个文件读写位置,这样大家不干扰
那就有两个Struct file结构体
在这里插入图片描述
现在父子都有读写,规定只想让父子进行单向通信
如果保留父子的读写,还得确认哪些数据是父进程写的,哪些是子进程写的,这不就复杂了,设计上把文件这一套拿过来通信,而没有设计复杂的通信模块就是为了简单
所以到底谁读谁写,就由用户决定谁读谁写
我今天就想让子进程进行写入,父进程进行读取,我该怎么办?
那就让子进程4号文件描述符去写,父进程3号去读,现在就没关闭父进程的写,子进程的读,这样也不是不行,但是万一误操作就会影响子进程的写,所以建议关闭父进程的写,子进程的读,struct file的引用计数由2变为1
在这里插入图片描述

这就是管道通信的原理
正是因为他的通信理念本着简单只能进行单向通信,所以把它命名为管道!
为什么是单向通信的?
1、他是基于文件通信的
2、设计者本来就是为了简单,图省事

如果我要进行双向通信呢???
可以创建多个管道,那不就有读写了

如果没有任何关系,可以用我们刚刚讲的原理进行通信呢?
不能
必须是父子关系,子进程才能拷贝一份独立的文件描述符表才能看到父进程的内存级文件
兄弟关系也可以,父进程的文件描述符表能让所有兄弟进程都拷贝一份
子进程再创建一个子进程,那就是孙子进程,父进程给了子进程,子进程给了孙子进程
使用管道通信,进程之间需要具有血缘关系,常用于父子
但是不能理解为只有一套文件描述符表,而是每个进程都拷贝了父进程文件描述符表,各有一份

这个内存级文件有没有名字?有没有路径?有没有inode?
通通都没有,因为这个文件不需要名字,父子进程看到同一个份资源是采用继承父进程资源得到的,他不需要任何路径去标识它,不需要任何文件名来区分它,更不需要inode
所以这个管道就叫匿名管道

至此,通信了吗???
没有,我们只是建立通信管道 ------- 为什么这么费劲??答:进程具有独立性,通信是有成本的!

2、接口
int pipe( int pipefd[2] )形参是输出型参数,成功返回0,失败返回-1
****加粗样式****

pipe系统调用由父进程以读写方式打开内存级文件,并且把文件stuct file和文件缓冲区创建好
形参把打开文件的2个文件描述符数字带出来,让用户使用,一定是3和4
pipefd[ 0 ] : 读下标 张嘴读书
pipefd[ 1 ] : 写下标 用笔写字

3、编码实现
在这里插入图片描述
pipe调用后就相当于做了下面这件工作,打开两个文件描述符,创建好缓冲区
在这里插入图片描述
接下来就要fork创建子进程了
在这里插入图片描述
此时相对于做了第二件工作
在这里插入图片描述
根据通信需求子进程写,父进程读,来关闭对应的文件描述符,struct file引用计数由2减到1
在这里插入图片描述
至此完成了第三项工作,建立好了管道
在这里插入图片描述

在这里插入图片描述
管道已经建立好了,子进程写的文件描述符有,父进程读的文件fd也有,这是内存级文件那,文件怎么写这就怎么写,调用write read
这就是linux一切皆文件的好处
在这里插入图片描述
用系统接口来访问对应的管道
问题 父子通信时经历了几次拷贝呢?
buffer叫做用户层缓冲区,把用户层缓冲区拷贝到文件缓冲区,父进程通过read把内核文件缓冲区拷贝到应用层buffer
所以经历2次拷贝
子进程和父进程和OS 通信 就像爸爸妈妈吵架时把你叫过来,去跟你妈说该吃饭了
OS是中间媒介

上面的代码中,子进程每隔1秒写一个,父进程没有sleep
父进程跑过去读,如果根本就不关心有没有数据,直接把缓冲区数据拿出来就读,打出来就是乱码
如果父进程根本不会等子进程,发现结果应该是子进程每隔一秒写一条,父进程很恣意妄为疯狂的打印
可事实并没有这样的事情发生
在这里插入图片描述
显然父进程是要照顾子进程的,父进程把一条读完了,下一次再读没有数据时父进程就必须要等缓冲区有数据

让子进程写一条休眠50s,父进程一直读,就会发现父进程并没有疯狂打印确实会阻塞等待管道有数据再来读,这说明父子进程是会进行协同的----------父子进程是会进程协同的,同步与互斥的—保护管道文件的数据安全
在这里插入图片描述
管道就像一个队列一样,有数据你就读,没数据你就等

如果让子进程写入时让他写快一点,父进程 读慢一点,又是什么现象呢?
在这里插入图片描述
子进程一直写,写到2458 后 写满 子进程就写不了了就等待读端去读,读完标记缓冲区为为可覆盖
换句话说对写也一样,把写端写满了,写端就不能在写了,必须等读端进行读取
在这里插入图片描述
父进程为什么一次性把数据全读上来了?
对于父进程来讲,只要管道里有数据,有多少读多少,虽然子进程曾经是一条一条写入的,但父进程读取时会把曾经写多次的信息一气都读上来,父进程看来读到的是一个一个字符,只要缓冲区够大,能读多少读多少
至于读上来的数据让用户对应的字符串要分离开来,我不管,就好比石油管道是一桶加还是2桶加1W桶加,读出来是碗接还是盆接,是用户的事情,我只做管道自己的工作 -------管道是面向字节流的
后面要做约定,比如写端每次写15,读端每次读15

衍生问题 管道是有固定大小的,多少?
ulimit -a
查看OS对重要资源的限制
在这里插入图片描述
pipe size管道大小 512一共有8个 就是4KB
我们代码让子进程每次往缓冲区写一个字节,看看写满是多少,写满就会阻塞
在这里插入图片描述
在这里插入图片描述
65536/1024 = 64KB
这和查出来的不一样,管道在不同的内核中有所差别。
这个pipe size可以当成 PIPE_BUF
PIPE BUF又是什么?
假设父子进程规定,必须把hello world要作为整体被读取
当子进程向管道刚刚写了个hello ,这个hello就被父进程读走了,父进程拿到的数据就不完整----数据不一致问题
所以要保证要么就不读,要么就把Hello world全读完,这个工作叫做读取原子性问题
管道为了保证读取原子性,规定了PIPE_BUF大小,只要父进程或子进程读写单位小于PIPE BUF,
读写的过程就是原子的,当往管道里写hello world没有超过PIPE BUF,在写期间管道里有数据了父进程也不会来读

管道的4中情况:
1、读写端正常,管道如果为空,读端就要阻塞
2.、读写端正常,管道如果被写满,写端就要阻塞
1,2 管道就像队列一样
3、读端正常读,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会被阻塞
4、写端是正常写入,读端关闭了。操作系统就要杀掉正在写入的进程。如何干掉?通过信号杀掉
操作系统是不会做,低效,浪费等类似的工作的。如果做了,就是操作系统的bug
为什么让子进程写入,父进程读取?
如果反过来,父进程写,那么子进程关闭时,父进程就被杀了,那就没法验证了,这样设置方便验证
验证:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

管道的特征:
1.具有血缘关系的进程进行进程间通信
4. 管道只能单向通信
5. 父子进程是会进程协同的,同步与互斥的—保护管道文件的数据安全
6. 管道是面向字节流的 //读端看来全是一个一个字节
5.管道是基于文件的,而文件的生命周期是随进程的!
如果管道父子进程退出了,两个文件描述符引用计数由1变0,OS会自动回收这个内存级文件
所以代码可以不关管道的读写端close(int fd),因为进程都要退了,OS会回收它的文件
进程如果退出了,管道自然也会释放掉
所以所有进程都默认打开了0,1,2你从来没关过!

4、谈谈管道的应用场景
1.匿名管道和我们之前学到的cat test.txt | head -10 | tail -5 的这个管道有啥关系呢?
这个管道就是匿名管道
自定义shell
我们想让我们的shell支持 | 管道,代码该如何写?

2.使用管道实现一个简易版本的进程池
创建进程需要系统调用,系统调用也是有成本的,频繁系统调用其实是比较低效的
那我们可以先提前申请10个进程,让他们通过管道和父进程能够通信
父进程写,子进程读,因为匿名管道的协同,父进程向子进程发送约定好的任务码,子进程才会醒来执行任务,不然收不到任务码子进程就阻塞
在这里插入图片描述
最关键的是这么多的管道,我们要做管理
只能先描述,在组织
我们把一个管道用类描述出来,大概用到的属性如父进程要使用的wfd,建立好对应管道的子进程pid,子进程名等
在这里插入图片描述
再用数组把所有channel管理起来
在这里插入图片描述

对管道的管理转化为对数组下标的管理
之后的工作就是先创建管道,再创建子进程,和创建一个匿名管道通信一样,只不过现在有10个子进程,那就套进循环,创建好子进程和管道后,父进程就可以填写管道管理的数组字段了,写端文件描述符有了,子进程pid也有了,数组也就填好了

之后的问题就是这10个子进程,我不能老是让一个子进程工作吧,
我得让所有子进程都能平均的工作一下
这是负载均衡的问题
我们采用的策略是:
1、随机数选择数组下标,也就是选择一个写端和子进程
2、轮转数组下标,也就是从第一个子进程轮转到最后一个然后从头开始

子进程的清理工作问题
管道文件会随着进程退出而被OS回收
但是子进程需要回收,我们采用管道的第三种情况就是让父进程的写端关闭,子进程就会读到文件结尾read返回0,死循环也就break了,之后子进程就会exit退出僵尸等待父进程回收
现在有个bug就是如果关闭一个等待一个进程就会卡住,
在这里插入图片描述
只能这样的循环关闭,循环等待
在这里插入图片描述
这个为什么可以,因为关闭第一个不影响,直到关闭到最后一个,最后一个的wfd只有一个,那么子进程读到0 就僵尸了,那它的fd表也就不指向前面管道的w端了!循环关闭前面的父进程文件描述符都关了,也就是说前一个进程的管道w关也都关闭了,所以子进程从最后一个倒着一个一个的僵尸,最后一起回收掉

因为子进程会继承之前父进程打开的w端,第一个管道除了自己的w端,如果有3个子进程除了自己的还有2个是子进程继承下来的w端
我们还是希望每个子进程只有2个读写端
在这里插入图片描述

1、倒着关闭fd,倒着等待子进程,但没解决多个写端问题
在这里插入图片描述
这个是先关闭最后一个wfd,然后对应子进程僵尸,子进程指向上一个管道的w端也都关闭了,被回收,倒着往上关闭,管道由下往上对应的w端数量为1,2,3…10,如果有10个进程

2、创建子进程时,关闭所继承父进程的所有w端
在这里插入图片描述
进程池的好处是比较高效,不用频繁调用系统调用
而且每个进程都能并发工作,就像我们游戏中地图刷新的同时,野怪也在刷新。


匿名管道是具有血缘关系的进程进行进程间通信!
如果毫不相干的进程之间相互通信呢?

命名管道

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

myfifo和匿名管道打开的文件一样都是内存级文件,向这个文件追加写,文件大小不会改变也就是不会刷新到磁盘
理解:
1、如果两个不同的进程,打开同一个文件的时候,在内核中,操作系统会打开几个文件?
OS会创建两个不同的struct file这是肯定的,文件的inode ,操作方法集,缓冲区只会维护一份
维护两个属性还更麻烦呢,那你说一份缓冲区会不会冲突,你都用两个进程打开同一个文件了,你都不怕我怕什么

所以命名管道的图还是和匿名管道一样
在这里插入图片描述
进程间通信的前提:先让不同进程看到同一份资源,匿名管道是通过父子进程继承看到同一个文件,今天命名管道是如何保证两个不同进程打开的是同一个文件?为什么?
在这里插入图片描述
可以看到myfifo是有Inode的,真真切切是有名字的,所以叫命名管道
在同路径下同一个文件名 = 路径+ 文件名 是具有唯一性的,也就保证了看到同一份资源

编码:
mkfifo 创建命名管道
在这里插入图片描述
unlinke删除管道文件
在这里插入图片描述
在这里插入图片描述
创建两个不相干的进程,利用函数mkfifo来在指定目录下创建一个命名管道,并且设置好权限用八进制0664
因为命名管道是文件,所以可以直接用系统调用open打开这个管道文件,然后一个进程写write,另一个进程读read
我们让server进程来管理管道:管道创建,管道删除

值得一提的是
server进程在创建好管道文件后,要等待写入方client进程打开管道,自己才会打开文件,向后执行,client不打开管道则server的open阻塞了,反之亦然
在这里插入图片描述
可以利用open之后的输出来测试一下
因为你要读,可是你连写都没有,那你凭什么读呢?

总结:
命名管道和匿名管道的特性一样,面向字节流,同步互斥,生命周期随进程,单向通信
区别:它通过文件名+路径的方式让不相关的进程看到同一份资源

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值