Linux System V 共享内存 + 信号量互斥踩坑全记录(进程 A 创建、B/C 互斥访问)
Linux System V 共享内存+信号量互斥踩坑全记录(进程A创建、B/C互斥访问)
一、题目需求
编写三段程序:
- 进程A:唯一创建共享内存、信号量,初始化共享内存字符串,设置信号量初值为1(互斥锁),阻塞等待测试完毕再销毁内核资源;
- 进程B、进程C:代码完全一致,只打开已存在的共享内存与信号量,使用信号量实现互斥,读取共享内存内容;
- 要求同一时刻只有一个进程能访问临界区(共享内存),避免并发竞争。
二、整体原理铺垫
1. System V IPC 分工规则
shmget / semget带IPC_CREAT | IPC_EXCL:不存在则新建,已存在则报错,仅进程A使用;shmget / semget只传权限0644:仅打开已有资源,不存在直接报错,B/C专用;- 信号量初值设为1:二元信号量,实现互斥;
sem_lock:semop(semid, {0,-1,0}),P操作,占用资源;sem_unlock:semop(semid, {0,1,0}),V操作,释放资源;
- 共享内存:
shmat挂载到进程虚拟地址,用完shmdt卸载;内核资源必须手动IPC_RMID销毁。
2. 关键特性(踩坑核心)
System V 信号量不会随进程退出自动释放锁!
如果V操作执行失败,哪怕程序正常退出,信号量数值永远保持0,后续所有进程执行P操作直接永久阻塞卡死。
三、完整可运行代码
进程A(创建者 Pro_A.c)
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#define SHM_PROJ 80
#define SEM_PROJ 20
#define SHM_SIZE 256
// 信号量共用体,用于初始化初值
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
// P操作 上锁
void sem_lock(int semid)
{
struct sembuf op = {0, -1, 0};
semop(semid, &op, 1);
}
// V操作 解锁
void sem_unlock(int semid)
{
struct sembuf op = {0, 1, 0};
semop(semid, &op, 1);
}
int main()
{
key_t shm_key = ftok(".", SHM_PROJ);
key_t sem_key = ftok(".", SEM_PROJ);
// 1. 创建共享内存
int shmid = shmget(shm_key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0644);
if (-1 == shmid)
{
fprintf(stderr, "shmget error,errno:%d,%s\n", errno, strerror(errno));
shmid = shmget(shm_key, SHM_SIZE, 0644);
if (shmid == -1)
{
perror("shmget get failed");
return -1;
}
}
printf("【A】共享内存创建/获取成功\n");
// 2. 创建信号量
int semid = semget(sem_key, 1, IPC_CREAT | IPC_EXCL | 0644);
union semun arg;
arg.val = 1;
if (-1 == semid)
{
fprintf(stderr, "semget error,errno:%d,%s\n", errno, strerror(errno));
semid = semget(sem_key, 1, 0644);
if (semid == -1)
{
perror("semget get failed");
return -1;
}
}
else
{
semctl(semid, 0, SETVAL, arg);
printf("【A】信号量新建,初值设置为1\n");
}
// 3. 挂载共享内存
char *buf = (char *)shmat(shmid, NULL, 0);
if ((void *)-1 == buf)
{
perror("shmat 挂载失败");
return -1;
}
// 上锁初始化临界资源
sem_lock(semid);
strcpy(buf, "共享临界资源");
printf("【A】共享内存初始化完成:%s\n", buf);
sem_unlock(semid);
shmdt(buf);
printf("【A】已卸载本地映射,新开终端运行 ./PB ./PC\n");
printf("测试完成按回车销毁内核资源\n");
getchar();
// 销毁IPC资源
semctl(semid, 0, IPC_RMID);
shmctl(shmid, IPC_RMID, NULL);
printf("【A】资源销毁完毕,程序退出\n");
return 0;
}
进程B / 进程C(使用者 Pro_B.c / Pro_C.c,两份代码完全一样)
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#define SHM_PROJ 80
#define SEM_PROJ 20
#define SHM_SIZE 256
void sem_lock(int semid)
{
struct sembuf op = {0, -1, 0};
semop(semid, &op, 1);
}
void sem_unlock(int semid)
{
struct sembuf op = {0, 1, 0};
semop(semid, &op, 1);
}
int main()
{
key_t shm_key = ftok(".", SHM_PROJ);
key_t sem_key = ftok(".", SEM_PROJ);
int shmid = shmget(shm_key, SHM_SIZE, 0644);
if (-1 == shmid)
{
fprintf(stderr, "shmget error,errno:%d,%s\n", errno, strerror(errno));
return -1;
}
int semid = semget(sem_key, 1, 0644);
if (-1 == semid)
{
fprintf(stderr, "semget error,errno:%d,%s\n", errno, strerror(errno));
return -1;
}
char *buf = (char *)shmat(shmid, NULL, 0);
if ((void *)-1 == buf)
{
perror("shmat 挂载共享内存失败");
return -1;
}
// 临界区:互斥访问
sem_lock(semid);
printf("==== 当前进程读取共享内存 ====\n");
printf("内容:%s\n", buf);
printf("==== 访问结束,释放锁 ====\n");
sem_unlock(semid);
shmdt(buf);
return 0;
}
四、编译&标准运行流程
1. 编译命令
# 编译进程A
gcc Pro_A.c -o PA
# 编译进程B
gcc Pro_B.c -o PB
# 编译进程C
gcc Pro_C.c -o PC
2. 正确运行顺序
- 终端1执行
./PA,程序阻塞等待(不要关闭); - 新开终端2执行
./PB,正常打印内容,自动释放锁; - 新开终端3执行
./PC,正常打印内容; - 全部测试完成,回到终端1按下回车,自动删除共享内存、信号量。
3. 查看/清理残留IPC工具命令
# 查看所有信号量(命令与参数必须有空格)
ipcs -s
# 查看所有共享内存
ipcs -m
# 强制删除卡死的信号量(数字替换为ipcs查到的ID)
ipcrm -s 12345
五、实战踩坑合集(结合你遇到的全部问题)
坑1:解锁函数参数传错,永久死锁(你最核心问题)
错误代码:
sem_unlock(shmid); // shmid是共享内存ID
问题分析:
sem_unlock 内部调用semop,第一个参数必须是信号量ID semid;传入非法ID时,V操作直接失败,信号量数值永远保持0。
B程序运行完毕退出,但锁没有释放,后续运行C、B全部卡在sem_lock。
修复:
sem_unlock(semid);
坑2:循环打印 printf("%s", &buf[i]) 乱码、重复输出
错误写法:
for(int i=0; i<strlen(buf); i++)
{
printf("%s", &buf[i]);
}
原理:%s要求传入完整字符串首地址,自动读到\0停止;
i=0打印全部文字,i=1从第二个字符打印,循环叠加输出大量重复文字,还会访问无终止符内存出现菱形乱码方块。
两种修复方案:
- 直接打印完整字符串(推荐):
printf("%s\n", buf); - 逐个打印字符:
printf("%c", buf[i]);
坑3:sizeof(buf) 遍历共享内存完全失效
char *buf = shmat(...);
for(int i=0; i<sizeof(buf); i++)
原理:buf是指针,64位系统sizeof(buf)=8,只会循环8次,和256字节共享内存大小无关。
- 遍历完整内存:固定写
i < 256; - 只遍历有效文字:
strlen(buf)。
坑4:strlen 不能统计int数组元素
strlen仅识别单字节\0作为结束标记,只能用于char字符串;int数组的数字0占4字节,无法用strlen统计有效元素,栈数组求长度用sizeof(arr)/sizeof(int),指针只能手动存常量。
坑5:命令行拼写/语法错误
ipcs-s报错 command not found:命令和参数中间必须加空格ipcs -s;./pc提示 No such file:Linux文件名区分大小写,编译生成PB必须执行./PB;- 缺少ipcs工具:Ubuntu执行
sudo apt install ipcs安装。
坑6:进程A没有阻塞等待,资源提前销毁
如果A不加getchar(),创建资源后立刻执行IPC_RMID销毁;此时运行B/C,sem_lock直接阻塞。
解决方案:A增加getchar()阻塞,测试完成再手动释放资源。
坑7:信号量仅新建时初始化初值1
int semid = semget(..., IPC_CREAT|IPC_EXCL|0644);
if(semid == -1) {
// 已存在,只获取,不重置值
} else {
semctl(semid,0,SETVAL,arg); // 仅新建赋值1
}
错误写法:每次运行A都执行SETVAL重置为1;若B正在占用锁(semval=0),强行重置会导致多进程同时访问临界区,互斥失效。
坑8:头文件冗余、单词拼写错误
- 重复导入
<sys/ipc.h>、无用头文件<sys/msg.h>、<sys/stat.h>; - 报错打印
shmgeit,正确拼写shmget,影响代码规范性。
六、核心知识点总结
- 资源权限区分:A带
IPC_CREAT新建,B/C只打开; - 互斥核心:二元信号量初值=1,一次P对应一次V;
- 信号量不会自动释放锁,参数写错直接永久卡死;
- 指针
sizeof获取的是地址大小,不是缓冲区长度; strlen仅适用于带\0的char字符串;- System V IPC内核资源必须手动销毁,可通过
ipcs/ipcrm命令管理。
七、拓展思考题
- 如果去掉信号量,同时运行B、C会出现什么现象?(并发乱码、数据竞争)
- 进程B运行时强制
kill杀掉,信号量会自动释放吗?(不会,必须手动清理) - 能否用管道/消息队列替代信号量实现互斥?

浙公网安备 33010602011771号