进程间通信
1、进程间通信的概念
进程间通信是操作系统为用户提供的几种通信方式。
进程间因为每一个进程都有一个虚拟地址空间,在保证了进程独立性的同时,却使得进程间无法直接通信,因此需要操作系统来提供进程间通信方式,并且因为通信的场景不同,所以提供的方式也有多种。
2、进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止
时要通知父进程)。 - 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另
一个进程的所有陷入和异常,并能够及时知道它的状态改变
3、进程间通信方式的种类
(System V标准)
- 管道:用于进程间的数据传输
- 共享内存:用于进程间的数据共享
- 消息队列:用于进程间的数据传输
- 信号量:用于进程间的数据控制
- Socket通信(后面介绍)
3.1 管道
管道的本质
内核中的一块缓冲区
—通过半双工通信实现数据
通过让多个进程都能访问到同一块缓冲区来实现进程通信
- 单工:已经确定方向,只能进行单向通信
- 双工:具有双向性,可以互相进行通信
- 半双工:原本具有双向性,但是只能选择方向进行单向通信
管道的分类
- 匿名管道
- 命名管道
匿名管道
这块内核中的缓冲区没有标识,其他进程无法直接访问管道
只能用于具有亲缘关系的进程间通信(只能使用父子进程复制的方式获取操作句柄)
创建管道时,操作系统会提供两个操作句柄(文件描述符),其中一个用于管道读取数据,另一个向管道写入数据,因此只能通过创建子进程,子进程通过复制父进程的方式,获取到管道的操作句柄进而实现访问同一个管道通信。
创建匿名管道
//常见一个匿名管道,向用户通过参数pipefd返回管道的操作句柄
int pipe(int pipefd[2])
//pipefd[0]:用于从管道读取数据
//pipefd[1]:用于向管道写入数据
//返回值:成功返回0,失败返回-1
特性
- 若管道中没有数据,则read会阻塞;
- 若管道写满了,继续write则会阻塞;
- 管道自带同步与互斥
- 若管道中所有的写端都被关闭(表示当前没有进程写入数据),read读完管道中的数据之后,就不会再阻塞,而是返回0
- 若管道中所有的读端都被关闭(表示没有进程读取数据了),继续write会触发异常,程序退出
同步:通过条件判断,判断当前进程是否能访问,不能访问则等待,能访问的时候再唤醒,实现临界资源(临界资源表示大家都能访问到的资源)访问的合理性
体现:若管道中没有数据,则read会阻塞;若管道写满了,继续write则会阻塞。
互斥:通过保证同一时间只有一个进程能够访问临界资源,保证临界资源的安全性。
体现:安全性是指对管道进行数据操作的大小不超过 PIPE_BUF=4096(默认大小) 的时候,则保证操作的原子性。( 原子性是指一步完成,不可被打断)
同步保证访问临界资源的合理性,但是不保证安全性;
互斥保证访问临界资源的安全性,但是不保证合理性。
关闭读端/写端
//关闭写端
close(pipefd[1]);
//关闭读端
close(pipefd[0]);
ps -ef 命令的默认功能:将结果写入到标准输出
grep ssh 一直从标准输入读取数据进行过滤(如果读不到数据则等待)
命名管道
内核中的缓冲区具有标识符
(标识符是一个可见于文件系统的管道文件),其他进程可以通过这个标识符找到这个缓冲区(通过打开一个管道文件,进而访问同一块缓冲区),进而实现通信
创建命名管道
//命令
mkfifo filenam
//接口
int mkfifo(const char *pathname, mode_t mode);
//pathname:管道文件名称
//mode:文件权限
//成功返回0,失败返回-1
打开特性
- 若管道文件以只读的方式打开,则会阻塞,直到这个管道文件被以写的方式打开
- 若管道文件以只写的方式打开,则会阻塞,直到这个管道文件被以读的方式打开
- 若管道文件以读写的方式打开,则不会阻塞
管道特性
- 管道生命周期随进程
- 半双工通信
- 自带同步与互斥
- 提供字节流服务—有序、连接、可靠的字节流传输–传输比较灵活
3.2 共享内存
最快的进程间通信方式
实现原理
- 在物理内存上开辟一块内存空间
- 将这块物理内存映射到进程的虚拟地址空间
- 进程就可以通过虚拟地址直接访问这块物理内存
多个进程要映射同一块物理内存,就可以通过这块内存实现数据共享
共享内存是最快的进程间通信方式的原因
因为共享内存直接通过虚拟空间地址映射访问物理内存,而其他方式都是内核中的缓冲区,在通信时都会涉及用户态与内核态之间的两次数据拷贝,通信速度会减慢;
共享内存少了两次用户态与内核态的数据拷贝,因此通信速度更快。
共享内存的操作流程
-
创建共享内存(开辟物理内存空间,具有标识符)
-
将共享内存映射到各个进程的虚拟地址空间
-
直接通过虚拟地址进行内存操作
-
解除映射关系
-
删除共享内存
-
创建共享内存
int shmget(key_t key,int size,int flag)
//key:共享内存的标识符,多个进程通过相同的标识符可以打开同一块共享内存
//size:共享内存的大小
//flag:IPC_CREAT | IPC_EXCL | 权限
//返回值:成功返回一个操作句柄,失败返回-1
- 将共享内存段连接到进程地址空间
void *shmat(int shmid,void *addr,int flag);
//shmid:共享内存的操作句柄
//addr:映射到虚拟地址空间的首地址,通常置空NULL,一般由操作系统来决定
//flag:通常置0(可读可写)==> 前提是拥有设置可读可写的权限 SHM_RDONLY 只读
//返回值:成功返回映射到虚拟地址空间的首地址,通常这个地址对内存进行操作,失败返回-1
- 解除映射
int shmdt(void *shmstart);
//shmstart:映射到虚拟地址空间的首地址
//返回值:成功返回0,失败返回-1
- 删除映射
int shmctl(int shmid, int cmd,struct shmid_ds *buf);
//shmid:操作句柄
//cmd:具体对共享内存要进行的操作----IPC_RMID--删除共享内存
//返回值:成功返回0,失败返回-1
相关命令:
ipcs 查看进程间通信资源
ipcrm 删除进程间通信资源
ipcs -m 查看共享内存
ipcs -q 查看消息队列
ipcs -s 查看信号量
ipcrm -m shmid 删除进程间通信资源
当删除共享内存的时候,共享内存不会立即被删除(因为有可能会造成正在访问的进程崩溃),而是将key修改为0,表示这块共享内存将不再继续接收映射链接,当这块共享内存的映射链接数为0 的时候,则自动释放
特性
- 最快的进程间通信方式
- 生命周期随内核
共享内存的操作是不安全的(并不会自动具备同步于互斥关系,需要操作用户进行控制)
3.3 消息队列
本质上就是内核中的一个优先级队列,多个进程通过向同一个队列添加节点和获取节点实现通信,传输一个有类型(优先级)的数据块
特性
- 自带同步与互斥
- 生命周期随内核
- 数据传输自带优先级
注意事项:
struct msgbuf{int type,char buf[***]};这个结构体需要用户自定义
msgget 创建
msgsnd 添加节点
msgrcv 获取节点
msgctl 操作–删除消息队列 IPC_RMID
3.4 信号量
用于实现进程间的同步与互斥
信号量本质:内核中的一个计数器+pcb等待队列(对资源进行计数)
-
同步:通过条件判断,判断当前进程是否能访问,不能访问则等待,能访问的时候再唤醒,实现临界资源访问的合理性
同步的实现: 信号量是一个对资源的计数,可以通过计数判断是否能够获取一个资源进行处理,若计数<0,则表示不能获取(并且对计数-1),需要等待(加入pcb队列),此时若其他进程生产一个资源,则会对计数+1,若计数<=0,则唤醒一个进程。(只保证合理,不保证安全) -
互斥:通过保证同一时间只有一个进程能够访问临界资源,保证临界资源的安全性。
互斥的实现:通过只有0或1 的计数器,实现临界资源访问状态的标记;在访问临界资源之前先获取信号量,计数-1;若计数<0,则使进程等待(将进程pcb加入队列中);否则可以对临界资源进行访问(并且在访问期间,已经将临界资源的状态置位不可访问状态,因此可以保证其他进程不会再访问临界资源),当前进程访问完毕后,则对计数进行+1,则唤醒一个进程(将一个pcb出队,置位运行状态)