【嵌入式Linux应用开发基础】进程间通信:信号量
父进程和子进程在进入临界区之前都先执行 P 操作获取信号量,离开临界区时执行 V 操作释放信号量,从而保证同一时刻只有一个进程能够进入临界区。:对信号量集中的信号量进行操作,如 P 操作(等待信号量,资源减 1)和 V 操作(释放信号量,资源加 1)。信号量的工作原理基于两种操作:等待(P操作)和发送(V操作)。:用于控制信号量集,如初始化信号量的值、获取信号量的状态等。代码语言:javascri
一、信号量的基本概念
信号量(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销毁前确保无线程在等待。
- System V信号量:使用
6.3. 初始值与场景匹配
- 二进制信号量:初始值应为1(互斥锁)。
- 计数信号量:初始值应为资源总数(如缓冲区大小)。
6.4. 信号量的持久性
- 命名信号量:在文件系统(如
/dev/shm)中持久化,需手动清理残留。 - System V信号量:内核中残留需通过
ipcrm命令删除。
更多推荐



所有评论(0)