一,为什么操作系统给用户提供进程间通信的方式
- 因为进程之间有独立的地址空间,导致进程间无法直接进行通信
- 场景:数据传输,数据共享,进程控制,事件通知
二, 分类
管道—进程间数据资源的传输
1,特点
- 半双工通信----(双向选择的单向通信)数据只能从这一端流向对端–可供用户选择的单向通信
- 提供流式服务,面向字节流的数据传输—传输灵活但是会造成数据粘连
- 管道的生命周期随进程,
读写特性
- 如果管道中无数据,read会阻塞,直到读到数据然后返回(管道的大小是有限制的,一般为64k)
- 如果管道中数据满了,write会阻塞,直到数据被读取,管道中有空闲位置,然后写入数据后返回,
- 如果管道的所有读端都被关闭,则write会触发异常–SIGPIPE—使进程退出
- 如果管道的所有写端都关闭,则read读完数据后返回0,通知用户没人写数据了
-
自带同步与互斥特性 当读写大小小于PIPE_BUF(4096)时保证操作的原子性—操作不可被打断
- 互斥—保证对临界资源(都可以访问到的公共资源)同一时间的唯一访问性
- 同步—保证对临界资源访问的时序可控性—保证公平,我操作的时候你不能操作(互斥),我操作的完了你才能操作(同步)
2,原理
- 内核的一块缓冲区,通信双方都可以访问到的区域
- 管道本质就是一个内核的缓冲区,因此在使用管道通信的过程中,涉及到用户态和内核态数据交换的问题,因此比较慢,两次拷贝(第一次用户态数据拷贝到内核态的缓冲区,第二次是将内核态数据拷贝到用户态)
3,匿名管道(pipe)
- 只能用于具有亲缘关系的进程间通信----父进程创建一个匿名管道,然后fork创建子进程,子进程复制了父进程的文件描述符表,因此子进程也有两个文件描述符指向该缓冲区。因此父子进程都用文件描述符标识一个管道,可以实现进程间通信
- 接口int pipe(int pipefd[2])— pipefd是输出型参数,返回两个文件描述符,pipefd[0]读数据 另一个写数据,返回值-1表示失败,
- ps -ef | grep ssh
4,命名管道(fifo)
- 可见于文件系统,因为创建命名管道会随之在文件系统中创建一个命名管道文件,因为所有的进程都可以通过打开管道文件,进而获得命名管道的操作句柄,因此命名管道可以用于同一主机上任意进程间通信,
- 管道的原理依然是内核的缓冲区,只是通过文件向所有进程都提供了能够访问管道的方法,
- 匿名管道和命名管道的区别------通信范围的大小不同
命名管道的的打开特性,
- 若管道没有被写的方式打开,这时候如果只读打开就会阻塞,直到文件以被写的方式打开
- 若管道没有被读的方式打开,这时候如果只写打开就会阻塞,直到文件以被读的方式打开
- 若管道以读写方式打开,则不会阻塞
- 其他读写特性与匿名管道一致
- 管道文件ls -al 是以p开头的
匿名管道和命名管道的区别
- 匿名管道只能用于具有亲缘关系的进程间通信(匿名管道没有名字,只能通过fork的操作让子进程获得和父进程相同的文件描述符),而命名管道可以让同一主机上的任意两个进程实现通信,(命名管道有名字,可见于文件系统)
共享内存—(内存共享数据共享)
原理
- 多个进程将同一块物理空间映射到自己的虚拟地址空间中,达到数据共享的目的,这是最快的IPC方式,
- 在物理内存上开辟一块空间,将这块空间映射到进程的虚拟地址空间上,进程可以通过虚拟地址进行访问操作,如果一块内存被多个进程映射,那么多个进程访问同一块内存可以实现进程间通信
- 相较于其他IPC方式(将数据从用户态拷贝到内核态,用的时候从内核态拷贝到用户态),共享内存少了两次拷贝操作,共享内存可以直接通过(虚拟)地址间接堆内存操作,并且反馈到其他进程,
使用流程
- 创建共享内存 shmget
- 将共享内存映射到虚拟地址空间 shmat
- 对共享内存进行操作 memcpy…
- 解除映射关系 shmdt
- 删除共享内存 shmctl
接口
- 创建共享内存-----int shmget(key_t key, size_t size, int shmflg);
1,key:共享内存标识 操作系统中的标识符(共享内存的名字)----key_t ftok(const char *pathname, int proj_id) 通过文件的iNode节点和proj_id共同得出一个key的值,
2,size:共享内存大小
3,shmflg:打开方式/创建权限—IPC_CREAT 共享内存不存在就创建,存在就打开 —IPC_EXCL 与ICP_CREAT同用,若存在就报错,不存在就创建 —mode_flags 权限
4, 返回值:操作句柄shmid(程序内部的标识符,通过这个id来操作共享内、存),失败返回-1; - 将共享内存映射到虚拟地址空上—void *shmat(int shmid, const void *shmaddr, int shmflg);
1,shmid 创建共享内存返回的操作句柄
2, shmaddr ,用于指定映射在虚拟地址空间的首地址,通常为NULL
3,shmflg 通常为0—可读可写
4,返回值:成功—返回映射首地址(通过这个地址对共享内存操作) 失败—(viod星号)-1 - 解除映射关系 —int shmdt(const void *shmaddr)
1,传输虚拟地址空间的首地址,也就是shmat的返回值 - 删除共享内存----int shmctl(int shmid, int cmd, struct shmid_ds *buf)
1,shmid ---- 贡献内存的操作句柄
2, cmd ----- 对共享内存要进行的操作(删除)
IPC_RMID 删除共享内存(删除共享内存,并不是立即删除的,因为这个共享内存可能还连接的有其他进程,只是拒绝后续映射连接,当贡献内存的映射连接数为0的时候则删除内存(nattch(映射连接数)))
3, buf ---- 设置或者获取一些信息放到buf中,用不上就置为NULL
共享内存查看
- ipcs 查看进程间通信的方式
- ipcs -m 查看共享内存
- ipcrm -m shmid 删除指定的共享内存
消息队列
原理
- 消息队列传输的是有类型的数据块,用户可以根据自己的需要选择性的获取某些类型的数据,
- 在操作系统内核维护了一个环形链式队列,具有head和tail,有数据来了往里面插入一个数据块,这个数据块大致是一个结构体,是一个具有类型的数据块
- 当有进程获取数据的时候,从head开始遍历,找到对应类型的数据并返回
- 消息队列提供了一个进程向另一个进程发送一块数据的方法,每个数据块都被认为是有一个类型的,接收进程的数据块可以有不同的类型值
流程
- 创建---------添加数据节点------------获取数据节点---------------删除
- msgget------msgsnd----------------- msgrc------------------msgctl
现状
- 消息队列现状几乎已经被淘汰了,主要因为限制太多了,比如底层维护的结构体中的msg_qbytes表示传输的最大字节数,意思是传输的大小有上限,
信号量
- 相当于内核中具有等待队列的计数器,用于资源计数,没资源就等待,有资源就唤醒
- 若计数<=0表示没有资源可用,没有资源字需要等待,如计数>=0表示有资源可用,则可以获取资源,然后计数减一,
- 如何等待:如果某一个进程释放了资源,并且有进程在等待,则计数加一,并且唤醒等待的进程
- 信号量实现进程间的同步与互斥(二元信号量,信号量的值只存在1和0),
三,几种进程间通信方式的对比
匿名管道和命名管道优缺点
优点:
- 如果传输的数据大小在规定范围内,4k以内就会自带同步与互斥,很安全
- 命名管道可以实现同一台主机的任意两个进程之间的通信问题
缺点:
- 底层的缓冲区太小,导致传输的数据大小有上限
- 匿名管道只用于有亲缘关系的进程间通信,有局限
- 管道是半双工的通信的,数据只能流向一个方向,如果要实现双向通信,要创建两个管道,很繁琐
- 管道传输以流的方式传输,导致数据粘连,数据本身没有边界
消息队列
优点:
- 自带同步与互斥机制,底层缓冲区或者单消息的大小只要不超过64k就不会就很安全
- 可以提供双向通信,要加一个标识
- 可以通过消息中的type进行分类,将type值设置为进程的pid,就可以实现父进程处理父进程接收的任务,子进程处理子进程的任务
缺点:
- 缓冲区还是太小了,存在上限,
- 只能用于同一个key值的进程间通信,只要有相同的key值,进程就可以定位到同一个消息队列上
共享内存
优点:
- 几乎没有上限
- 没有单向通信的限制
- 不局限于父子进程间的通信