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。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐