文章目录
- 管道
- 匿名管道 pipe
- 命名管道 FIFO
- 共享内存
- 共享内存的使用流程:
- 消息队列
- 信号量
- 套接字
在之前的博客中讲过,虚拟空间出现的其中一个目的就是解决 进程没有独立性,可能访问同一块物理内存 的问题。因为这种独立性,进程之间无法直接进行通信,操作系统为了解决这种问题,提出了多种适用于不同情境下的通信方式:
- 数据传输:管道、消息队列
- 数据共享:共享内存
- 进程控制:信号量
管道
管道的本质其实就是内核中的一块缓冲区,多个进程通过访问同一个缓冲区就可以实现进程间的通信。自 Linux 2.6.11
内核起,管道容量的大小默认是 65536
字节,但可以通过 fcntl函数
来修改管道容量。
管道分为两种:匿名管道、命名管道
匿名管道 pipe
因为没有具体的文件描述符,所以只能用于具有亲缘(父子)关系的进程之间的通信。
原理: 父进程在创建管道的时候操作系统会返回管道的文件描述符,然后生成子进程时子进程会通过拷贝父进程的 pcb
来获取到这个管道的描述符,所以他们可以通过这个文件描述符来访问同一个管道,来实现进程间的通信。而不具备亲缘关系的进程则无法通过这个文件描述符来访问同一个管道。
返回值:成功返回 0
,失败返回 -1
。
也就是说读/写操作的流程是:
- 关闭 读/写 端
- 进行 写/读 操作
- 写/读 完关闭 写/读 端
图解父子进程通过管道通信的流程:
一开始父进程创建管道
父进程fork创建子进程
关闭多余描述符
代码示例:示例一 示例二
如果没有特意规定,那么父子进程究竟是谁先执行是不确定的,假设如果子进程还没写入,父进程却已经开始读了,这时候应该是会读不到东西的,会在屏幕上输出空行,但是这种情况并没有发生,这里就牵扯到了管道的读写特性:
- 如果管道中没有数据,则调用
read
读取数据会阻塞。 - 如果管道中数据满了,则调用
write
写入数据会阻塞。 - 如果管道的所有 读端 被关闭,继续调用
write
时,会因为无法读出而产生异常导致进程退出。 - 如果管道的所有 写端 被关闭,继续调用
read
时,read
读完管道中的所有数据后不再阻塞,返回0
退出。
命名管道 FIFO
命名管道也是内核中的一块缓冲区,但是它 具有标识符 。这个标识符是一个可见于文件系统的管道文件,能够被其他进程找到并打开管道文件来获取管道的操作句柄,多个进程可以通过打开这个管道文件来访问同一块缓冲区来实现通信。
- 可以在没有亲缘关系(非父子)的进程之间进行通信,这是与无名管道最大的区别。
- 他是以一个特性的文件存储在文件系统中, 所以对他的操作与其路径名有关联。
接口:
int mkfifo(const char *filename,mode_t mode);
filename
——管道的标识符,通过这个标识符来访问管道,创建之前这个标识符必须不存在。
mode
——权限掩码。
返回值——若成功则返回0
,否则返回-1
。
代码示例: 命名管道实现进程的信息传递【mkfifo函数、open函数】
open 打开命名管道的特性:
- 若文件以只读打开,则会阻塞,直到文件被以写的方式打开。
- 若文件以只写打开,则会阻塞,直到文件被以读的方式打开。
管道的特性:
- 管道是半双工通信(可以选择方向的单向传输),这个可以从上面的示意图看出来。
- 管道的读写特性(无论命名匿名都一样)。
- 管道声明周期随进程,打开管道的所有进程退出后管道就会被释放。
- 管道提供字节流传输服务。
- 命名管道额外有一个打开特性,只读打开会阻塞直到被以写打开,只写打开会阻塞直到被以读打开。
- 管道自带同步和互斥。
共享内存
共享内存即在 物理内存 上开辟一块空间,然后 多个进程 通过 页表 将这 同一个物理内存 映射到自己的 虚拟地址空间 中,通过自己的 虚拟地址空间 来访问这块 物理内存 ,达到了数据共享的目的。
也正是因为这种特性,使得 共享内存成为了最快的进程间通信的方式 ,因为它 直接通过虚拟地址来访问物理内存,比前面的管道和后面的消息队列 少了内核态和用户态的几次数据拷贝和交互 。
特点:
- 生命周期随内核。
- 由于多个进程可以同时操作,因此需要进行同步。但不自带同步与互斥,而是借助信号量来实现同步与互斥。
共享内存的使用流程:
1. 创建共享内存
头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
定义函数:
int shmget(key_t key, size_t size, int shmflg)
参数:
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的。返回值:成功返回共享内存标识符,失败返回-1。
2. 将共享内存映射到虚拟地址空间
头文件:
#include <sys/types.h>
#include <sys/shm.h>
定义函数:
void *shmat(int shmid, const void *shmaddr, int shmflg)
参数:
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:权限标志返回值:成功返回指向共享内存映射在虚拟地址空间的指针(即首地址),失败返回-1。
3. 共享内存管理
头文件:
#include <sys/types.h>
#include <sys/shm.h>
定义函数:
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
参数:
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构返回值:成功返回0,失败返回-1
4. 解除映射关系
头文件:
#include <sys/types.h>
#include <sys/shm.h>
定义函数:
int shmdt(const void *shmaddr)
参数:shmaddr: 由shmat所返回的指针
返回值:成功返回0,失败返回-1
消息队列
概念:
- 消息队列是内核中的一个优先级队列,多个进程通过访问同一个队列,进行添加节点或者获取节点来实现通信。
- 消息队列是消息的连接表, 放在内核中, 一个消息队列由一个标识符表示。
- 消息队列是面向记录的, 其中的消息具有特定的格式以及特定的优先级。
- 消息队列独立于发送和接受进程中, 也就是当一个进程被销毁, 他在消息队列中的信息是不会被删除的。
- 消息队列中的内容可以实现随机查询, 也就是消息不一定要以
FIFO
读取, 也可以按消息的类型读取。
特性:
- 自带同步与互斥
- 生命周期随内核
流程:
1. 创建消息队列
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
定义函数:
int msgget(key_t key, int msgflg)
参数:
key:消息队列对象的关键字
msgflg:消息队列的建立标志和存取权限返回值:成功执行时,返回消息队列标识值。失败返回-1
2. 进程可以向队列中添加/获取节点
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
添加节点:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
获取节点:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:
msqid:消息队列对象的标识符
msgp:消息缓冲区指针
msgsz:消息数据的长度
msgtyp:决定从队列中返回哪条消息
msgflg:消息队列状态返回值:成功执行时,返回0。失败返回-1
3. 删除消息队列
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
定义函数:
int msgctl(int msqid, int cmd, struct msqid_ds *buf)
参数:
msqid:消息队列对象的标识符
cmd:函数要对消息队列进行的操作
buf:取出系统保存的消息队列的 msqid_ds 数据,并将其存入参数 buf 指向的 msqid_ds 结构中返回值:成功执行时,返回0。失败返回-1
信号量
概念:
- 信号量与IPC结构不同,它其实是 内核中的一个计数器和阻塞队列 ,通过信号量来对 临界资源的访问进行控制,来 实现进程间的同步与互斥 ,而不是用于进程间消息的通信的。
- 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
- 信号量基于操作系统的
PV
操作,程序对信号量的操作都是原子操作。 - 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
例如:有一个能容纳 n
人的餐厅,用一个计数器表示 n
,如果有人进入则 n - 1
,如果有人出来则 n + 1
,只有 n > 0
时才能进入,如果 n <= 0
时,则说明没有位置,需要将进程挂起并放入阻塞队列中,直到有人出来使资源释放时,才能将后续进程从阻塞队列中唤醒获取资源。
- 同步:通过条件判断实现临界资源访问的合理性
- 互斥:通过同一时间的唯一访问来实现临界资源访问的安全性
POSIX信号量: POSIX
信号量和 SystemV
信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但 POSIX
可以用于线程间同步。
流程:
#include <semaphore.h>//初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
/*
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初值
*///销毁信号量
int sem_destroy(sem_t *sem);//等待信号量
int sem_wait(sem_t *sem);
//功能:等待信号量,会将信号量的值减1//发布信号量
int sem_post(sem_t *sem);
//功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
套接字
使用套接字也可以实现进程间的通信,与其他机制不同的是,它可以实现不同机器之前的进程间的通信。