目录
1. 进程间通信
1.1 进程间通信目的
进程之间具有独立性(每个进程都有自己的虚拟地址空间,访问的都是自己的虚拟地址,将一个数据的地址就算交给其他进程,其他进程在自己的虚拟地址空间隐射实际上是没有这个数据的)因此进程间无法直接通信,但是在中大型项目中多个进程协同工作很常见,这个时候进程通信就成为必要。
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
1.2 进程间通信分类
管道、共享内存、消息队列、信号量。
- 管道
匿名管道pipe
命名管道 - System V IPC
System V 消息队列
System V 共享内存
System V 信号量 - POSIX IPC
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
2. 管道(PIPE)
2.1什么是管道
- 生活中的管道:
自来水管、污水管、天然气管道,多种多样,但是目的都是传输资源。并且在传输资源中具有半双工通信的特性。 - 半双工通信
可以选择方向的单向通信。 - 本质
命名管道:内核中的一块缓冲区就是内核中开辟的一块内存,多个进程访问同一个缓冲区实现通信。 - 匿名管道的分类
匿名管道:内核中的一块缓冲区,没有标识符,无法被其他进程找到(只能用于亲缘关系的进程间通信)
内核中开辟的这块缓冲区,具有标识符,能够被其他进程找到。(可以用于同一主机上任意进程间通信)
2.2 匿名管道
- 匿名管道:
一个进程通过系统调用在内核中创建了一个匿名管道,为了能够让用户操作这个管道,因此调用返回了文件描述符作为这个管道的操作句柄。
其他进程因为这个管道没有标识符,找不到所以无法进行通信。
但是如果这个创建管道的进程创建了一个子进程。这时候子进程复制了父进程(包括文件描述信息表),所以子进程相当于也有这个文件描述符可以操作这个管道。
匿名管道——只能用于具体亲缘关系的进程间通信。
只有通过子进程复制父进程的方式才能获取到同一个管道的操作句柄。
2.3 命名管道
- 命名管道:
一个进程创建一个命名管道,这个命名管道会在文件系统中创建出一个管道文件(可以看得到的,实际上就是管道的名字),多个进程通过打开同一个管道文件,访问内核中统一个缓冲区实现通信。
注意:命名管道文件只是一个名字,只是为了作为一个标识让进程能够找到同一缓冲区
2.4 管道接口介绍
2.4.1匿名管道的创建
- 匿名管道的创建:int pipe(int pioefd[2]);
- pioefd[2]:具有2个int元素的数组,用于接收管道创建成功返回的文件描述符——操作句柄。
- pioefd[0]:用于从管道中读取数据;
- pioefd[1]:用于向管道中写入数据。
- 返回值:成功返回0;失败返回-1;
- 注意:匿名管道的创建一定要在创建子进程之前,这样才能被子进程复制到操作句柄。
- 关闭文件描述符:并不是释放缓冲区,也不是删除文件,只是断开了和管道的一端连接而已。缓冲区是当连接数为0(所有打开管道的句柄都关闭了才会被释放)。
- 创建举例:
编写makefile文件
然后make一下~然后运行程序,
2.4.2 命名管道的创建
- 本质:内核中的一块缓冲区(但是具有一个可见于文件系统的管道文件作为名字,多个进程通过打开同一个管道文件访问同一个管道)
- 命令:makefifi filename
创建命名管道文件(创建命名管道文件,并不会立即创建内核中的缓冲区,而是在有进程访问的时候再去创建。) - 接口:int makefifo(char * filename,mode_t mode);
- 独特的打开特性:如果命名管道以只读打开,则会阻塞,直到管道文件被其他进程以写的方式打开;如果命名管道被只写打开,则会阻塞,直到管道文件被其他进程以读的方式打开。
- 原理:管道的缓冲区没有进程确定要写入数据且有进程读数据的情况下,缓冲区就没必要开辟,空占资源。
2.5 管道的特性(匿名和命名通用特性)
2.5.1 半双工通信
可以选择方向的单向通信。
2.5.2 管道的读写特性:
如果管道中没有数据,则read会阻塞,如果管道中数据满了,则write会阻塞。
阻塞:为了完成一个功能,发起调用,功能无法完成则调用一直等待。
非阻塞:为了完成一个功能,发起调用,功能无法完成则调用报错返回。
- 阻塞举例
结果
由上面的例子我们可以看到我们的管道是有大小的:大概接近64K。管道是内核的一块缓冲区,所以不能无限制增长。(会造成资源耗尽系统奔溃)
2.4.3 其他特性:
- 管道的操作是会阻塞的,但是如果管道的所有写端被关闭了(即管道已经不可能写入数据),这时候read读取完管道的数据后,就不再阻塞了,而返回0。(这里的0表示没人写入了,而不能单单以为是没有数据了)
代码示例:
注意:我们一开始只关闭了子进程的读端,没有关闭父进程的读端,然后程序就阻塞了。要注意一定要所有的读端都关闭。
结果展示:
2. 管道的读端被关闭了(管道没人读数据了),则继续write就会触发异常,导致进程退出。
代码示例:
结果:
- 自带同步与互斥
互斥:通过同一时间只有一个进程能够访问保证操作安全。管道中数据的操作大小不超过PIPE_ BUF大小保证原子操作。
同步:通过一些条件限制保证进程对资源访问的秩序(不能一直写,也不能一直读)。管道没有数据则read阻塞;管道数据满了则write阻塞。
原子操作:要么完成要么不做,中间不会被打断的操作
2.6 shell中管道符的实现
- ps -ef | grep **
- 管道符:连接两个命名,将前边命名的输出结果,作为后边命令的输入传递给后边的命令进行处理。——通过匿名管道实现的。
- ps -ef:ps进程,功能:将所有的进程信息打印出来(写入到标准输出) grep ssh:grep 进程,不断的捕捉标准输入的
数据进行字符串匹配,然后输出 - 管道符的实现:让前面的命令进程,不再把数据写入到标准输出,而是写入到一个管道中;让后面的命令进程,不再从标准输入读取数据,而是从一个管道中读取。
- 代码演示:
思路:
- 创建一个管道
- 创建PS进程(创建一个子进程,程序替换成为ps),在程序替换前,把标准输出重定向到管道。
- 创建grep进程(创建一个子进程,程序替换成为ps),在程序替换之前,把标准输入重定向到管道。
- 等待子进程退出
代码演示:
注意:程序退出的时候,需要把写端关闭。但是要在wait之前。
结果
3. 共享内存
3.1 特性
- 特性:所有进程间通信方式中最快的一种;生命周期随内核。
- 本质原理:开辟一块物理内存,需要进行通信的进程将这块物理内存映射到自己的虚拟地址空间中,直接使用自己的用户空间地址进行访问。
共享内存速度快,是因为通过虚拟地址直接访问内存,相较于其他的方式少了两次用户空间与内核空间之间的数据拷
贝共享内存是一种覆盖式操作。
3.2 操作流程
操作流程:
- 创建或打开共享内存
- 将共享内存映射到虚拟地址空间
- 内存操作(memcpy, prin…)
- 解除映射关系
- 删除共享内存(实际上就算进行了删除操作,共享内存也不会立即被删除,等到共享内存的映射连接数为0的时候才会删除)
- 1.创建或打开
int shmget(key_ t key, size_ t size, int shmflag);
key:共享内存标识符(为了让多个进程找到同一个);
size:需要创建的共享内存大小(仅创建时有效);
shmflag: IPC_ CREAT| IPC_ EXCL | 0664(IPC_ CREAT:不存在则创建,存在则打开。IPC_ EXCL与IPC CREAT搭配使用,表示存在则报错,不存在则创建打开);
mode: 0664 设置共享内存访问权限;
返回值:成功返回一个非负整数——操作句柄;失败返回-1.
- 2.建立映射关系:
void * shmat (int shm_id, void * shmaddr , int shmflag)
shm_ id: shmget返回的操作句柄;
shmaddr:通常置NUL,让系统自动分配建立映射;
shmflag: SHM RDONLY- 只读; 0-默认是可读可写;
返回值:成功返回映射的首地址;失败返回(void*)- 1。
- 3.解除映射关系
int shmdt(void *shm_start)
shm_start:映射首地址-shmat的返回值
返回值:成功返回0;失败返回-1
- 4.删除共享内存:
int shmctl(int shm_id, int cmd,struct shmid ds *buf)
shmid:操作句柄(表示我们要操作哪一块内存);
cmd: IPC_RMID -标记删除(不再接受新的映射)
buf:用于获取共享内存信息,用不上就置NULL
返回值:成功返回0;针对IPC_RMID失败返回-1;
举例:
(1)vi shm_read.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/shm.h>
#define IPC_KEY 0x12345678
#define PROJ_ID 0x87654321
int main()
{
//key_t ftok(const char *pathname, int proj_id);
//创建标识符 key_t key = ftok("./", PROJ_ID);
//
//创建或打开 int shmget(key, size, shmflg)
int shmid = shmget(IPC_KEY, 4096, IPC_CREAT|0664);
if (shmid < 0) {
perror("shmget error");
return -1;
}
//建立映射关系 void *shmat(shmid, shmaddr, shmflg)
void *shm_start = shmat(shmid, NULL, 0);
if (shm_start == (void*)-1) {
perror("shmat error");
return -1;
}
while(1) {
printf("%s\n", (char*)shm_start);
sleep(1);
}
//解除映射关系 int shmdt(void *shmstart);
shmdt(shm_start);
//删除共享内存 int shmctl(shmid, cmd, buf)
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
(2)vi shm_write.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#define IPC_KEY 0x12345678
#define PROJ_ID 0x87654321
int main()
{
//key_t ftok(const char *pathname, int proj_id);
//创建标识符 key_t key = ftok("./", PROJ_ID);
//
//创建或打开 int shmget(key, size, shmflg)
int shmid = shmget(IPC_KEY, 4096, IPC_CREAT|0664);
if (shmid < 0) {
perror("shmget error");
return -1;
}
//建立映射关系 void *shmat(shmid, shmaddr, shmflg)
void *shm_start = shmat(shmid, NULL, 0);
if (shm_start == (void*)-1) {
perror("shmat error");
return -1;
}
int i = 0;
while(1) {
//snprintf(shm_start, 4096, "风好冷~%d", i++);
char buf[] = {'a', 'b'};
memcpy(shm_start, buf, 2);
sleep(1);
}
//解除映射关系 int shmdt(void *shmstart);
shmdt(shm_start);
//删除共享内存 int shmctl(shmid, cmd, buf)
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
(3)vi makefile
all:shm_read shm_write
shm_write:shm_write.c
gcc $^ -o $@
shm_read:shm_read.c
gcc $^ -o $@
(4)开两个终端分别运行./shm_read ./shm_write
(5)通过命令ipcs -m 查看
3.3 注意
- 共享内存是一种覆盖式内存操作
- 共享内存的操作需要考虑安全问题(没有同步互斥关系)
4. 消息队列
4.1 本质
在内核中创建一个优先级队列;具有标识符能被其他进程找到。多个进程通过访问同一个队列,通过添加或者获取节点实现通信。
消息队列传输的都是数据节点,节点中包含两个信息:(1)类型(2)数据
4.2 特性
- 双工通信
- 自带同步和互斥
- 生命周期随内核
5. 信号量
本节所说的进程间通信的方式都是systemV标准的
5.1 本质
信号量本质上就是内核中的一个计数器
作用:实现进程间的同步和互斥(保护进程间对临界资源的访问操作)
临界资源:大家都能访问的资源
- 如何实现保护操作:
同步:通过一些条件让进程资源的访问更加有序
互斥:通过让进程同一时间对资源的唯一访问保证操作安全 - 信号量实现同步和互斥原理
通过计数器对资源进行技术,若计数大于0则表示可以访问资源,若资源小于等于0则表示不能访问,则阻塞进程。 - 操作
P操作:在进程访问资源之前进行,判断计数是否大于0,大于0则正确返回,计数-1;大于0,则阻塞进程,计数-1;
V操作:当产生一个新的资源后,计数+1,唤醒一个阻塞进程。 - 实现互斥:
初始化临界资源计数器为1;(计数为1表示资源只有一个)
在访问临界资源之前进行P操作
在访问临界资源之后进行V操作 - 实现同步:
根据资源数量初始化计算器。访问/获取资源之前进行P操作。产生一个新资源之后进行V操作。