目录
进程间通信 (IPC):操作系统为用户提供的几种进程间通信方式。
- 主要因为进程之间具有独立性,每个进程访问的都是自己的虚拟地址,而不是直接访问物理内存,无法访问同一块区域,因此无法实现数据通信,所以才需要操作系统提供进程间通信方式,实现进程间的通信。
操作系统针对不同的通信场景提供了多种不同的通信方式(System V(罗马5)标准):
数据传输--> 管道(从Unix而来)/消息队列,数据共享--> 共享内存,进程控制--> 信号量。
1. 管道
本质:内核中的一块缓冲区;多个进程访问同一个管道(一块缓冲区)就可以实现通信。
种类:匿名管道/命名管道
- 匿名管道:这块内核中的缓冲区没有具体的标识,只能用于具有亲缘关系的进程间通信。子进程通过复制父进程的方式,获取到管道的操作句柄进而实现访问同一个管道通信。
创建匿名管道,向用户(通过参数pipefd)返回管道的操作句柄:int pipe(int pipefd[2]);
pipefd[2]---> 具有两个int型节点的数组的首地址,用于接收创建管道返回的操作句柄。
pipefd[0]--> 用于从管道中读取数据, pipefd[1]--> 用于向管道中写入数据。
管道是一个单向的资源传输,自身并不限制资源的传输方向;管道是一个半双工通信(可以选择方向的单向传输);使用的时候不使用哪一端,就关闭那一端。
返回值:0---成功 -1---失败
管道的读写特性:
- 若管道中没有数据,则调用read读取数据会阻塞;
- 若管道中数据满了,则调用write写入数据会阻塞;管道是一块缓冲区(内存空间),并非无限制大
- 若管道的所有读端pipefd[0]被关闭,则继续调用write会产生异常导致进程退出;
- 若管道的所有写端pipefd[1]被关闭,则继续调用read,read读完管道中的数据后不再阻塞,而是返回0.
阻塞:为了完成一个功能,发起调用,若当前不具备完成的条件,则一直等待。
命令行中管道符的实现:ps-ef | grep ssh
ps-ef--> 默认将结果打印到标准输出
grep ssh --> 默认从标准输入读取数据进行过滤
管道自带同步与互斥
同步:通过条件判断,判断当前进程是否能访问,不能访问则等待,能访问的时候,再唤醒,实现对临界资源访问的合理性
体现:若管道没有数据,则read读取数据会阻塞,若管道写满了,继续write会阻塞
互斥:通过保证同一时间只有一个进程能够访问临界资源,保证临界资源访问的安全性
体现:对管道进行数据操作的大小不超过PIPE_BUF=(4096字节)的时候,则保证操作的原子性(一步完成,中间不被打断)
- 命名管道:内核中的缓冲区具有标识符(标识符是一个可见于文件系统的管道文件),其他的进程可以通过这个标识符,找到这块缓冲区(通过打开同一个管道文件,进而访问到同一块缓冲区),进而实现通信。
可用于同一主机上的任意进程间通信。
可以命令操作: mkfifo filename 创建管道文件。
代码中的操作:int mkfifo(const char* pathname, mode_t mode)---创建命名管道文件
pathname:管道文件名称
mode:管道文件权限
成功返回0 ,失败返回-1
剩下的操作与文件IO操作类似。
open打开命名管道的特性:
- 若管道文件以只读的方式打开,则会阻塞,直到这个管道文件被以写的方式打开;
- 若管道文件以只写的方式打开,则会阻塞,直到这个管道文件被以读的方式打开;
- 若管道文件以读写的方式打开,则不会阻塞。
1.1 总结管道
本质:内核中的一块缓冲区
分类:匿名管道-->只能用于具有亲缘关系的进程间通信;命名管道-->可用于同一主机上的任意进程间通信
特性:
- 管道是半双工通信(可以选择方向的单向传输)
- 管道的读写特性(不管匿名还是命名都一样): 若管道中没有数据,则调用read读取数据会阻塞; 若管道中数据满了,则调用write写入数据会阻塞; 若管道的所有读端pipefd[0]被关闭,则继续调用write会产生异常导致进程退出; 若管道的所有写端pipefd[1]被关闭,则继续调用read,read读完管道中的数据后不再阻塞,而是返回0。
- 管道生命周期随进程(打开管道的所有进程退出,管道就会被释放[命名管道也一样,本质都是缓冲区,文件只是标识符])
- 提供字节流传输服务--可靠的,有序的,基于连接的一种灵活性比较高的传输服务---传输比较灵活
- 命名管道额外特性: 只读打开则会阻塞,直到文件被以写方式打开; 只写打开则会阻塞,直到文件被以读方式打开。
- 管道自带同步与互斥: 同步:通过条件判断实现临界资源访问的合理性--> 管道中没有数据则调用read会阻塞/管道中数据满了则调用write会阻塞; 互斥:通过唯一访问实现临界资源访问的安全性--> 管道的读写操作在PIPI_BUF大小以内保证操作的原子性。临界资源:大家都能访问到的资源。原子操作:不能被打断的操作,指的是一个操作要么一次完成,要么就不做。
2. 共享内存:最快的进程间通信方式,生命周期随内核。
原理:在物理内存上开辟一块内存空间,多个进程将同一块物理内存空间映射到自己的虚拟地址空间,这些进程通过自己的虚拟地址直接访问这块空间,实现数据共享。
管道的通信中:涉及到两次用户态与内核态之间的数据拷贝;将数据写入管道,从管道读取数据。
共享内存的通信:直接通过虚拟地址映射访问物理内存实现共享内存中的数据操作,相较于管道这种通信少了两次用户态与内核态之间的数据拷贝操作,因此速度快。
- 共享内存的操作流程:
- 创建共享内存--> 在物理内存上开辟一块内存空间
- 进程将共享内存映射到自己的虚拟地址空间
- 进程就可以通过虚拟地址空间直接访问这块物理内存
- 解除虚拟地址空间与共享内存的映射关系
- 释放共享内存资源
1、创建共享内存(开辟物理内存空间--具有标识符)
- int shmget(key_t key,size_t size, int shmflg)
key:内核中共享内存的标识符,多个进程通过相同的标识符才能打开同一块共享内存
size:共享内存大小--以内存页为单位进行分配
shmflg: IPC_CREAT-存在则打开,不存在则创建 | IPC_EXCL 与IPC_CREAT同时使用,若存在则报错,不存在则创建 | 权限 (标志位|权限)
返回值:成功返回一个共享内存的操作句柄--> 非负整数,失败返回-1;
生成一个key值:key_t ftok(const char *pathname, int proj_id); --> 通过inode节点号与projid合成一个key
2、将共享内存映射到各个进程的虚拟地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg)
shmid:shmget返回的共享内存操作句柄,
shmaddr:共享内存映射在虚拟地址空间的首地址,通常置NULL,
shmflg:映射成功之后对共享内存可以进行的操作,通常置0-可读可写 SHM_RDONLY-只读(前提是有读的权限)
返回值:成功返回共享内存映射在虚拟地址空间中的首地址,通过这个地址对内存进行操作,失败返回(void*)-1
3、直接通过虚拟地址进行内存操作
4、解除映射关系
int shmdt(const void* shmaddr)
shmaddr:映射在虚拟地址空间的首地址
返回值:成功返回0,失败返回-1
5、删除共享内存
int shmctl(int shmid, int cmd,struct shmid_ds * buf);
shmid:共享内存操作句柄
cmd:具体对共享内存要进行的操作---> IPC_RMID--删除共享内存
buf:用于获取/设置共享内存信息的结构,不使用置NULL
返回值:成功返回0,失败返回-1
共享内存删除的时候,并不会立即被删除,只是将其状态置为被销毁状态(因为有可能会造成正在访问的进程崩溃),将key修改为0,移除标识--> 为了不让这个共享内存继续被其他进程映射连接,然后等到当前共享内存映射连接数为0的时候,才会真正删除这块共享内存。
操作系统中进程间通信资源的命令操作:
ipcs: 查看进程间通信资源 ipcrm 删除进程间通信资源 ipcrm -m shmid
-m:查看共享内存
-q: 查看消息队列
-s: 查看信号量
共享内存数据的写入,是一种针对地址指向空间的覆盖式写入。
2.1 总结共享内存
本质原理:多个进程将同一块物理内存映射到自己的虚拟地址空间实现数据共享
特性:
- 最快的进程间通信方式
- 生命周期随内核
注意事项:共享内存的操作是不安全的(并不会自动具备同步与互斥关系,需要操作用户进行控制)
操作:代码操作流程/具体的代码+命令操作ipcs/ipcrm
3. 消息队列及总结
本质上就是内核中的一个优先级队列,多个进程通过向同一个队列中添加节点和获取节点实现通信。
传输一个有类型(优先级)的数据块。
特性:
- 自带同步与互斥
- 生命周期随内核
- 数据传输自带优先级
1、msgget 创建消息队列
2、msgsnd向队列中添加节点/获取节点
msgrcv获取节点
3、msgctl操作-删除消息队列 IPC_RMID
注意事项:
struct msgbuf
{
int type;
char buf[***]
} ; 这个结构体需要用户自己定义
4. 信号量
用于实现进程间的同步与互斥(共享内存本身不提供同步与互斥,操作存在安全问题,因此需要使用信号量保护对共享内存的操作),生命周期随内核。
互斥:保证同一时间只有一个进程访问临界资源实现临界资源的互斥访问保证安全性
同步:通过一种条件的判断,不能访问则等待,能访问再唤醒,实现对临界资源访问的合理性
本质:内核中的一个计数器+pcb等待队列(对资源进行计数)
同步的实现: 信号量这个计数器可以对数据资源进行计数,进程在访问资源之前,先通过计数判断能否访问资源,
计数>0 才能访问;获取一个资源,计数-1;
计数<=0 不能访问;计数-1,将pcb状态置为可中断休眠状态,加入等待队列;
其他进程产生了资源,计数+1;若计数>0 则什么都不做;若计数<0则从等待队列中唤醒一个pcb去获取资源。
互斥的实现:保证同一时间只有一个进程能够访问资源。通过只有0/1的计数器,实现对临界资源访问状态的标记;在访问临界资源之前先获取信号量,计数-1;若计数<0则使进程等待(将进程pcb加入队列中);否则则可以对临界资源进行访问(并且在访问期间,已经将临界资源的状态置为不可访问状态,因此可以保证其他进程不会再访问临界资源), 当前进程访问完毕后,则对计数+1,则唤醒一个进程(将一个pcb出队,置位运行状态)
信号量实现互斥的思想:以资源只有一份,只有一个进程能够获取;用完了放回来;下一个进程在获取的思想。
4.1 总结信号量
信号量的本质:计数器+等待队列+使进程等待/唤醒的接口
同步的实现:通过计数器对资源进行计数;计数>0表示能获取;计数<=0表示不能获取,通过等待接口使进程等待加入等待队列;等待有资源的时候唤醒。
互斥的实现:通过保证计数器不会大于1,保证同一时间只有一个进程能够访问资源。