进程通信是什么?
两个或者多个进程实现数据层面的交互,通信是有成本的。
为什么要有进程间通信:
在现实中进程之间一定会存在:
基本数据交互
发送命令
某种协同
通知
所以需要进程间通信。
如何实现进程间通信?
进程间通信的本质:必须让不同的进程看到用一份“资源”
“资源”:特定形式的内存空间
“资源”谁提供?操作系统,如果由进程之间提供会破坏进程之间的独立性
进程访问这个空间,本质就是访问操作系统
基于文件级别的通讯方式 ———— 管道
一般的操作系统,会有一个独立的通信模块 —— 隶属于文件系统 —— IPC通信模块定制标准 —— 进程通信是有标准的 ——system V && posix
管道:
管道原理:
用管道原理进行通信的一定是具有血缘关系的进程:父子,兄弟,爷孙
对应父子进程都具有独立的task_struct对应有独立的struct_file,但对应在数组中相同序号指向同一批文件:对应的stdin,stdout,stderrno都是公用的:
如果父子进程想要创建自己的管道文件,对应一定是存在内存中而非磁盘中。
如何建立通信信道?
对应父子进程对同一个文件进行读写,对应就创建了通信。
对于父子进程来说,读写时文件的inode相同,同一个文件缓冲区
不同的fd执行不同的操作,在不同的进程中存储。
如果父子进程同时进行写入读取,那么对应的管道文件不是就乱了吗?
所以我们最好规定,一个进程读,一个进程写。
这也就注定了管道只能进行单向通信:
所以在struct_file中的引用计数就会起作用,对应有不同的fd指向它,引用计数+1。
可以看到上面的内存文件(管道文件)没有名字,也对应叫匿名管道。
2.对应创建匿名管道文件的接口:pipe
对应的头文件和接口。
对应参数中数组的两个元素一定是读方法和写方法。
注意接口的使用:
下面写代码来验证:
对应的运行结果:
可以看到在012后面创建了两个文件:记住下标为0的是读,为1的是写。
接下来创建子进程让子进程写,父进程读:
先看看字符串是否构建成功:
可以看到构建成功了。
再向父进程写入:
对应代码的执行结果:
可以看到执行起来了。
下面来验证管道中的四种情况:
1.读写端正常,管道如果为空,读端就阻塞
2.读写端正常,管道如果被写满,写段就要阻塞
写满多大呢?修改代码来验证:
子进程一次写入一个字节:
父进程不读:
对应代码结果:
最后一个值是6535,对应管道缓冲区的大小位64KB。
3.读端正常,写段关闭,读端会读到0,表明文件结尾,不会阻塞。
0:
修改代码:
对应文件读完了。
可以看到后面n变为0,对应文件为空,所以我们要在文件读取是在加一个判断:
对应的结果:
对应代码更完善。
4.写段写入正常,读端关闭,操作系统通过信号干掉正在写入的进程。
代码:
对应获取子进程的退出码和退出信号:
让父进程读5秒就停,子进程继续写对应结果:
对应的是pipe错误退出。
对应让子进程写入,父进程读取,为了方便看到退出信号,对应就是操作系统终止了进程
管道的特征:
1.只有具有血缘关系的进程才能进行管道通信
2.管道只能进行单向通行
3.父子进程是会进行协同的:解决临界资源竞争问题(多线程)
4.管道是面向字节流的(网络)
5.管道是基于文件的,而文件的生命周期是随进程的。
细节:
1.对应管道通信其实就是对应的缓冲区拷贝:
写入是从用户到内存,读取是从内存到用户
2.管道的固定大小是可以改变的
3.snprintf的使用:
管道的使用场景:
1.自制shell中支持管道(文件重定向),这里不实现了,主要实现下一个
2.用管道实现一个简易版本的进程池:
进程池的概念:
对应让父进程写,子进程读,都读相同大小的内容,这样格式一样
对应可以实现一个简单的游戏日志:
将这些子进程管理起来,也是先描述,再组织。
代码对应对子进程的描述:
子进程初始化:
对应先对初始化好的子进程打印一下:
main函数:
对应代码的执行结果:
接下来让子进程执行一点有意义的东西:
注意对应输入输出函数对应的规范:
输入:const &
输出:*
输入输出: &
定义一个头文件来存放日志:
main函数中初始化这个日志函数指针数组:
控制子进程:
用菜单来对应要实现的功能.
子进程运行的时候也要修改:
执行对应的函数。
对应退出函数,关闭文件描述符,等待子进程:
对应的结果:
对应没有问题。
初始化bug:
命名管道:
上面的匿名管道都是具有血缘关系之间的进程的通行,那么如果没有血缘关系呢?
对应就是命名管道。
对命名管道的理解:
命名管道本质只是管道的一个标识,它不存在这块管道缓冲区还是存在的。
如果两个不同的进程,打开同一个文件的时候,在内核中,操作系统会打开几个文件?
同一个文件。
对应的文件都是路径+文件名
对应的编码:
对应的结果:
实现一个自己的日志:
共享内存:
原理:
主要分为三步:
1.申请物理内存。
2.挂接到进程地址空间中。
3.返回起始虚拟地址。
直接代码对应的系统接口:
shmget:
对应返回值和参数的含义:
创建K:
ftok:
对key的理解:
1.key对应是一个int,至于数字是多少不重要,重要的是它的唯一性,能够让不同的进程执行唯一标识。
2.通过key创建共享进程
3.key存在共享内存的描述对象中
4.上面创建key的函数的两个参数都是由用户自己约定的,
5.key 路径 唯一性
shmat:
将共享内存链接到对应的共享内存。
shmdt:
脱离连接。
shmctl:
用于控制共享内存的这里是把它删掉。
3.共享内存的特性:
1.共享内存没有同步互斥之类的保护机制
2.共享内存是所用进程中通信中,速度最快的。(拷贝少)
3.共享内存内部的数据由用户自己来维护:shmid
共享内存的本质:
1.开辟一块物理空间,让多个进程映射到同一块物理内存到自己的地址空间进行访问。
2.共享内存的删除不是直接删除,而是删除进程与物理地址的映射关系,到映射链数为0的时候,才为真正的删除。
3.共享内存的生命周期是随内核的(本质是一块内存)。
只要不被删除就一直存在系统中。
消息队列信号量:
1.将共享内存的接口与消息队列的接口进行比较:
对应的get:
接口基本相似。
都有key所以都要用到ftok接口。
对应的ctrl接口:
接口的参数都相同,对应的最后一个参数需要自己创建结构体,自己传指针。
对应描述的结构体:
从这里可以看到c语言玩继承的影子。
消息队列的原理:
就是让不同的进程看到同一个队列:
A,B进程的发送方式都是以数据块的形式发送:
发送的数据块必须是带类型的,不然就不知道是谁发送的。
进程间通信在内核中的数据结构设计:
所有的IPC资源都是存在系统的IPC模块中的:
其中不同的通信方法都是对系统内存中一块空间的数据结构的继承:
对应的key都是面向系统内核的,id都是面向用户的。对应不同的通信方式都是存在同一个指针数组的。
学习信号量的储备知识:
管道通信的不足:
1.AB看到同一份资源,如果不加保护,会导致数据不一致问题。
2.加锁:互斥访问,对应任何时候,只允许一个执行流访问资源。
3.共享的,任何时候只允许一个执行流访问的资源叫临界资源。
解释一个现象:
多进程,多线程并发打印会导致:
显示器上的消息:
错乱
与命令行混在一起
信号量:
本质:
是一个计数器,用来描述临界资源中资源数量的多少。
原理和概念:
1.申请计数器成功了,就表示具有访问资源的权限了。
2.申请计数器资源是对资源的预订机制
3.计数器可以有效保证进入共享资源的执行流的数量
4.所以每个执行流访问共享资源不需进过计数器,不能直接访问
上面对应的“计数器”就是信号量
那么如果信号量对应的共享资源只有一个呢?
意味着只能有一个执行流来访问这个临界资源。
这样的信号量叫二元信号量,其实就是一个锁。
信号量的pv操作:
p操作:申请信号量,本质是对计数器的--
v操作:释放信号量,本质上是对信号量的++
两个概念:
1.多个信号量:对应着多种不同的共享资源的管理
2.信号量是几:对应共享资源在信号量中的几号位
信号量凭什么也是通信的一种?
通信不仅仅是通信数据,互相协同也是
要协同,本质也是通信,信号量要被所用要通信的进程看到。
信号量的接口太复杂,这里不看了。