Linux 内核 semaphore(信号量)机制详解
Linux信号量机制是操作系统实现并发控制的重要工具,主要用于资源计数、进程同步和互斥访问。内核通过struct semaphore结构体实现计数信号量,包含计数器count、自旋锁lock和等待队列wait_list。核心操作包括down(获取资源)和up(释放资源),支持阻塞、中断、超时等多种获取方式。信号量适用于生产者-消费者模型、资源池管理等场景,但与互斥锁相比性能较低且无持有者概念。使用
1. 背景与意义
1.1 并发与同步的挑战
在多任务操作系统中,多个进程或线程可能会并发访问共享资源(如内存、设备、数据结构等)。如果没有合适的同步机制,容易导致数据竞争、死锁、资源泄漏等严重问题。为此,操作系统提供了多种同步原语,信号量(semaphore)就是其中一种经典且强大的机制。
1.2 信号量的作用
信号量是一种用于控制多个进程/线程对共享资源访问的同步工具。它可以用来实现互斥(mutex)、资源计数、生产者-消费者等多种同步场景。Linux 内核实现了两种信号量:
-
二值信号量(binary semaphore):类似互斥锁,只能取 0 或 1。
-
计数信号量(counting semaphore):可取任意非负整数,表示可用资源的数量。
Linux 内核的 struct semaphore
实现的是计数信号量。
2. semaphore 的核心原理
2.1 计数信号量的基本思想
-
信号量维护一个整数计数值
count
,表示可用资源的数量。 -
P 操作(down/acquire):当进程需要访问资源时,尝试将信号量减 1。如果
count > 0
,则立即获得资源;如果count == 0
,则进程进入睡眠,等待资源可用。 -
V 操作(up/release):当进程释放资源时,将信号量加 1。如果有等待进程,则唤醒其中一个。
2.2 semaphore 的数据结构
struct semaphore {
raw_spinlock_t lock; // 自旋锁,保护信号量的并发访问
unsigned int count; // 当前可用资源数
struct list_head wait_list; // 等待队列,挂载所有等待该信号量的进程
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
#ifdef CONFIG_DETECT_HUNG_TASK_BLOCKER
unsigned long last_holder;
#endif
};
-
lock:保护信号量的所有操作,保证多核/多线程安全。
-
count:信号量的核心计数器。
-
wait_list:等待队列,存放所有因资源不足而睡眠的进程。
3. semaphore 的内核实现机制
3.1 down/acquire 操作
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_killable(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
int down_timeout(struct semaphore *sem, long timeout);
-
down 尝试获取信号量。如果
count > 0
,立即获得资源并返回;否则将当前进程加入等待队列并睡眠,直到资源可用。不能被信号中断。 -
down_interruptible类似于 down,但睡眠期间可被信号中断。如果被信号唤醒,返回 -EINTR,否则返回 0。
-
down_killable只响应致命信号(如 SIGKILL),被唤醒时返回 -EINTR,否则返回 0。
-
down_trylock尝试获取信号量,如果
count > 0
,立即获得资源并返回 0;否则立即返回 1,不会睡眠。 -
down_timeout尝试获取信号量,如果资源不可用则睡眠,超时后返回 -ETIME。
3.2 up/release 操作
void up(struct semaphore *sem);
- 释放信号量,将
count
加 1。如果有等待进程,则唤醒其中一个。
3.3 内核实现细节
-
所有操作都通过自旋锁保护,保证并发安全。
-
等待队列采用链表实现,支持多个进程同时等待。
-
睡眠采用
schedule_timeout
,支持超时和信号中断。 -
唤醒采用
wake_q
机制,支持批量唤醒和优化。
4. semaphore 的用法
4.1 初始化
4.1.1 初始化
//静态初始化
DEFINE_SEMAPHORE(my_sem); // 初始值为 1
//动态初始化
struct semaphore my_sem;
sema_init(&my_sem, 3); // 初始值为 3
4.2 获取信号量
down(&my_sem); // 获取信号量,不可中断
down_interruptible(&my_sem); // 可中断
down_killable(&my_sem); // 仅响应致命信号
down_trylock(&my_sem); // 尝试获取,不等待
down_timeout(&my_sem, 5*HZ); // 最多等待 5 秒
4.3 释放信号量
up(&my_sem); // 释放信号量
4.4 典型用法场景
4.4.1 互斥访问(类似 mutex)
DEFINE_SEMAPHORE(lock);
void critical_section(void)
{
down(&lock);
// 临界区
up(&lock);
}
4.4.2 资源计数(如连接池、缓冲区)
struct semaphore buf_sem;
sema_init(&buf_sem, N_BUFFERS);
void producer(void)
{
down(&buf_sem); // 等待有空闲缓冲区
// 生产数据
up(&buf_sem); // 释放缓冲区
}
4.4.3 生产者-消费者模型
struct semaphore empty, full;
sema_init(&empty, N); // 空缓冲区数
sema_init(&full, 0); // 满缓冲区数
void producer(void)
{
down(&empty);
// 生产数据
up(&full);
}
void consumer(void)
{
down(&full);
// 消费数据
up(&empty);
}
4.4.4 设备驱动同步
-
控制对硬件资源的访问,防止并发冲突。
-
等待硬件事件完成(如 DMA 传输、I/O 完成等)。
5. semaphore 的实现源码分析
5.1 down 实现
void down(struct semaphore *sem)
{
unsigned long flags;
might_sleep();
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
__sem_acquire(sem);
else
__down(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
-
如果
count > 0
,直接减 1 并返回。 -
否则调用
__down
,将当前进程加入等待队列并睡眠。
5.2 up 实现
void up(struct semaphore *sem)
{
unsigned long flags;
DEFINE_WAKE_Q(wake_q);
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(list_empty(&sem->wait_list)))
sem->count++;
else
__up(sem, &wake_q);
raw_spin_unlock_irqrestore(&sem->lock, flags);
if (!wake_q_empty(&wake_q))
wake_up_q(&wake_q);
}
-
如果没有等待进程,直接加 1。
-
否则唤醒等待队列中的一个进程。
5.3 等待队列机制
-
每个等待的进程用
struct semaphore_waiter
挂在wait_list
上。 -
睡眠时设置进程状态为
TASK_UNINTERRUPTIBLE
或TASK_INTERRUPTIBLE
。 -
唤醒时将进程状态设置为
TASK_RUNNING
并从等待队列移除。
6. semaphore 与 mutex 的区别
特性 | semaphore(信号量) | mutex(互斥锁) |
---|---|---|
资源计数 | 支持(可>1) | 仅支持1 |
拥有者 | 无 | 有(只能由持有者释放) |
可用场景 | 互斥、计数、同步 | 仅互斥 |
可用上下文 | 进程、软中断、硬中断 | 仅进程上下文 |
死锁检测 | 无 | 有 |
性能 | 较低 | 较高 |
推荐用途 | 资源池、生产者-消费者 | 临界区互斥 |
- mutex 更适合互斥锁场景,semaphore 更适合资源计数和同步。
- mutex 只能由持有者释放,semaphore 可由任意线程释放。
7. 总结
-
semaphore 是 Linux 内核中经典的同步原语,适合资源计数、生产者-消费者、跨上下文同步等场景。
-
down/up 操作通过自旋锁和等待队列实现高效的并发控制。
-
推荐互斥锁场景优先使用 mutex,信号量用于资源池、同步等更广泛场景。
-
正确初始化、合理使用 down/up 及其变体,能有效提升系统的健壮性和性能。
7.1 推荐用法
-
互斥锁场景优先使用 mutex,只有需要计数或跨上下文同步时才用 semaphore。
-
初始化信号量时,确保初始值合理,避免死锁或资源泄漏。
-
使用 down_interruptible/down_killable 支持信号中断,提升系统响应性。
-
up() 可在任意上下文调用,适合中断处理程序唤醒等待进程。
7.2 注意事项
-
不要在持有信号量时睡眠或阻塞,避免死锁。
-
down() 可能导致进程睡眠,不能在原子上下文(如中断处理、spinlock 内)调用。
-
down_trylock() 可在中断上下文使用,但要注意返回值与 mutex_trylock 相反。
-
信号量的释放顺序不受持有者限制,需谨慎设计同步逻辑。
7.3 性能优化
-
避免频繁的 up/down 操作,减少锁竞争。
-
对于高性能互斥场景,优先考虑 mutex 或 spinlock。
更多推荐
所有评论(0)