Linux System V 共享内存 + 信号量互斥踩坑全记录(进程 A 创建、B/C 互斥访问)

Linux System V 共享内存+信号量互斥踩坑全记录(进程A创建、B/C互斥访问)

一、题目需求

编写三段程序:

  1. 进程A:唯一创建共享内存信号量,初始化共享内存字符串,设置信号量初值为1(互斥锁),阻塞等待测试完毕再销毁内核资源;
  2. 进程B、进程C:代码完全一致,只打开已存在的共享内存与信号量,使用信号量实现互斥,读取共享内存内容;
  3. 要求同一时刻只有一个进程能访问临界区(共享内存),避免并发竞争。

二、整体原理铺垫

1. System V IPC 分工规则

  • shmget / semgetIPC_CREAT | IPC_EXCL:不存在则新建,已存在则报错,仅进程A使用;
  • shmget / semget 只传权限0644:仅打开已有资源,不存在直接报错,B/C专用;
  • 信号量初值设为1:二元信号量,实现互斥;
    • sem_locksemop(semid, {0,-1,0}),P操作,占用资源;
    • sem_unlocksemop(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. 终端1执行 ./PA,程序阻塞等待(不要关闭);
  2. 新开终端2执行 ./PB,正常打印内容,自动释放锁;
  3. 新开终端3执行 ./PC,正常打印内容;
  4. 全部测试完成,回到终端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从第二个字符打印,循环叠加输出大量重复文字,还会访问无终止符内存出现菱形乱码方块。

两种修复方案:

  1. 直接打印完整字符串(推荐):printf("%s\n", buf);
  2. 逐个打印字符: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:命令行拼写/语法错误

  1. ipcs-s 报错 command not found:命令和参数中间必须加空格 ipcs -s
  2. ./pc 提示 No such file:Linux文件名区分大小写,编译生成PB必须执行./PB
  3. 缺少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,影响代码规范性。

六、核心知识点总结

  1. 资源权限区分:A带IPC_CREAT新建,B/C只打开;
  2. 互斥核心:二元信号量初值=1,一次P对应一次V;
  3. 信号量不会自动释放锁,参数写错直接永久卡死;
  4. 指针sizeof获取的是地址大小,不是缓冲区长度;
  5. strlen仅适用于带\0的char字符串;
  6. System V IPC内核资源必须手动销毁,可通过ipcs/ipcrm命令管理。

七、拓展思考题

  1. 如果去掉信号量,同时运行B、C会出现什么现象?(并发乱码、数据竞争)
  2. 进程B运行时强制kill杀掉,信号量会自动释放吗?(不会,必须手动清理)
  3. 能否用管道/消息队列替代信号量实现互斥?
posted @ 2026-06-27 10:14  刘梓健  阅读(10)  评论(0)    收藏  举报