首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【嵌入式Linux应用开发基础】进程间通信(5):信号量

【嵌入式Linux应用开发基础】进程间通信(5):信号量

作者头像
byte轻骑兵
发布2026-01-21 15:39:06
发布2026-01-21 15:39:06
2020
举报

在嵌入式 Linux 应用开发中,信号量是一种常用的进程间通信(IPC)机制,用于实现进程之间的同步和互斥。

一、信号量的基本概念

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

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

二、信号量的工作原理

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

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

三、信号量的相关函数

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

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

代码语言:javascript
复制
#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
复制
#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
复制
struct sembuf {
    unsigned short sem_num;  // 信号量集中信号量的编号(从 0 开始)
    short          sem_op;   // 操作值,P 操作为 -1,V 操作为 +1
    short          sem_flg;  // 操作标志,如 IPC_NOWAIT(非阻塞操作)
};

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

代码语言:javascript
复制
#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
复制
#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
复制
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
复制
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
复制
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
复制
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
复制
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命令删除。

6.5. 错误处理

  • 关键操作必须检查返回值:
代码语言:javascript
复制
if (sem_wait(sem) == -1) {
    perror("sem_wait failed");
    // 处理错误(如超时或信号中断)
}

6.6. 性能优化

  • 轻量级场景:优先使用POSIX未命名信号量(线程间)或命名信号量(进程间)。
  • 复杂同步:System V信号量支持原子操作多个信号量(semop)。

6.7. 信号量与中断上下文

  • 限制:在Linux内核中,信号量不可用于中断上下文(需使用自旋锁)。
  • 用户态:进程间信号量无此限制。

七、总结

信号量是嵌入式Linux中解决并发问题的核心工具,正确使用需遵循以下原则:

①场景驱动选择

  • 互斥锁 → 二进制信号量。
  • 资源计数 → 计数信号量。
  • 跨进程同步 → 命名信号量或System V信号量。

②资源管理铁律

  • 谁创建,谁销毁。
  • 成对操作(sem_wait/sem_post)。

③嵌入式系统特殊考量

  • 避免动态内存分配(优先静态初始化)。
  • 确保实时性(避免长时间阻塞)。

通过结合其他IPC机制(如共享内存+信号量),可构建高效可靠的多任务系统。

八、参考资料

  • 《UNIX 环境网络编程 - 进程间通信》:“Unix 编程三件套” 之一,经典的进程间通信参考书籍,对信号量的原理、机制和使用有深入讲解。
  • 《嵌入式 Linux 应用开发详解》:偏向实践,会涉及如何使用 Linux 内核提供的 API 进行进程间通信,包括信号量的使用,书中详细讲解相关开发流程,并提供大量代码示例,适合有一定基础想进行实践操作的读者。
  • 《深入理解 Linux 内核》:内容全面,涵盖 Linux 内核的各个方面,理解内核的整体机制对于深入理解信号量在嵌入式 Linux 中的工作原理和应用场景非常有帮助,适合有一定基础且想深入研究的读者。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-02-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、信号量的基本概念
  • 二、信号量的工作原理
  • 三、信号量的相关函数
  • 四、信号量实现互斥的示例代码
  • 五、关键使用场景
    • 5.1. 互斥访问共享资源(Mutex)
    • 5.2. 生产者-消费者模型
    • 5.3. 多任务同步(屏障)
    • 5.4. 有限资源池管理
    • 5.5. 读写者问题
  • 六、注意事项与常见问题
    • 6.1. 避免死锁(Deadlock)
    • 6.2. 防止资源泄漏
    • 6.3. 初始值与场景匹配
    • 6.4. 信号量的持久性
    • 6.5. 错误处理
    • 6.6. 性能优化
    • 6.7. 信号量与中断上下文
  • 七、总结
  • 八、参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档