第一个程序:共享内存读取程序(消费者)
该程序作为消费者,从共享内存中读取数据,通过信号量保证只有当生产者写入数据后才能读取。
/*4 - 读共享内存*/
#include<stdio.h> // 标准输入输出库
#include<stdlib.h> // 标准库,包含exit等函数
#include<unistd.h> // 包含UNIX系统调用,如sleep
#include <sys/types.h> // 定义系统数据类型
#include <sys/ipc.h> // 包含IPC(进程间通信)相关函数定义
#include <sys/shm.h> // 共享内存相关函数定义
#include <sys/sem.h> // 信号量相关函数定义
#include <string.h> // 字符串处理函数库int main()
{//1、确定 文件路径名 ---获取消息队列的key值key_t key = ftok(".", 'a'); // 用当前目录和字符'a'生成唯一key值,用于标识共享内存和信号量if(key == -1){ // 检查ftok函数调用是否失败perror("ftok error"); // 输出错误信息return -1; // 程序异常退出}//2、根据key值 获取共享内存的ID,如果共享内存不存在则创建// IPC_CREAT表示不存在则创建,0666表示权限(所有者、组、其他用户都可读写)int shmid = shmget(key,100,IPC_CREAT|0666); if(shmid==-1){ // 检查shmget函数调用是否失败perror("shmget"); // 输出错误信息exit(-1); // 程序异常退出}//根据key创建/获取信号量集,创建1个信号量int semid = semget(key,1,IPC_CREAT|0666); if(semid==-1){ // 检查semget函数调用是否失败perror("semget"); // 输出错误信息exit(-1); // 程序异常退出}//3.映射共享内存到当前进程的地址空间,第二个参数0表示由系统自动分配地址void *p = shmat(shmid,0,0); if(p==(void *)-1){ // 检查shmat函数调用是否失败perror("shmat"); // 输出错误信息exit(-1); // 程序异常退出}//4.使用共享内存(消费者逻辑)struct sembuf buf; // 定义信号量操作结构体while(1){ // 无限循环,持续读取数据buf.sem_num = 0; // 操作第0个信号量(信号量集索引)buf.sem_op = -1; // 执行P操作(申请资源,信号量值减1)buf.sem_flg = 0; // 0表示阻塞模式,若资源不可用则等待semop(semid,&buf,1); // 执行信号量操作printf("data = %d\n",*(int *)p); // 从共享内存读取整数并打印}//5.不再使用可以解除映射(实际不会执行,因为上面是无限循环)shmdt(p);//6.不再需要可以删除共享内存(注释掉表示不自动删除)//shmctl(shmid,IPC_RMID,0);return 0;
}
第二个程序:共享内存写入程序(生产者)
该程序作为生产者,向共享内存中写入数据,通过信号量通知消费者可以读取数据。
/*5 - 信号量集保护共享内存*/
#include<stdio.h> // 标准输入输出库
#include<stdlib.h> // 标准库,包含exit等函数
#include<unistd.h> // 包含UNIX系统调用,如sleep
#include <sys/types.h> // 定义系统数据类型
#include <sys/ipc.h> // 包含IPC(进程间通信)相关函数定义
#include <sys/shm.h> // 共享内存相关函数定义
#include <string.h> // 字符串处理函数库
#include <sys/sem.h> // 信号量相关函数定义int main()
{//1、确定 文件路径名 ---获取消息队列的key值key_t key = ftok(".", 'a'); // 用当前目录和字符'a'生成唯一key值,与消费者保持一致if(key == -1){ // 检查ftok函数调用是否失败perror("ftok error"); // 输出错误信息return -1; // 程序异常退出}//2、根据key值 获取共享内存的ID,如果共享内存不存在则创建// IPC_CREAT表示不存在则创建,0666表示权限(所有者、组、其他用户都可读写)int shmid = shmget(key,100,IPC_CREAT|0666); if(shmid==-1){ // 检查shmget函数调用是否失败perror("shmget"); // 输出错误信息exit(-1); // 程序异常退出}//根据key创建/获取信号量集,创建1个信号量int semid = semget(key,1,IPC_CREAT|0666); if(semid==-1){ // 检查semget函数调用是否失败perror("semget"); // 输出错误信息exit(-1); // 程序异常退出}//设置信号量的初始值为0(用于同步,初始状态下没有数据可读)semctl(semid,0,SETVAL,0);//3.映射共享内存到当前进程的地址空间,第二个参数0表示由系统自动分配地址void *p = shmat(shmid,0,0); if(p==(void *)-1){ // 检查shmat函数调用是否失败perror("shmat"); // 输出错误信息exit(-1); // 程序异常退出}struct sembuf buf; // 定义信号量操作结构体//生产者逻辑while(1){ // 无限循环,持续写入数据printf("请输入一个整数:"); // 提示用户输入scanf("%d",(int *)p); // 将用户输入的整数写入共享内存buf.sem_num = 0; // 操作第0个信号量(信号量集索引)buf.sem_op = 1; // 执行V操作(释放资源,信号量值加1)buf.sem_flg = 0; // 0表示阻塞模式semop(semid,&buf,1); // 执行信号量操作,通知消费者有新数据}//5.不再使用可以解除映射(实际不会执行,因为上面是无限循环)shmdt(p);//6.不再需要可以删除共享内存(注释掉表示不自动删除)//shmctl(shmid,IPC_RMID,0);return 0;
}
两个程序的整体作用
这两个程序配合实现了基于共享内存和信号量的进程间通信,具体说明如下:
核心功能:
- 生产者程序(第二个)从用户输入获取整数,写入共享内存,并通过信号量通知消费者。
- 消费者程序(第一个)通过信号量等待,当检测到生产者写入数据后,从共享内存读取并打印该整数。
信号量的作用:
- 实现了生产者和消费者的同步:消费者必须等待生产者写入数据后才能读取,避免读取到空数据或旧数据。
- 信号量初始值为 0,生产者写入数据后执行 V 操作(值 + 1),消费者读取前执行 P 操作(值 - 1),确保数据读写顺序正确。
共享内存的作用:
提供了一个两个进程都能访问的内存区域,实现高效的数据传递(相比管道等方式,共享内存无需数据拷贝,速度更快)。
通过这种机制,两个独立进程可以安全、有序地进行数据交换。
如果没有 PV 操作(或类似的同步互斥机制),在多任务 / 多进程环境中会出现一系列严重问题,核心是共享资源访问冲突和任务执行顺序混乱,具体表现如下:
1. 共享资源 “争抢” 导致数据混乱(互斥问题)
假设有两个任务同时操作一个共享变量(比如银行账户余额):
- 任务 A 要给账户加 100 元,读取当前余额为 500 元,准备计算成 600 元;
- 此时任务 B 突然介入,读取余额还是 500 元,减去 50 元(计算成 450 元)并写入;
- 任务 A 接着执行,把自己计算的 600 元写入。
最终余额变成 600 元(正确结果应为 500+100-50=550 元),数据被破坏。
没有 PV 操作时,多个任务会 “同时” 抢占共享资源,导致操作交叉覆盖,结果错误。
2. 任务执行顺序失控(同步问题)
比如任务 A 负责采集传感器数据,任务 B 负责处理数据:
- 任务 B 可能在任务 A 还没采集到数据时就开始处理,导致处理 “空数据”;
- 任务 A 可能连续采集了多组数据,但任务 B 只处理了其中一部分,导致数据积压或丢失。
没有 PV 操作时,任务间缺乏 “等待 - 通知” 机制,无法按逻辑顺序执行,系统功能紊乱。
3. 系统陷入 “死锁” 或 “饥饿”
- 死锁:两个任务分别占用对方需要的资源,且都不释放,互相等待对方放手,导致系统卡死。例如:任务 A 占用打印机,等待扫描仪;任务 B 占用扫描仪,等待打印机,两者永远僵持。
- 饥饿:高优先级任务持续抢占资源,低优先级任务永远得不到执行机会(比如一个后台统计任务始终无法运行)。
没有 PV 操作时,无法合理分配资源使用权,容易出现这类极端问题。
4. 实时系统失去 “实时性”
在实时系统(如工业控制、自动驾驶)中,任务必须在严格时间内响应。
- 若多个任务无序争抢 CPU 或外设,关键任务(如紧急刹车控制)可能被低优先级任务打断,导致响应超时,引发安全事故。
没有 PV 操作时,无法保证高优先级任务的优先执行权,实时性无从谈起。
总结
PV 操作(及信号量机制)是多任务系统的 “交通规则”:
- 没有规则,车辆(任务)会乱抢车道(资源),导致撞车(数据错误)、堵车(死锁)、紧急车辆被堵(实时性失效)。
- 因此,任何支持多任务的系统(如 μC/OS、Linux 内核)都必须有类似 PV 操作的同步互斥机制,否则根本无法稳定运行。
P 和 V 是荷兰语 “Proberen” 和 “Verhogen” 的缩写)。在操作系统和多任务编程中,PV 操作是配合信号量使用的一组原子操作,主要作用是实现任务间的同步与互斥,确保共享资源的安全访问。
具体作用可以用两个生活场景理解:
1. 实现 “互斥”:防止多个任务同时操作共享资源
比如多个任务需要使用同一个打印机(共享资源):
- P 操作:相当于 “申请使用打印机”。如果打印机空闲(信号量 > 0),任务占用它;如果被占用(信号量 = 0),任务就排队等待,不能强行使用。
- V 操作:相当于 “用完打印机归还”。释放打印机后,通知排队的任务 “现在可以使用了”。
通过 PV 操作,能保证同一时间只有一个任务使用打印机,避免打印内容混乱。
2. 实现 “同步”:控制任务的执行顺序
比如任务 A 需要先生产数据,任务 B 才能处理数据:
- 任务 A 生产完数据后,执行V 操作(相当于 “通知 B 可以处理了”)。
- 任务 B 开始前执行P 操作(相当于 “等待 A 的通知”),如果没收到通知(信号量 = 0),就暂停等待,直到 A 通知后再执行。
通过 PV 操作,能确保 B 在 A 完成后才执行,避免 B 因没数据而 “做无用功”。
总结
PV 操作的核心作用是通过信号量的 “申请 - 释放” 机制,协调多个任务的执行节奏:
- 防止多个任务同时操作共享资源(互斥);
- 保证任务按预期顺序执行(同步);
- 避免数据混乱、资源冲突等问题,让多任务系统稳定运行。
这在嵌入式系统(如 μC/OS)、操作系统内核等场景中非常重要。