进程间通信上(IPC)
总共五种,分别是:管道、信号量、消息队列、共享内存、套接字
1.管道
管道又分为命名管道和匿名管道
作用:比方说我现在想在a进程与b进程之间通信,希望在a进程写入数据,b进程读取数据,这时候就可以使用管道。那去使用.txt文件不是一样可以做到吗?a进程在.txt文件写入内容,b进程在.txt文件读取内容。是的,使用纯文本文档一样可以做到,但是管道的效率远超文件(因为纯文本文档的写入与读取需要使用I/O设备,而写入管道的数据会存放在内存中)。
命名管道:
创建命名管道:使用mkfifo+文件名
(删除方法与其他文件也一致)
使用示例:
a.c文件→
b.c文件→
很简单的两串代码,主要还是open()/close()和write()/read()函数能否熟练使用,那么,此时只编译运行a.c:
发现程序直接阻塞住了(甚至连fd的值都没有打印出来)
同理只运行b.c也是阻塞状态:
只有当两个进程同时运行时:
此时fd的值才能正常打出,此时给a进程写入数据→
此时两个进程同时结束,可见通信最少必须在两个进程之间完成,同时管道也是最基础的IPC机制
若是改成循环(即进程间多次发生通信)
a.c b.c
主要注意循环终止条件!当管道空时,读端会阻塞;管道满时,写段会阻塞。写段先关闭,读段read()返回值==0;读段先关闭,写段write()会触发信号SIGPIPE(13)程序退出
管道文件不占用内存,只是一个接口,实际上的内容写在内存开辟的一块空间上
匿名管道:
匿名管道主要用于父子进程之间的通信,它不需要先创建文件,而是直接在代码中通过函数创建匿名管道:
pipe()函数创建匿名管道,成功返回0,失败返回-1;参数为数组,其中pipefd[0] 代表读端(读权限)。pipefd[1]代表写端(写权限),以下为示例:
若是在父子进程中使用:
#include <unistd.h> int main() { int fd[2]; pipe(fd); // 创建管道,fd[0]读端,fd[1]写端 if (fork() == 0) { // 子进程 close(fd[0]); // 关闭读端 write(fd[1], "Hello", 6); } else { // 父进程 close(fd[1]); // 关闭写端 char buf[10]; read(fd[0], buf, 6); printf("Received: %s\n", buf); } }总结:管道是一个半双工通信(什么是半双工?即两个进程只能由一个读取数据,一个写入数据,但是对哪边进行读取或写入不做要求,而手机通信是全双工通信,收音机是单工通信)。命名管道可以在任意两个进程间通信,匿名管道只能在父子进程之间通信,管道本质上是内核维护的内存缓冲区。
2.信号量
信号量是一种用于进程/线程同步的机制,它不仅仅用于进程间通信,更核心的作用是控制多个进程/线程对共享资源的访问。
- 信号量的值:0,1,2,3都可以,需要多少信号量由程序员自己来决定
- p操作(等待):对信号量减一,若信号量的值为0,则阻塞
- v操作(释放):若有进程此时在等待,则唤醒等待进程,否则信号量+1
想象一个停车场:
信号量的值= 停车场的空位数量。
P操作(等待) = 一辆车进入停车场。空位减一。如果没有空位,车子就必须等待。
V操作(发送) = 一辆车离开停车场。空位加一。如果有车在等待,就放一辆车进来。
临界资源:同一时刻只允许一个进程访问的资源
临界区:访问临界资源的代码段
信号量函数:
#include <sys/sem.h> int semget(key_t key, int nsems, int semflg);参数key_t key,唯一唯一标识信号量集的键值。(可以理解为同一个门牌号,只要门牌号一样就是同一间房间,不需要特定的门牌号)。
参数int nsems,信号量个数,想定义几个信号量就定义几个。
参数int semflg,权限标志和创建选项的组合,权限位与文件权限类似(0600),创建选项:
IPC_CREAT:如果不存在则创建,如果已存在则忽略
IPC_EXCL:与IPC_CREAT一起使用,如果已存在则失败返回值:创建成功返回信号量集的标识符(非负整数),失败返回-1
int semctl(int semid, int semnum, int cmd, ...);参数int semid,由函数semget()返回的信号量集
参数int semnum,信号量在集合中的索引(类似数组下标,例如创建了两个信号量,则第一个信号量索引为0,第二个信号量索引为1,以此类推)
参数int cmd,要执行的控制命令。
常用命令:
IPC_RMID:立即删除信号量集
SETVAL:设置单个信号量的值
GETVAL:获取单个信号量的值
SETALL:设置所有信号量的值
GETALL:获取所有信号量的值
IPC_STAT:获取信号量集的状态信息第四个参数...,可变参数,通过自己定义的联合体union semun来输入
union semun { int val; // SETVAL 用的值 struct semid_ds *buf; // IPC_STAT, IPC_SET 用的缓冲区 unsigned short *array; // SETALL, GETALL 用的数组 struct seminfo *__buf; // IPC_INFO 用的缓冲区(Linux特有) };
int semop(int semid, struct sembuf *sops, size_t nsops);参数int semid,由函数semget()返回的信号量集
参数struct sembuf* sops,指向操作数组的指针(其实就是指向一个结构体的指针,不知道为啥非要这样说...)
参数size_t nsops,操作数组的操作个数
sembuf结构体:
struct sembuf { unsigned short sem_num; // 信号量在集合中的索引 short sem_op; // 操作类型 short sem_flg; // 操作标志 };结构体参数unsigned short sem_num,信号量在集合中的索引(同上,类似数组下标)
结构体参数short sem_op,赋值-1为p操作,1为v操作
结构体参数short sem_flg,通常填写SEM_UNDO(进程退出时自动撤销操作,防止死锁)或0(默认阻塞行为)
此时存在两个进程text和tmp,代码如下
tmp.c text.c
此时同时运行这两个程序(放在后台可一起运行)→
会发现这两个进程同时混在一起运行,无法做到一个进程运行完一次循环另一个进程再去运行。
通过对上面三个函数进行封装得到自己的信号量函数:
sem_init()
sem_p()
sem_v()
sem_destroy()
此时再对text进程与tmp进程使用信号量进行控制:
tmp.c text.c
运行结果:
总结:信号量是一个功能强大的同步原语,其核心是一个受保护的整型变量和两个原子操作(P 和 V)。它通过控制进程的"等待"和"唤醒"来协调对共享资源的访问。
个人小结:管道这里倒是通俗易懂,信号量的接口实在是太多了...真的就是只能理解理解原理了,联合体需要自己编写,结构体需要记,还有各种特殊参数QAQ。真的不好记,进程间通信上课还因为前一天晚上熬夜导致没有好好听课,现在补回放只感觉路漫漫其修远兮啊。