嵌入式C/C++进阶-进程/线程通信《信号量,一文详解》
目录
1 简介
信号量 是一种操作系统提供的 同步机制(Synchronization Mechanism),用于在多进程或多线程中协调共享资源的访问,防止出现竞争条件(race condition)。
1.1 本质
信号量的本质是一个由操作系统内核维护的原子计数器,通过阻塞与唤醒机制协调多个执行流对共享资源的访问;当资源可用时计数减一,不可用时线程进入等待队列,释放资源时计数加一并唤醒等待线程,从而实现并发同步与互斥控制。
1.2 实现原理
信号量的实现原理可以通俗地理解为:操作系统在内核中维护一个“计数器”(一般俗称S)和一个“排队区”。当线程想要使用某个共享资源时,就去计数器那里“抢票”(P操作)——如果还有票(计数>0),它拿走一张就能进入;如果没票了(计数≤0),它就被操作系统安排到“等待区”睡觉。等其他线程用完资源把票还回来时(V操作),操作系统会从等待区叫醒一个线程继续运行。整个“抢票、睡觉、唤醒”的过程由内核保证是原子的,防止竞争和混乱。
P 操作(也叫 wait 或 down)表示“请求资源”:把信号量计数器减一;如果结果小于 0,说明资源已用完,线程就会被阻塞等待。
V 操作(也叫 signal 或 up)表示“释放资源”:把计数器加一;如果结果小于等于 0,说明有线程在等待,系统会唤醒其中一个继续执行。
1.3 标准
好的,我帮你把 信号量标准对比 梳理成一张紧凑表格:
| 特性 | System V 信号量 | POSIX 信号量 |
|---|---|---|
| 标准来源 | UNIX System V | POSIX.1b(实时扩展) |
| 使用范围 | 进程间通信(IPC) | 线程与进程同步 |
| 创建方式 | 通过 key_t 获取或创建信号量集 |
匿名信号量:sem_init();命名信号量:sem_open() |
| P/V 操作 | semop() |
sem_wait() / sem_post() |
| 控制/删除 | semctl() |
sem_destroy() / sem_unlink() |
| 命名支持 | 不直接支持 | 支持命名信号量 |
| 接口复杂度 | 相对复杂 | 简单直观 |
| 常见场景 | 传统 UNIX IPC,跨进程同步 | 多线程同步,现代进程同步 |
| 适合场景 | 长期存在的资源同步 | 临时或动态资源同步 |
1.4 接口

2 C语言实现
2.1 System V 信号量使用示例
/**
* File: sysv_sem_demo.c
* Build: gcc sysv_sem_demo.c -o sysv_sem_demo
* Run: ./sysv_sem_demo
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
/* System V 的 semctl 第4个参数需要 union semun */
union semun {
int val; // SETVAL
struct semid_ds *buf; // IPC_STAT, IPC_SET
unsigned short *array; // GETALL, SETALL
};
int main(void)
{
key_t key = 1234; // 信号量键值(可使用 ftok() 动态生成)
int semid;
// 1️⃣ 创建或获取信号量集(只有1个信号量)
semid = semget(key, 1, IPC_CREAT | 0666);
if (semid == -1) {
perror("semget");
exit(EXIT_FAILURE);
}
// 2️⃣ 设置信号量初始值为 1
union semun arg;
arg.val = 1;
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl - SETVAL");
exit(EXIT_FAILURE);
}
printf("Semaphore created (semid=%d), initial value=1\n", semid);
// 定义 P / V 操作结构
struct sembuf p = {0, -1, SEM_UNDO}; // P 操作:占用资源
struct sembuf v = {0, 1, SEM_UNDO}; // V 操作:释放资源
// 3️⃣ 执行 P 操作(进入临界区)
printf("Process %d trying to enter critical section...\n", getpid());
if (semop(semid, &p, 1) == -1) {
perror("semop - P");
exit(EXIT_FAILURE);
}
printf("Process %d entered critical section.\n", getpid());
// 临界区:模拟资源使用
sleep(2);
// 4️⃣ 执行 V 操作(离开临界区)
printf("Process %d leaving critical section...\n", getpid());
if (semop(semid, &v, 1) == -1) {
perror("semop - V");
exit(EXIT_FAILURE);
}
printf("Process %d exited critical section.\n", getpid());
// 5️⃣ 删除信号量(清理)
if (semctl(semid, 0, IPC_RMID, arg) == -1) {
perror("semctl - IPC_RMID");
exit(EXIT_FAILURE);
}
printf("Semaphore removed.\n");
return 0;
}
🧩 运行效果示例:
$ gcc sysv_sem_demo.c -o sysv_sem_demo
$ ./sysv_sem_demo
Semaphore created (semid=32768), initial value=1
Process 12563 trying to enter critical section...
Process 12563 entered critical section.
Process 12563 leaving critical section...
Process 12563 exited critical section.
Semaphore removed.
2.2 POSIX 信号量示例
匿名信号量示例
用于线程间同步(或者共享内存的多进程同步)
/**
* posix_sem_anonymous.c
* 构建: gcc posix_sem_anonymous.c -o posix_sem_anonymous -pthread
* 运行: ./posix_sem_anonymous
*
* 使用匿名信号量在两个线程间同步。
*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
sem_t sem; // 匿名信号量
void* thread_func(void* arg) {
for (int i = 0; i < 5; ++i) {
sem_wait(&sem); // P操作(等待信号量)
printf("Child thread: step %d\n", i + 1);
sleep(1);
sem_post(&sem); // V操作(释放信号量)
}
return NULL;
}
int main(void) {
pthread_t tid;
// 初始化信号量: pshared=0 (线程间同步), 初始值=1
if (sem_init(&sem, 0, 1) != 0) {
perror("sem_init");
exit(EXIT_FAILURE);
}
pthread_create(&tid, NULL, thread_func, NULL);
for (int i = 0; i < 5; ++i) {
sem_wait(&sem); // P操作
printf("Main thread: step %d\n", i + 1);
sleep(1);
sem_post(&sem); // V操作
}
pthread_join(tid, NULL);
sem_destroy(&sem); // 销毁信号量
return 0;
}
✅ 输出示例:
Main thread: step 1
Child thread: step 1
Main thread: step 2
Child thread: step 2
...
命名信号量示例
不同进程通过同一个名字(以
/开头)共享信号量。
适合两个不相关的程序同步。
📘 1) sem_create.c(初始化信号量)
/**
* sem_create.c
* Build: gcc sem_create.c -o sem_create -lrt
* Run: ./sem_create
*/
#include <stdio.h>
#include <semaphore.h>
#include <fcntl.h> // O_CREAT, O_EXCL
#include <sys/stat.h> // mode constants
#include <stdlib.h>
#include <errno.h>
int main(void) {
sem_t *sem = sem_open("/demo_sem", O_CREAT | O_EXCL, 0666, 1);
if (sem == SEM_FAILED) {
if (errno == EEXIST) {
printf("Semaphore already exists, open existing one.\n");
sem = sem_open("/demo_sem", 0);
} else {
perror("sem_open");
exit(EXIT_FAILURE);
}
}
printf("Semaphore created or opened successfully.\n");
sem_close(sem);
return 0;
}
📘 2) procA.c(等待资源)
/**
* procA.c
* Build: gcc procA.c -o procA -lrt
* Run: ./procA
*/
#include <stdio.h>
#include <semaphore.h>
#include <fcntl.h>
#include <unistd.h>
int main(void) {
sem_t *sem = sem_open("/demo_sem", 0);
if (sem == SEM_FAILED) {
perror("sem_open");
return 1;
}
for (int i = 0; i < 5; ++i) {
sem_wait(sem);
printf("[procA] got resource, iteration %d\n", i + 1);
sleep(1);
sem_post(sem);
sleep(1);
}
sem_close(sem);
return 0;
}
📘 3) procB.c(也访问同一信号量)
/**
* procB.c
* Build: gcc procB.c -o procB -lrt
* Run: ./procB
*/
#include <stdio.h>
#include <semaphore.h>
#include <fcntl.h>
#include <unistd.h>
int main(void) {
sem_t *sem = sem_open("/demo_sem", 0);
if (sem == SEM_FAILED) {
perror("sem_open");
return 1;
}
for (int i = 0; i < 5; ++i) {
sem_wait(sem);
printf("[procB] got resource, iteration %d\n", i + 1);
sleep(1);
sem_post(sem);
sleep(1);
}
sem_close(sem);
return 0;
}
📘 4) 运行方式:
# 先创建信号量(只需运行一次)
gcc sem_create.c -o sem_create -lrt
./sem_create
# 编译两个进程
gcc procA.c -o procA -lrt
gcc procB.c -o procB -lrt
# 在两个不同终端运行
./procA
./procB
输出将交替出现:
[procA] got resource, iteration 1
[procB] got resource, iteration 1
[procA] got resource, iteration 2
[procB] got resource, iteration 2
...
📘 5) 删除信号量
# 删除命名信号量
gcc -o sem_unlink -x c - <<'EOF'
#include <semaphore.h>
#include <stdio.h>
int main() {
sem_unlink("/demo_sem");
puts("Semaphore /demo_sem removed.");
return 0;
}
EOF
./sem_unlink
3 面试常见问题
-
什么是信号量
信号量是一种用于进程或线程同步的内核机制,本质是一个整型计数器,用于表示可用资源的数量。它可以控制多个进程或线程对共享资源的访问,防止竞争条件。当信号量值大于0表示有可用资源,为0表示资源被占满,小于0时表示有等待的进程或线程。 -
信号量的分类
信号量分为计数信号量和二值信号量两类。计数信号量的值可以大于1,适用于控制资源池数量;二值信号量的取值只能为0或1,功能类似互斥锁。此外按实现方式又分为System V信号量和POSIX信号量。 -
信号量与互斥锁的区别
互斥锁主要用于线程间的互斥访问,只能取0或1,有锁的拥有者概念,必须由加锁者解锁;信号量可用于进程或线程间同步,无拥有者概念,值可以大于1,用于控制资源数量。 -
信号量与条件变量的区别
条件变量用于等待某种条件的发生,不具备计数功能,只是同步手段;信号量具有计数属性,可同时表示资源数量和同步状态。 -
System V信号量的主要接口
System V信号量使用三个主要系统调用:semget创建或获取信号量集,semop执行P/V操作(等待或释放),semctl用于控制信号量(设置初始值、获取状态、删除信号量等)。 -
System V信号量的使用步骤
第一步调用semget创建或打开信号量集;第二步使用semctl初始化信号量值;第三步通过semop执行P操作(等待)或V操作(释放);最后在不再需要时调用semctl删除信号量集。 -
POSIX信号量的主要接口
POSIX信号量提供了更简洁的API,包括sem_init用于初始化,sem_wait执行P操作,sem_post执行V操作,sem_destroy销毁信号量。命名信号量还可使用sem_open、sem_unlink在进程间共享。 -
System V与POSIX信号量的区别
System V信号量基于内核持久化机制,可以在进程退出后依然存在,支持信号量集合;POSIX信号量更轻量,语义简单,分为命名和匿名两类,命名信号量可跨进程使用,匿名信号量用于线程间同步。 -
P操作和V操作的含义
P操作表示请求资源(通常会将信号量值减1),当值小于0时阻塞等待;V操作表示释放资源(将信号量值加1),若有等待进程则唤醒。 -
信号量中的union semun结构体
union semun是semctl函数的第四个参数,用于传递不同类型的数据。其成员包括int val(用于SETVAL设置单个信号量值),struct semid_ds *buf(用于IPC_STAT或IPC_SET获取和修改信号量属性),unsigned short *array(用于GETALL或SETALL操作整个信号量集)。 -
一个信号量集合中可以包含多个信号量
在System V机制下,semget创建的是一个信号量集(数组),每个信号量可独立控制不同的资源。val表示单个信号量的当前计数值,用于表示可用资源数量。 -
信号量在多进程同步中的作用
两个或多个独立进程可以通过共享同一个System V或POSIX命名信号量实现同步。例如一个进程执行sem_wait等待资源,另一个进程执行sem_post释放资源,从而保证执行顺序。 -
信号量与共享内存、消息队列的区别
共享内存用于数据共享,消息队列用于数据传递,而信号量主要用于同步和互斥控制。通常信号量会与共享内存配合使用,保证访问时的互斥安全。 -
信号量使用中的常见错误
常见错误包括未初始化信号量、对已经销毁的信号量操作、未正确删除System V信号量(导致资源泄露)、信号量初始值设置错误或未匹配的P/V操作导致死锁。 -
信号量的典型应用场景
用于生产者消费者模型、资源池管理、进程同步、共享内存保护、临界区访问控制等场景。 -
二值信号量与互斥锁的优劣
二值信号量在跨进程同步方面比互斥锁更灵活,但无法记录锁拥有者,因此在异常退出时可能导致信号量永远占用;互斥锁能自动处理线程退出后的释放。 -
POSIX命名信号量的使用示例
使用sem_open创建命名信号量,sem_wait和sem_post进行同步,多个进程通过相同名称访问该信号量;使用sem_unlink删除。 -
信号量值的初始意义
信号量初始值通常表示可用资源数,例如初始化为1表示一次只能有一个进程进入临界区。 -
如何查看System V信号量
可使用ipcs命令查看当前系统中的System V信号量资源,使用ipcrm命令删除。 -
信号量的优缺点
优点是机制简单、内核级同步、效率较高;缺点是操作复杂、调试困难、误用容易导致死锁,System V信号量还可能导致内核残留资源。
更多推荐



所有评论(0)