谈进程间通信
目录
进程通信介绍
进程间通信⽬的
进程间通信
管道
匿名管道
匿名管道的原理
基于匿名管道-----进程池
命名管道
systemV共享内存
各个函数
进程通信介绍
进程间通信⽬的
- 数据传输:⼀个进程需要将它的数据发送给另⼀个进程。
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:⼀个进程需要向另⼀个或⼀组进程发送消息,通知它(它们)发⽣了某种事件(如进 程终⽌时要通知⽗进程)。
- 进程控制:有些进程希望完全控制另⼀个进程的执⾏(如Debug进程),此时控制进程希望能够 拦截另⼀个进程的所有陷⼊和异常,并能够及时知道它的状态改变。
进程间通信
什么是进程间通信?
1.
进程间通信(Inter-Process Communication, IPC) 是指在不同进程之间传递或交换数据、信息的机制或方法。由于进程在操作系统中都有自己的地址空间(相互独立,相互隔离),无法直接访问彼此的内存,因此需要借助操作系统提供的特定机制来实现通信。
2.
IPC的核心定义:
IPC 是操作系统为协调多个进程(可能运行在同一台计算机或不同计算机上)而设计的一组技术,允许进程之间共享数据、同步操作或传递消息,从而实现协作或资源共享。
3.
也就是说进程间通信的本质就是:先让不同的进程看到同一份资源(内存),然后才有的进程间通信的条件!于是管道诞生!
管道
什么是管道?
- 管道是Unix中最古⽼的进程间通信的形式。
- 我们把从⼀个进程连接到另⼀个进程的⼀个数据流称为⼀个“管道”
匿名管道
我们先回顾一下:
1.
如果一个进程要对一个文件读写,这个文件需要被加载到内存,而我们之前学过,OS需要对这个进程进行管理,进程有个pcb,pcb中有个files数据结构,指向一个文件描述符表,文件加载啊进来,对应的struct file数据结构填到文件描述符表中,文件的属性加载到file数据结构的inode处,内容加载到file数据结构的缓冲区,至此,这个进程就可以对这个文件进行读写!
2.
如果此时进程创建子进程,我们知道,子进程的内核数据结构会拷贝父进程的内核数据结构,也就是说子进程虽然有自己的一份文件描述符表,但是却和父进程指向同一份文件资源(通信的前提),如果此时,父进程进行写入,子进程读出,进程间通信成立!!!
这只是我们回顾一下,并不是匿名管道,回顾的原因是强调通信的前提,和再次理解一切皆文件的思想,管道也是如此!!!
匿名管道的原理
所以,看待管道,就如同看待⽂件⼀样!管道的使⽤和⽂件⼀致,迎合了“Linux⼀切皆⽂件思 想”。
方法:
pipe函数中的fd是输出型参数。
测试:
我们测试进程读写快慢情况:
然后我们不断用sleep,来控制父子进程的读写快慢情况。
总结:
1.
写端写的慢,读端读的快:读就要阻塞,要等写端。
2.
写端写的快,读端读的慢:会将管道写满,满的时候,写端阻塞等读端,读完之后,重新写,继续写满....
3.
写关闭了,继续读:read就会读到返回值为0,表示文件结尾。
4.
读端关闭,写继续:写再写入,没有任何意义,OS不会做无意义的事,OS会直接杀掉写端进程,发送异常信号,13号信号,SIGPIPE。
5.特性:
- 匿名管道,只能用来进行具有血缘关系的进程间的通信(常用父子)。
- 管道具有同步机制。
- 管道是面向字节流的读写的。
- 管道是单向通信的。(任何一个时刻,一个发,一个收,这也是半双工情况。任何时刻,可以同时发和收,这样是全双工)。
基于匿名管道-----进程池
我们来写一个进程池,由父进程作为写端,想多个管道中写,多子进程分别读管道。
思路:
我们可以循环创建若干个管道和若干个子进程,一旦创建了子进程,子进程就立马read管道(让它阻塞在这里),父进程将若干个管道用vector管理起来,选择任务码,挑选子进程(采用轮询的方式,让每个子进程的读压力趋于均等),然后向管道中写任务码,子进程从管道读到了任务码,执行任务码。
代码:
Task.hpp:将任务管理起来
ProcessPool.hpp:进程池
父进程跑起来:
回收管道和子进程问题:
回收问题有一个坑,我们仔细讲解一下:
1.
当我们将管道的所有的写端关闭,OS就会将读端进程杀掉,并回收管道,这样就将管道回收了。
2.
于是,我们直接循环等待一个个子进程回收就行!
3.
但是,回收有一个坑:当父进程创建第一个子进程的时候,父进程关闭3(读端),4是父进程的写端,子进程关闭4(写端),3是子进程的读端,再创建一个子进程,这个子进程就会拷贝父进程的文件描述符表,此时,这个子进程也会有对第一个管道的写端(4)......
这样循环往复,子进程永远都是3读端,而且还会对其先创建的兄弟进程的管道进行写端。
4.
解决办法--1:
所有的管道类都会插入到vector中,父进程倒着关闭写端,当父进程关闭最后一个管道写端,最后一个管道和子进程就会被回收,那么倒数第二个也会被回收。
解决方法--2:
当第一次创建管道和子进程时,vector为空,子进程关闭读和写端,父进程才会将4插入到vector,第二次创建管道和子进程时,子进程遍历vector关闭写端,..... 也就是说每一次创建子进程和管道,子进程遍历vector关闭写端要比父进程将管道插入vector要先执行,这样就实现了子进程只读对应的管道,而不干扰其他管道。
命名管道
- 如果我们想在不相关的进程之间交换数据,可以使⽤FIFO⽂件来做这项⼯作,它经常被称为命名 管道。
- 命名管道是⼀种特殊类型的⽂件
命名管道的特性和匿名管道一致,只是命名管道可以和不相关的进程通信,也就是说命名管道信息也是同步机制,而且单向通信。
指令创建和删除:
实现一个简单的代码两个无血缘关系的进程交互:
服务端(server.cc)创建管道,打开管道,并且阻塞在open函数内部,客户端(client.cc)打开管道(直到客户端打开管道,服务端open函数才会返回),客户端向管道写入,服务端读取,客户端不写了,服务端需要将管道删除!
服务端(server.cc):
客户端(client.cc):
执行起来:
systemV共享内存
共享内存区是最快的IPC形式。⼀旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执⾏进⼊内核的系统调⽤来传递彼此的数据.
共享内存是在物理内存中开辟一块空间,并且映射到虚拟地址空间的共享区,一旦有进程需要使用它,只需要填充页表即可寻址到其真实物理地址处,也就是说在每个进程看来,它自己就独享这块内存,其实本质也是让多个进程看到同一块内存资源。
2.当有多个共享内存时,根据我们之前学习的内容,把它们管理起来,只需要先描述,再组织起来即可!这是共享内存的结构体:
各个函数
ftok函数:
shmget函数:
shmat函数:
shmdt函数:
shmctl函数:
如:
这样我们就拿到了,某个共享内存的结构体全部属性:
指令操作:
代码客户端(Client.cc)和服务端(Server.cc)交互:
服务端创建并连接共享内存,客户端连接共享内存,服务端不断的读(和管道不同的是,当没有写入时,管道会一直阻塞,而共享内存却仍然可以读,只不过为空),客户端进行写入....
注意:共享内存在创建的时候,它的大小,必须是4kb(4096)的整数倍,如果不符合整数倍,OS会向上4KB取整。
共享内存是进程间通信中,速度最快的方式:映射之后,读写,可以直接被进程看到,不需要进行系统调用进行读写内容。无同步机制。
无同步机制,我们怎么让共享内存由同步机制?
我们可以在共享内存上层加一个命名管道即可!
并发铺垫:
- 多个执⾏流(进程),能看到的同⼀份公共资源:共享资源
- 被保护起来的资源叫做临界资源
- 保护的⽅式常⻅:互斥与同步
- 任何时刻,只允许⼀个执⾏流访问资源,叫做互斥
- 多个执⾏流,访问临界资源的时候,具有⼀定的顺序性,叫做同步
- 系统中某些资源⼀次只允许⼀个进程使⽤,称这样的资源为临界资源或互斥资源。
- 在进程中涉及到互斥资源的程序段叫临界区。你写的代码=访问临界资源的代码(临界区)+不访问 临界资源的代码(⾮临界区)
- 所谓的对共享资源进⾏保护,本质是对访问共享资源的代码进⾏保护
我们下期见!!!