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