一、信号量的基本概念

信号量(Semaphore)本质上是一个计数器,它的值表示系统中某种资源的数量。信号量有两种类型:

  • 二进制信号量:取值只有 0 和 1,常用于实现互斥,保证同一时刻只有一个进程能够访问共享资源,类似于一把锁。
  • 计数信号量:取值可以是任意非负整数,用于管理多个相同类型的资源,当有进程获取资源时,信号量的值减 1;当进程释放资源时,信号量的值加 1。

二、信号量的工作原理

信号量的工作原理基于两种操作:等待(P操作)和发送(V操作)。

  • 等待(P操作):如果信号量的值大于零,则给它减1;如果信号量的值为零,则挂起该进程的执行,直到信号量的值变为正数。
  • 发送(V操作):如果有其他进程因等待信号量而被挂起,则唤醒该进程;如果没有进程因等待信号量而挂起,则给信号量加1。

三、信号量的相关函数

在 Linux 系统中,信号量的操作主要通过 sys/sem.h 头文件中定义的函数来实现,常见的函数有:

①semget():创建或获取一个信号量集。

代码语言:javascript

AI代码解释

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
  • key:是一个唯一标识信号量集的键值,可以通过 ftok() 函数生成。
  • nsems:指定信号量集中信号量的数量。
  • semflg:标志位,用于指定创建或获取信号量集的权限和选项。如果是创建新的信号量集,semflg 通常设置为 IPC_CREAT | 0666(0666 表示权限)
  • 返回值:成功时返回信号量集的标识符,失败时返回 -1。

②semop():对信号量集中的信号量进行操作,如 P 操作(等待信号量,资源减 1)和 V 操作(释放信号量,资源加 1)。

代码语言:javascript

AI代码解释

#include <sys/types.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, unsigned nsops);
  • semid:信号量集的标识符,由 semget() 函数返回。
  • sops:指向一个 struct sembuf 结构体数组的指针,struct sembuf 结构体定义了对每个信号量的操作。
  • nsops:表示 sops 数组中元素的数量。
  • 返回值:成功时返回 0,失败时返回 -1。

struct sembuf 结构体的定义如下:

代码语言:javascript

AI代码解释

struct sembuf {
    unsigned short sem_num;  // 信号量集中信号量的编号(从 0 开始)
    short          sem_op;   // 操作值,P 操作为 -1,V 操作为 +1
    short          sem_flg;  // 操作标志,如 IPC_NOWAIT(非阻塞操作)
};

③semctl():用于控制信号量集,如初始化信号量的值、获取信号量的状态等。

代码语言:javascript

AI代码解释

#include <sys/types.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);
  • semid:信号量集的标识符。
  • semnum:信号量集中信号量的编号(从 0 开始)。
  • cmd:指定要执行的操作命令,如 SETVAL(设置信号量的值)、GETVAL(获取信号量的值)等。
  • ...:根据 cmd 的不同,可能需要额外的参数。
  • 返回值:根据 cmd 的不同,返回值有所不同,成功时返回相应的结果,失败时返回 -1。

四、信号量实现互斥的示例代码

代码语言:javascript

AI代码解释

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>

// 定义信号量操作函数
void semaphore_p(int semid) {
    struct sembuf sem_op;
    sem_op.sem_num = 0;  // 信号量编号为 0
    sem_op.sem_op = -1;  // P 操作
    sem_op.sem_flg = 0;
    semop(semid, &sem_op, 1);
}

void semaphore_v(int semid) {
    struct sembuf sem_op;
    sem_op.sem_num = 0;  // 信号量编号为 0
    sem_op.sem_op = 1;   // V 操作
    sem_op.sem_flg = 0;
    semop(semid, &sem_op, 1);
}

int main() {
    key_t key;
    int semid;

    // 生成唯一的键值
    key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok");
        exit(1);
    }

    // 创建信号量集
    semid = semget(key, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        exit(1);
    }

    // 初始化信号量的值为 1(二进制信号量,用于互斥)
    if (semctl(semid, 0, SETVAL, 1) == -1) {
        perror("semctl");
        exit(1);
    }

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(1);
    } else if (pid == 0) {  // 子进程
        semaphore_p(semid);
        printf("Child process: Entering critical section\n");
        sleep(2);  // 模拟在临界区的操作
        printf("Child process: Leaving critical section\n");
        semaphore_v(semid);
    } else {  // 父进程
        semaphore_p(semid);
        printf("Parent process: Entering critical section\n");
        sleep(1);  // 模拟在临界区的操作
        printf("Parent process: Leaving critical section\n");
        semaphore_v(semid);
    }

    // 删除信号量集
    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl");
        exit(1);
    }

    return 0;
}

父子进程通过信号量实现了对临界区的互斥访问。父进程和子进程在进入临界区之前都先执行 P 操作获取信号量,离开临界区时执行 V 操作释放信号量,从而保证同一时刻只有一个进程能够进入临界区。

五、关键使用场景

5.1. 互斥访问共享资源(Mutex)
  • 场景:多个进程/线程需要互斥访问共享硬件资源(如GPIO、SPI总线)或共享内存区域。
  • 实现:使用二进制信号量(初始值为1)。
  • 示例

代码语言:javascript

AI代码解释

sem_t mutex;
sem_init(&mutex, 1, 1); // 初始化为1(跨进程)

// 进程A
sem_wait(&mutex);  // 进入临界区
access_shared_resource();
sem_post(&mutex);  // 退出临界区

// 进程B同理
5.2. 生产者-消费者模型
  • 场景:生产者向缓冲区写入数据,消费者从缓冲区读取数据,需避免缓冲区溢出或读空。
  • 实现:使用两个计数信号量
    • empty:表示空闲缓冲区数量(初始值为缓冲区大小)。
    • full:表示已填充缓冲区数量(初始值为0)。
  • 代码逻辑

代码语言:javascript

AI代码解释

sem_t empty, full;
sem_init(&empty, 1, BUFFER_SIZE); // 初始空闲缓冲区数量
sem_init(&full, 1, 0);           // 初始已填充数量

// 生产者
sem_wait(&empty);  // 等待空闲缓冲区
write_to_buffer();
sem_post(&full);   // 增加已填充计数

// 消费者
sem_wait(&full);   // 等待有数据的缓冲区
read_from_buffer();
sem_post(&empty);  // 释放空闲缓冲区
5.3. 多任务同步(屏障)
  • 场景:多个任务需在某一点同步(如同时启动或结束)。
  • 实现:使用计数信号量跟踪到达同步点的任务数。
  • 示例:等待3个线程完成初始化:

代码语言:javascript

AI代码解释

sem_t sync_sem;
sem_init(&sync_sem, 1, 0);  // 初始值为0

// 每个线程完成初始化后调用
sem_post(&sync_sem);

// 主线程等待所有线程完成
for (int i=0; i<3; i++) {
    sem_wait(&sync_sem);
}
5.4. 有限资源池管理
  • 场景:管理有限资源(如数据库连接池、线程池)。
  • 实现:使用计数信号量表示可用资源数。
  • 代码逻辑

代码语言:javascript

AI代码解释

sem_t pool;
sem_init(&pool, 1, MAX_CONNECTIONS); // 初始为最大连接数

// 申请资源
sem_wait(&pool);  // 资源数-1
use_connection();
sem_post(&pool);  // 资源数+1
5.5. 读写者问题
  • 场景:允许多个读者同时读,但写者需要独占访问。
  • 实现:使用两个信号量
    • rw_mutex:读写互斥(初始值为1)。
    • read_count_mutex:保护读者计数(初始值为1)。
  • 伪代码

代码语言:javascript

AI代码解释

sem_t rw_mutex, read_count_mutex;
int read_count = 0;

// 读者
sem_wait(&read_count_mutex);
read_count++;
if (read_count == 1) sem_wait(&rw_mutex); // 第一个读者锁写
sem_post(&read_count_mutex);

// 读操作...

sem_wait(&read_count_mutex);
read_count--;
if (read_count == 0) sem_post(&rw_mutex); // 最后一个读者解锁写
sem_post(&read_count_mutex);

// 写者
sem_wait(&rw_mutex);
// 写操作...
sem_post(&rw_mutex);

六、注意事项与常见问题

6.1. 避免死锁(Deadlock)
  • 问题:多个信号量操作顺序不当导致循环等待。
  • 解决方案
    • 统一所有进程/线程的锁申请顺序。
    • 使用超时机制(如sem_timedwait)。
    • 避免嵌套锁(如先锁A再锁B,其他线程不要反向操作)。
6.2. 防止资源泄漏
  • 问题:未正确关闭/删除信号量。
  • 规则
    • System V信号量:使用semctl(..., IPC_RMID)显式删除。
    • POSIX命名信号量:所有进程调用sem_close后,至少一个进程调用sem_unlink
    • 未命名信号量sem_destroy销毁前确保无线程在等待。
6.3. 初始值与场景匹配
  • 二进制信号量:初始值应为1(互斥锁)。
  • 计数信号量:初始值应为资源总数(如缓冲区大小)。
6.4. 信号量的持久性
  • 命名信号量:在文件系统(如/dev/shm)中持久化,需手动清理残留。
  • System V信号量:内核中残留需通过ipcrm命令删除。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐