Linux同步机制之信号量

System V 信号量

概述

System V 信号量是内核提供的同步原语, 用于跨进程的互斥与资源计数。信号量本质上是一个计数器, 支持 P 操作 (wait, 减 1) 和 V 操作 (signal, 加 1)。信号量通常用于控制对共享资源的访问, 实现进程间的同步。

典型用途:

  • 互斥锁: 初值 1, 保证同一时刻只有一个进程进入临界区
  • 计数信号量: 初值为可用资源数, 控制并发访问额度
  • 进程间同步: 作为事件/条件的等待与通知

信号量由内核维护, 通过键值 key 标识, 支持集合 (多个 sem 组成的数组)。

通信原理

基本概念

信号量的特点:

  1. 计数器: 本质上是一个非负整数计数器
  2. 原子操作: P 和 V 操作是原子的, 不会被中断
  3. 阻塞机制: 当信号量为 0 时, P 操作会阻塞
  4. 同步原语: 用于实现互斥和同步
  5. 集合概念: System V 信号量以集合形式存在

实现机制

  1. 创建/获取信号量集:

    • 使用 semget() 系统调用创建或获取信号量集
    • 通过唯一的键值 (key) 标识信号量集
    • 指定信号量集中信号量的数量
    • 返回信号量集标识符 (semid)
  2. 信号量操作:

    • 使用 semop() 对信号量进行 P/V 操作
    • P 操作 (wait): 信号量减 1, 如果为 0 则阻塞
    • V 操作 (signal): 信号量加 1, 唤醒等待的进程
  3. 信号量控制:

    • 使用 semctl() 设置信号量的初始值
    • 可以获取信号量的值
    • 可以删除信号量集
  4. 同步模式:

    复制代码
    进程A: P操作(等待)  ──┐
                           ├──>  [信号量]  ──>  控制共享资源访问
    进程B: P操作(等待)  ──┘
    
    进程C: V操作(释放)  ──>  信号量+1,唤醒等待进程
  5. 互斥模式:

    • 信号量初始值为 1
    • 进程访问共享资源前执行 P 操作
    • 访问完成后执行 V 操作
    • 保证同一时刻只有一个进程访问资源

使用流程

  1. semget 创建/获取信号量集 (指定 key、数量、权限, 可带 IPC_CREAT/IPC_EXCL)
  2. semctl 初始化值 (SETVAL/SETALL), 可查询/删除 (GETVAL/IPC_RMID)
  3. semop 执行 P/V 操作 (原子减/加), 支持阻塞或非阻塞 (SEM_UNDO/IPC_NOWAIT)
  4. 使用完后可由负责的进程调用 semctl(..., IPC_RMID, ...) 删除信号量集

API 说明

semget()

c 复制代码
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
  • 功能: 创建或获取信号量集
  • 参数 :
    • key: 信号量集的键值 (可以用 ftok() 生成或使用 IPC_PRIVATE)
    • nsems: 信号量集中信号量的数量
    • semflg: 标志位 (IPC_CREAT, IPC_EXCL, 权限等)
  • 返回值: 成功返回信号量集标识符, 失败返回 -1
  • 常见错误: EACCES, EEXIST, ENOENT, ENOMEM, ENOSPC

semop()

c 复制代码
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);
  • 功能: 原子执行一组 P/V 操作
  • 参数 :
    • semid: 信号量集标识符
    • sops: 操作数组指针
    • nsops: 操作数量
  • 返回值: 成功返回 0, 失败返回 -1
  • 常见错误: EACCES, EIDRM, EINVAL, EAGAIN (非阻塞且资源不足), EINTR (被信号中断)

sembuf 结构

c 复制代码
struct sembuf {
    unsigned short sem_num;  // 信号量索引/编号
    short sem_op;            // 操作值: 负数=等待并减 (P), 正数=加 (V), 0=等待为 0
    short sem_flg;           // 标志位: SEM_UNDO (进程退出自动回滚), IPC_NOWAIT (非阻塞)
};

semctl()

c 复制代码
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);
  • 功能: 控制/查询信号量
  • 参数 :
    • semid: 信号量集标识符
    • semnum: 信号量编号
    • cmd: 命令 (SETVAL 设置值, GETVAL 获取值, IPC_RMID 删除等)
    • ...: 可变参数 (根据 cmd 不同而不同, 需要 union semun 作为第四参数)
  • 常用 cmd :
    • SETVAL/GETVAL: 设定/获取单个信号量值
    • SETALL/GETALL: 设定/获取信号量数组
    • IPC_RMID: 删除信号量集
    • IPC_STAT/IPC_SET: 查询/设置权限与属性
  • 返回值: 根据 cmd 不同而不同, 失败返回 -1
  • 注意: 需要 union semun 作为第四参数 (用户需自行声明)

示例代码

示例 1: 互斥锁风格的 P/V

c 复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

// P 操作: 获取锁 (阻塞等待)
int sem_lock(int semid, int idx) {
    struct sembuf op = { .sem_num = idx, .sem_op = -1, .sem_flg = SEM_UNDO };
    return semop(semid, &op, 1);
}

// V 操作: 释放锁
int sem_unlock(int semid, int idx) {
    struct sembuf op = { .sem_num = idx, .sem_op = 1, .sem_flg = SEM_UNDO };
    return semop(semid, &op, 1);
}

int main(void) {
    key_t key = ftok(".", 's');
    int semid = semget(key, 1, IPC_CREAT | 0666);
    if (semid == -1) { 
        perror("semget"); 
        return 1; 
    }

    // 初始化为 1 (互斥锁)
    union semun arg; 
    arg.val = 1;
    if (semctl(semid, 0, SETVAL, arg) == -1) { 
        perror("semctl"); 
        return 1; 
    }

    if (sem_lock(semid, 0) == -1) { 
        perror("lock"); 
        return 1; 
    }
    printf("PID %d in critical section\n", getpid());
    sleep(1); // 模拟工作
    sem_unlock(semid, 0);

    // 由负责的进程删除信号量 (可选)
    semctl(semid, 0, IPC_RMID);
    return 0;
}

说明:

  • 互斥锁语义: 初值设为 1, P=-1 获取, V=+1 释放
  • 阻塞行为: 未持有锁时 P 会睡眠; 使用 IPC_NOWAIT 可改为非阻塞
  • SEM_UNDO: 进程异常退出时内核回滚 sem 值, 降低死锁风险

示例 2: 计数信号量 (资源池)

c 复制代码
// 初始化为资源数 N; P 获取一个资源, V 归还
// 将 SETVAL 初值设为可用资源总数 (例如连接数、缓冲块数)
// 当 sem_op=-1 时, 若值为 0 则阻塞或 EAGAIN (IPC_NOWAIT)

参考 examples/05-semaphore/ 目录下的更多示例代码。

性能评价

优点

  1. 同步机制: 提供强大的进程同步能力
  2. 原子操作: P/V 操作是原子的, 保证正确性
  3. 阻塞机制: 自动处理进程阻塞和唤醒
  4. 灵活性: 可以用于互斥、同步、资源计数等多种场景
  5. 持久性: 信号量集在系统中持久存在

缺点

  1. 复杂性: API 相对复杂, 需要理解信号量概念
  2. 系统限制: 系统对信号量数量有限制
  3. 死锁风险: 使用不当可能导致死锁
  4. 系统资源: 占用系统资源, 需要显式删除
  5. 性能开销: 需要系统调用, 有一定开销

性能特点

  • 延迟: 中等 (需要系统调用)
  • 吞吐量: 中等 (受系统限制影响)
  • CPU 占用: 中等
  • 内存占用: 小 (每个信号量占用少量内存)

适用场景

  • ✅ 进程同步和互斥
  • ✅ 控制共享资源访问
  • ✅ 生产者-消费者问题
  • ✅ 读者-写者问题
  • ✅ 需要复杂同步机制的场景
  • ❌ 简单的单向通信 (管道更合适)
  • ❌ 不需要同步的场景

注意事项

  1. 键值管理 : 使用 ftok() 生成键值或使用 IPC_PRIVATE; key 相同会共享同一信号量集, 部署时需管理好 key 分配
  2. 初始值设置 : 使用 semctl(SETVAL) 设置信号量初始值
  3. 资源清理 : 使用完毕后应该删除信号量集 (semctl(IPC_RMID)); 约定哪个进程初始化/删除信号量, 避免误删
  4. 死锁避免: 注意避免死锁, 合理设计 P/V 操作顺序; 保持一致的加锁顺序, 避免循环等待; 对长时间持锁的操作谨慎设计
  5. 错误处理: 注意处理信号量操作失败的情况
  6. 系统限制 : 注意系统的信号量数量限制 (ipcs -l 查看)
  7. SEM_UNDO 标志: 建议在 P/V 中使用, 防止进程崩溃后信号量泄漏, 降低死锁风险; 但大量使用可能有内核开销
  8. 权限与安全: 合理设置权限位, 避免被非预期进程操作
  9. 非阻塞模式: IPC_NOWAIT 可避免阻塞, 需处理 EAGAIN 重试或回退
  10. 删除时机: 删除会使阻塞的 semop 返回 EIDRM; 先确保使用方已退出或能正确处理
  11. 多 sem 场景 : semop 支持一次操作多个信号量, 可用于原子更新多个资源状态

常见错误码提示

  • EACCES: 权限不足
  • EEXIST: IPC_CREAT|IPC_EXCL 且已存在
  • ENOENT: 未指定 IPC_CREAT 且不存在
  • ENOMEM/ENOSPC: 资源不足或达到系统上限
  • EAGAIN: 非阻塞且当前不可用
  • EIDRM: 信号量集已被删除
  • EINTR: 被信号中断, 可按需重试
  • EINVAL: 参数无效

扩展阅读

  • man 2 semget
  • man 2 semop
  • man 2 semctl
  • man 7 sem_overview
相关推荐
dbitc6 小时前
WIN11把WSL2移动安装目录
linux·运维·ubuntu·wsl
嵌入式学习菌6 小时前
SPIFFS文件系统
服务器·物联网
旺仔Sec6 小时前
2026年度河北省职业院校技能竞赛“Web技术”(高职组)赛项竞赛任务
运维·服务器·前端
BullSmall6 小时前
linux 根据端口查看进程
linux·运维·服务器
herinspace6 小时前
管家婆软件年结存后快马商城操作注意事项
服务器·数据库·windows
嘻哈baby7 小时前
DDNS动态域名解析方案对比与实战配置
网络
_F_y7 小时前
Linux:进程间通信
linux
weixin_462446237 小时前
Kali/ubuntu Linux 中彻底删除 Cursor 编辑器(含 dpkg 非空目录警告解决)
linux·ubuntu·cursor
Hard but lovely7 小时前
linux:----进程守护化(Daemon)&&会话的原理
linux·运维·服务器