进程之间具有独立性, 无法直接通信, 但是在日常工作中我们通常需要多个进程协同工作.
进程通信的分类(systemV标准): 管道 PIPE, 共享内存, 消息队列, 信号量
所有进程通信方式的本质: 是内核空间中的一块缓冲区(内存).
管道本质:
是内核空间中的一块缓冲区(内存). 内核空间是一块公共区域, 多个进程通过访问这个公共区域内的缓冲区实现进程通信
管道特性:
1半双工通信, 可以选择方向的单向通信(用户决定哪个进程读哪个进程写).
2生命周期随进程结束而结束.
3以字节为单位持续传输数据. 先进先出, 先写的先读, 读和写有各自的读写位置, 不需要自己用fseek或lseek设置读的位置, 读取会接着上次读到的位置 后面写的数据不会覆盖前面写入的数据, 也是接着上次写的之后.
4自带同步和互斥特性, 同步就是通过一些条件限制进程访问资源的秩序, 保证了资源不被某一个进程长时间占用. 互斥则是同一时间只有一个进程能访问,保证了操作的安全性.
5管道中没有数据read阻塞直到write写入数据后read读取. 管道写满数据则write阻塞.
6如果管道所有写端被关闭(管道已经不可能写入数据),这时候read读取完管道中的数据后,就不再阻塞了,而返回0(这里的0表示没人写入了,千万不要单纯认为是没数据了)
管道的所有读端被关闭(管道没人读取数据),则继续write就会触发异常,进程退出.
管道分有匿名管道和命名管道:
匿名管道的创建: int pipe(int pipefd[2])
pipefd[2]:具有两个int元素的数组, 用于接收管道创建成功所返回的文件描述符(操作句柄).
其中pipefd[0]用于从管道中读取数据, pipefd[1]用于向管道内写入数据.
返回值: 成功的返回0, 失败返回-1;
创建了一个匿名管道后, 其他进程并没有这个管道的操作句柄, 但是在创建管道后创建子进程, 此时子进程复制了父进程的文件描述信息表,所以子进程也有了能操作这个管道的操作句柄, 所以说匿名管道只能用于有亲缘关系的进程间通信
注意:子进程和父进程之间的读写端都是各自独立的. 子进程的读写端是子进程自己的和父进程的不一样, 所以关闭时子进程父进程都要各自关闭自己的读写端:pipefd[0],pipefd[1].
实现:
管道读写特性:
1如果管道中没有数据, read会阻塞.
阻塞: 为了完成一个调用的功能, 功能无法实现时会一直等待.
非阻塞: 为了完成一个调用的功能, 功能无法实现则会直接报错返回.
例如在读写的时候, 写端3秒后再写, 则读端会阻塞3秒才能读到数据:
2.如果管道中数据满了, write会阻塞.
可以看到写了65440个字节之后阻塞了一会无法继续写入了,6秒之后父进程开始读(子进程因为while(1)还没结束所以还在阻塞呢), 所以而可以看出管道的大概大小就是64k.
3.管道所有写端被关闭(管道已经不可能写入数据),这时候read读取完管道中的数据后,就不再阻塞了,而返回0(这里的0表示没人写入了,千万不要单纯认为是没数据了). 写端都关闭再写则报错
4.管道的所有读端被关闭(管道没人读取数据),再write时就会触发异常,进程退出. 读端都关闭再读则报错
shell中管道符 '|' 的实现 : ps -ef | grep **
让前边的命令的数据不再写入标准输出中, 而是写入一个管道中 , 再让后面的命令不从标准输入里读数据, 而从前面命令写入的管道中读取. grep指令本身就是不断从标准输入中读取数据不退出
命名管道也是内核中的一块缓冲区, 但是具有可见于文件系统的管道文件名,所以命名管道可以用于同一主机上任意进程间的通信
创建命名管道方式:
1.通过mkfifo filename命令创建管道文件,filename是管道文件的文件名.
2.在读写文件中使用int mkfifo(char* filename, mode_t mode); 接口创建命名管道.
创建命名管道文件之后并不会直接在内核缓冲区中创建一块命名管道文件的区域,而是在进程访问的时候才会开辟空间(节省资源).
命名管道独特的打开特性:
以只读方式打开命名管道, 则会阻塞, 等待管道文件被其他进程以写的方式打开后再写.
以只写方式打开命名管道, 则会阻塞, 等待管道文件被其他进程以写的方式打开后再读.
对于下面的文件,如果fifo_write是循环写入,则fifo_read会一直读取,此时关闭fifo_write程序,fifo_read则会返回零表示所有写端被关闭.
本质原理:开辟一块物理内存 , 需要进行通信的进程将这块物理内存映射到自己的虚拟地址空间中 , 直接使用自己的用户空间地址进行访问.
特性:
1.共享内存是所有进程通信中方式中最快中的一种: 因为是通过虚拟地址直接访问内存, 相较于其他方式少了 用户态和内核态之间写入和读取两次数据拷贝操作(即没有任何的read,write, open数据拷贝操作).
2.共享内存生命周期随内核,在不人为删除的情况,所有映射进程退出也不会释放共享内存
3.共享内存是覆盖式操作, 下次写入的数据会覆盖之前写入的数据.
4.没有同步与互斥关系, 需要考虑安全问题. 所以用共享内存一般也会用下面的信号量保证安全.
操作流程:
1.创建或打开共享内存
int shmget(key_t key, size_t size, int shmflag);
key:共享内存标识符(为了让多个进程找到同一个), 自行设置, 0x12345678形式(四个字节). 或者使用创建共享内存标识符接口: key_t ftok(const char* pathname, int proj_id)
size:需要创建的共享内存大小(仅创建时有效,打开共享内存时(已创建有时)此数字无效.), 通常设置为页面大小整数倍.
shmflag: IPC_CREAT | 0664 IPC_CREAT:不存在则创建,存在则打开. IPC_EXCL:表示存在则报错,不存在则创建打开, 使用IPC_EXCL则必须使用IPC_CRATE. IPC_PRIVATE:设置为私有的,此时只有子进程复制父进程文件信息表之后只有子进程能访问这块共享内存,就像匿名管道一样mode: 0664设置共享内存访问权限,也是创建时有效.必须设置,不然连建立映射的权限都没有.
返回值:成功返回一个非负整数-操作句柄;失败返回-1;
⒉将共享内存映射到虚拟地址空间
void *shmat(int shm_id,void *shmaddr,int shmflag)
shm_id: shmget返回的操作句柄
shmaddr:通常置NULL,让系统自动分配建立映射
shmflag: SHM_RDONLY-只读;O-默认是可读可写
返回值:成功返回映射的首地址(shm_start:映射首地址);失败返回(void*)-1
3.内存操作(memcpy, printf....), 通过shmat返回的映射首地址操作修改共享内存内容.
4.解除映射关系
int shmdt(void* shm_start)
shm_start:shmat返回的首地址, 成功返回零, 失败返回-1. 进程终止也会关闭映射关系.
5.删除共享内存(实际上进行了删除操作,共享内存也不会立即被删除,而是等到共享内存的映射连接数为0的时候才会删除)
int shmctl(int shm_id,int cmd,struct shmid_ds *buf)
shm_id:操作句柄, shmget的返回值.
cmd: IPC_RMID , 标记删除, 不再接受新的映射,映射连接数为0时删除
buf:用于获取共享内存信息,用不上就置NULL
返回值:成功返回0;针对IPC_RMID失败返回-1;
实现:
shm_read.c:
shm_write.c:
结果如下图所示: 共享内存里没有数据读取是不会阻塞的(没有read接口, 从shm_start开始读读到\0读完然后换行), 然后向共享内存写入数据后就能读到, 此时若ctrl+c终止掉两个连接进程,连接数为0, 但是共享内存还没有删除, 因为我们是终止掉的, 进程在while死循环没有走下去进行shmctl删除共享内存, 没有人为删除共享内存声明周期是随内核的. 若映射连接数不为0的时候人为删除(ipcrm -m指令)共享内存, 这时共享内存是不会删除的, 会做一个标记,这个标记使得该共享内存不会接受后面来的新的映射建立且映射连接数为0时共享内存自动删除了,不需要再删除一次.
共享内存写入是覆盖式的, 若写入新的数据,就会从头开始覆盖. 比如另一个写进程(或某进程修改了写入的数据)写ab, 则就变成abllo bit.
本质: 内核中创建的优先级队列,具有标识符能被其他的进程找到.
特性: 双工通信 自带同步与互斥 声明周期随内核
本质: 内核中的一个计数器.
作用: 实现进程间的同步与互斥, 保护进程间对临界资源(大家都能访问的资源)的访问
信号量实现同步与互斥的原理: 通过计数器对资源进行计数, 计数大于零则表示可以访问资源, 计数小于等于0表示不能访问, 使访问资源的进程阻塞.
其他进程产生一个资源后, 计数加1 唤醒一个阻塞进程.
p操作: 在进程访问资源前进行,判断计数是否大于0. 大于0则正确返回计数-1, 小于零则阻塞进程,计数-1.
v操作: 产生一个新的资源, 计数加1, 唤醒一个阻塞进程.
实现互斥:初始化临界资源计数器为1 , 访问临界资源之前进行p操作 , 访问临界资源之后v操作.
实现同步:根据资源数量初始化计数器, 访问资源前p操作, 产生一个新资源后v操作.