进程间通信的目的
- 实现数据传输,一个进程将自己的数据发送给另一个进程。
- 资源的共享。多个进程共用同一份资源。
- 传递信息。例如:子进程退出需将自己的退出原因告知父进程。
- 进程控制。有的进程希望完全控制另一个进程。
- 共享数据。多个进程之间共享数据,当一个进程对数据进行修改,其他进程应该立即看到。
通信方式
- 管道
- systemV标准:共享内存、消息队列、信号量。
为什么操作系统要为进程提供通信方式,而不是进程之间直接通信
- 因为进程之间具有独立性。
- 每个进程都有自己的虚拟地址空间,每个进程的操作都是在自己的虚拟地址上进行,因此进程之间无法通信。
操作系统如何让进程之间进行通信
- 操作系统在每个进程之间提供一个媒介资源,使得每个进程都能访问到,根据场景的不同,提供具体的操作也不同。常见的方式有,管道、共享内存、消息队列、信号量等。
管道
将系统内核的一块缓冲区作为公共媒介,让多个进程可以访问从而实现进程通信。在一个进程中创建一个管道,操作系统为了能让该进程对其创建出来的管道进行读写操作,会向该进程返回两个操作句柄(文件描述符),一个用于读,一个用于写。
- 匿名管道
系统内核没有为该缓冲区设置标识符(系统没有为该管道起一个名字),所以普通的进程没法知道该管道的具体信息,因此只能用于具有亲缘关系的进程间通信(父子进程)。在父进程创建一个管道,同时创建了一个子进程,子进程会复制父进程的pcb,同时也会复制两个操作句柄(文件的描述符),并且父子进程控制的是同一管道。
int pipe(int pipfd[2]);用于创建一个匿名管道,并且通过参数返回两个管道的操作句柄。
pipfd[0];用于从管道中读取数据
pipfd[1];用于从管道中写入数据
- 命名管道
系统内核为该管道缓冲区设有标识符(有名字),因此同一台主机上的进程可以通过该标识符对该管道进行操作,从而实现进程之间通信。
makffio(char *filename,mode_t mode);
filename//命名管道的标识符
mode//管道的权限信息
//example
makffio("make",0664);//创建一个名称为make的管道,权限信息为664
管道的读写特性
- 如果管道中没有数据,则read端操作会被阻塞,如果管道中有数据,并且该缓冲区已满,则write端会被阻塞。
- 如果管道中write端操作被关闭,则read端操作读完管道中已有的数据后,就会返回0,不再阻塞。(read)返回0主要表示管道中所有的write端被关闭。
- 如果管道中read端被关闭,write端写入数据时会触发异常,进程退出。
- 如果管道被以只读打开,则会被阻塞,直到管道被另一个进程以只写方式打开。
- 如果管道被以只写打开,则会被阻塞,直到管道被另一个进程以只读方式打开。
- 没有数据read阻塞,数据写满write阻塞。
管道本质
是操作系统内核中的一块缓冲区,多个进程通过访问同一个缓冲区实现通信。
管道的特性
- 半双工通信。一端写入数据(write),另一端读数据(read),反之也可以。
- 生命周期随进程。哪一个进程创建的管道,等该进程销毁时,管道也随之销毁。
- 自带同步与互斥。同步:通过条件判断,实现对数据资源访问的合理性。互斥:通过同一时间的唯一访问,实现对数据资源访问的安全性。
共享内存
将一块物理地址映射到进程的虚拟地址空间中,进程通过自己的虚拟地址空间可以访问到该共享内存,从而实现通信。
每个虚拟地址中都有一个共享区,用来存放共享物理内存的虚拟地址,通过自己的页表映射到共享的物理内存,从而可以对该内存进行操作,同时另一个进程的虚拟地址空间也映射到该物理内存中,也可以对其进行操作,从而实现通信。
最快的通信方式
- 普通的通信方式:先将数据拷贝到内核中,再从内核中将数据拷贝出来。
- 共享内存直接通过虚拟地址空间访问物理内存,少了两次的数据拷贝
共享内存的创建与打开
#include<sys/ipc.h>
#include<sys/shm.h>
int shmget(key_t key,size_t size,int shmflg);
key:共享内存的标识符,多个进程通过表示符,才能访问同一块共享内存
size:要创建共享内存的大小
shmflg:
IPC_PRIVATE 私有,只能用于亲属关系的进程间通信
IPC_CREAT 共享内存不存在则创建,存在打开
IPC_EXCL 与IPC_CREAT同时使用,不存在则创建,存在则报错
返回值:共享内存在代码中的操作句柄
进程间通信标key值的生成。
#define proj_id 0x1234678
key_t ftok(const char *pathname,int proj_id);
pathname:是文件信息,目录路径
shmat用于将创建好的共享内存,映射到进程的虚拟地址空间中。
void *shmat(int shmid,const void *shmaddr,int shmflg);
shmid:创建内存时返回的操作句柄
shmaddr:共享内存映射在虚拟地址空间中的首地址,通常情况下为NULL
shmflg:在共享内存具有权限的基础上,可以设置在代码中的操作权限
SHM_RDONLY:只读权限
默认为0,可读可写
返回值:返回共享内存在虚拟地址空间的首地址,通过该地址可以访问共享内存
shmctl共享内存管理
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
shmid:操作句柄
cmd:对共享内存想要进行的操作
IPC_RMID:标记共享内存为将要销毁,实际上只是将链接数-1
buf:在管理共享内存的时候,通过该结构体返回数据,或者设置新的数据
共享内存的本质
不同的进程通过链接同一块物理内存,实现数据传输,从而实现通信。
特性
- 最快的通信方式
- 生命周期随内核
- 不安全,多个进程同时操作可能会造成数据二义性
ipcs 查看进程中通信资源
- ipcs -m 共享内存
- ipcs -q 消息队列
- ipcs -s 信号量
消息队列
本质
在操作系统内核中,创建一个优先级队列,多个进程间通过向队列中添加数据块或者获取数据块,从而实现通信。
特性
- 生命周期随内核。
- 消息队列自带互斥与排斥。
- 消息队列所能存储的数据是有最大长度限制的。
信号量
信号量本身并不是实现通信的,而是用于实现进程的同步与互斥。(保护进程间访问临界资源的时候不会出现数据二义性)
同步:通过一定条件判断,实现进程间对临界资源访问的时序合理性。
互斥:通过同一时间的唯一访问,实现进程间对临界资源访问的安全性。
临界资源:进程间都能访问的资源。
临界区:对临界资源进行操作的代码区。
信号量的本质
计数器+pcb等待队列
- 计数器:统计资源的数量,通过数量判断当前的访问是否合理。
- pcb等待队列:存放执行操作时不能获取资源而挂起的进程。
信号量同步的实现
数据是资源,初始化的时候有多少数据资源,计数器就会初始化相应的数量。
进程A去获取资源,若计数器>0,表示有资源,可以直接获取,同时计数-1;若计数器<=0,表示没有资源,则需要调度使得进程陷入休眠的接口,让进程等待-1。
进程B产生一个资源,判断若计数器<0,则调用唤醒一个休眠进程的接口,唤醒一个等待进程A,计数器+1;若计数器>=0,表示没有人等待资源,计数器直接+1即可。
信号量互斥的实现
当数据资源唯一时,在进程A访问临界资源时,将计数从1减为0(其他进程这时如果继续访问就会陷入等待队列中)
当等待进程A中的资源访问完毕时,将计数器进行+1(唤醒等待队列中的进程,各进程之间重新开始抢夺资源)