多进程/多线程并发编程,C/C++Unix/Linux应用层编程基本原理

System V 信号量与共享内存技术解析

System V 信号量与共享内存技术解析


一、System V 信号量

System V 信号量与共享内存是 Linux 系统中用于进程间通信(IPC)的重要机制。以下是关于信号量创建部分的详细解析:

1. semctl - 信号量的创建

声明

int semget(key_t key, int nsems, int semflg);

参数详解

  1. key
    信号量集的键值,通常通过 ftok 函数生成,用于唯一标识信号量集。可以使用 IPC_PRIVATE 来创建一个新的、唯一的信号量集。
  2. nsems
    信号量集中信号量的数量。如果只是访问已存在的信号量集,可以将其设为 0。
  3. semflg
    标志位,用于指定信号量集的创建和访问权限。常用的标志包括:
    IPC_CREAT:如果信号量集不存在,则创建它。
    IPC_EXCL:与 IPC_CREAT 一起使用,如果信号量集已存在,则返回错误。
    • 权限模式:如 0666,表示所有用户可读写。

返回值
• 成功返回信号量集的标识符,失败返回 -1。

2. semctl - 控制信号量集(通常用于初始化或查看信号量集合状态)

声明

int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);

参数详解

  1. semid
    信号量集的标识符,由 semget 返回。需确保进程对该信号量集有操作权限(如 IPC_RMID 需要所有者或超级用户权限)。
  2. semnum
    信号量在集合中的索引(从 0 开始)。仅对单信号量操作命令(如 SETVALGETVAL)有效;若 cmd 作用于整个集合(如 IPC_RMID),此参数可设为 0
  3. cmd
    控制命令,决定操作类型及 arg 参数用途:
    IPC_STAT:获取信号量集状态信息,arg 需指向 struct semid_ds 结构体。该结构包含内核维护的元数据:

    struct semid_ds {
        struct ipc_perm sem_perm;  // 权限与所有者信息
        time_t sem_otime;         // 最后一次 semop() 成功的时间(初始为0)
        time_t sem_ctime;         // 信号量集创建/修改时间
        struct sem *sem_base;     // 指向信号量数组
        unsigned short sem_nsems; // 信号量数量
    };

    关键字段
    sem_otime:首次创建时初始化为0,仅通过成功执行的 semop() 更新为非零值
    sem_ctime:记录信号量集的创建或配置修改时间
    IPC_SET:修改信号量集权限,需 struct semid_ds 指针更新 sem_perm 字段
    IPC_RMID:立即删除信号量集
    SETVAL/GETVAL:设置或获取单个信号量的值

  4. arg(共用体 union semun
    需用户自定义的联合体,根据 cmd 选择成员:

    union semun {
        int val;                // SETVAL/GETVAL 的值
        struct semid_ds *buf;   // IPC_STAT/IPC_SET 的状态缓冲区
        unsigned short *array;  // GETALL/SETALL 的数组指针
    };

多进程竞争防护策略
在信号量集创建与初始化过程中,需防范以下竞争场景:
进程A调用 semget 创建信号量后未完成初始化,进程B获取同一信号量并修改值,随后被进程A的初始化覆盖

解决方案:利用sem_otime 同步机制只让一个进程创建并初始化信号量集合
创建者进程:初始化后执行一次空操作(sem_op=0)的 semop(),强制更新 sem_otime 为非零值
其他进程:循环检查 sem_otime 是否为0,若为0则等待至其变为非零:

struct semid_ds ds;
union semun arg = {.buf = &ds};
do {
    semctl(semid, 0, IPC_STAT, arg);
    if (ds.sem_otime != 0) break;  // 确认初始化完成
    usleep(1000);  // 避免忙等待
} while (1);
3. semop - 执行原子操作(PV 操作)

声明

int semop(int semid, struct sembuf *sops, size_t nsops);

参数
semid:信号量标识符。
sops:指向 sembuf 结构数组的指针,每个结构定义一次操作:

struct sembuf {
    unsigned short sem_num;  // 信号量索引
    short sem_op;            // 操作值(负数:P 操作;正数:V 操作)
    short sem_flg;           // 标志(如 `IPC_NOWAIT` 非阻塞,`SEM_UNDO` 防死锁)
};

nsops:操作数组的长度。

返回值
• 成功返回 0,失败返回 -1。


二、System V 共享内存

共享内存允许多进程直接读写同一物理内存区域,无需内核介入,是最高效的 IPC 方式。核心系统调用如下:

1. shmget - 创建/获取共享内存段

声明

#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

参数
key:共享内存标识键,规则同信号量。
size:共享内存大小(单位:字节),实际分配按页对齐。
shmflg:标志位,如 IPC_CREATIPC_EXCL 及权限模式。

返回值
• 成功返回共享内存标识符,失败返回 -1。

生命周期管理
共享内存段的生命周期由内核维护,即使所有进程解除映射,仍需显式调用 shmctlIPC_RMID 命令删除。


2. shmat - 映射共享内存到进程地址空间

声明

void *shmat(int shmid, const void *shmaddr, int shmflg);

参数
shmid:共享内存标识符。
shmaddr:建议映射地址(通常设为 NULL,由系统自动分配)。
shmflg:标志位,如 SHM_RDONLY(只读映射)。

返回值
• 成功返回映射后的虚拟地址指针,失败返回 (void*) -1

注意点:
• 共享内存在每个使用它的进程的附加位置(可以理解为挂载点)不同,因此若共享内存中存储了指针,若希望存储(也只能指向)共享内存的另一处位置,该指针不能存储绝对地址,而是只能存储到目标位置的偏移量。


3. shmdt - 解除映射

声明

int shmdt(const void *shmaddr);

参数
shmaddrshmat 返回的地址指针。

返回值
• 成功返回 0,失败返回 -1。


4. shmctl - 控制共享内存段

声明

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数
shmid:共享内存标识符。
cmd:控制命令:
IPC_STAT:获取状态信息到 buf
IPC_SET:修改权限或所有者。
IPC_RMID:标记删除(当所有进程解除映射后实际释放)。
buf:指向 shmid_ds 结构体的指针,用于存储或修改状态。

返回值
• 成功返回 0,失败返回 -1。


三、关键注意事项与优化策略

  1. 同步机制选择
    信号量类型:二元信号量(互斥锁)适用于独占资源访问;多元信号量(如连接池)可控制资源数量。
    性能排序:互斥锁 > POSIX 信号量 > System V 信号量 > 文件锁。
  2. 资源泄漏防护
    • 显式调用 semctl/shmctl 删除 IPC 对象,避免因进程崩溃导致资源残留。
    • 使用 ipcsipcrm 命令检查与清理未释放的资源。
  3. 错误处理与调试
    • 所有系统调用需检查返回值,捕获 EACCES(权限不足)、EINVAL(无效参数)等错误。
    • 通过 semctlGETNCNT/GETZCNT 监控信号量等待队列,优化锁粒度。

四、总结

通过合理组合 System V 信号量与共享内存,可实现高效且安全的进程间协作。信号量的原子操作与共享内存的直接内存访问特性,使其在数据库连接池、实时数据处理等高并发场景中表现卓越。开发者需重点关注多进程初始化的竞争防护、资源生命周期管理及错误处理机制,以确保系统稳定性和性能。

回复

This is just a placeholder img.