Linux中信号量semaphore的实现

一、Linux信号量semaphore的实现

信号量(semaphore)是实现同步的重要机制之一,它允许进程在获取不到资源时进入睡眠等待

1.信号量结构体定义

首先,我们来看看信号量的结构体定义:

arduino 复制代码
/* 源码位置:include/asm/semaphore.h */
struct semaphore {
        atomic_t count; // 信号量的计数器
        int sleepers;
        wait_queue_head_t wait;
};

信号量使用 两个计数器 来管理争用:

  • count

    • 表示 可用资源数(初始值为正数,表示可用资源数量)
    • 当进程尝试获取信号量(down())时,count 减 1;释放信号量(up())时,count 加 1
    • 如果 count < 0,说明有进程在等待资源(即发生争用)
  • sleeping

    • 表示 当前正在等待(睡眠)的进程数
    • count 减到负数时,sleeping 会递增,记录等待队列的长度

下面我们来逐一分析

1.1.atomic_t定义

arduino 复制代码
/* 源码位置:include/asm/atomic.h */
typedef struct { volatile int counter; } atomic_t;

typedef struct { ... } atomic_t;

  • typedef:用于创建类型别名
  • atomic_t:新类型的名称

volatile int counter;

  • int counter:一个普通的 32 位整型变量,用于存储实际的值

  • volatile:关键字,告诉编译器:

    • 不要优化对此变量的访问(不要缓存到寄存器)
    • 每次访问都必须直接从内存读取
    • 每次修改都必须立即写回内存

atomic_t 本身只是容器,真正的原子性由专门的函数保证

1.2.wait_queue_head_t定义

arduino 复制代码
/* 源码位置:include/linux/list.h */
struct list_head {
        struct list_head *next, *prev;
};
arduino 复制代码
/* 源码位置:include/linux/wait.h */
struct __wait_queue_head {
        spinlock_t lock;
        struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

list_head

  • 这是linux内核的双向循环链表的表头

__wait_queue_head

  • 这个结构体在表头的基础上增加了spinlock_t
  • 确保在竞争环境下安全操作等待队列

2.信号量相关函数及实现

Linux 2.6.10 中与信号量操作相关的主要函数如下:

2.1.void down(struct semaphore *sem)

swift 复制代码
#define LOCK_SECTION_START(extra)               \
        ".subsection 1\n\t"                     \
        extra                                   \
        ".ifndef " LOCK_SECTION_NAME "\n\t"     \
        LOCK_SECTION_NAME ":\n\t"               \
        ".endif\n"
​
#define LOCK_SECTION_END                        \
        ".previous\n\t"
​
 #define LOCK "lock ; "
#define LOCK_SECTION_NAME                       \
        ".text.lock." __stringify(KBUILD_BASENAME)
static inline void down(struct semaphore * sem)
{
        might_sleep();
        __asm__ __volatile__(
                "# atomic down operation\n\t"
                LOCK "decl %0\n\t"     /* --sem->count */
                "js 2f\n"
                "1:\n"
                LOCK_SECTION_START("")
                "2:\tlea %0,%%eax\n\t"
                "call __down_failed\n\t"
                "jmp 1b\n"
                LOCK_SECTION_END
                :"=m" (sem->count)
                :
                :"memory","ax");
}
2.1.1.宏定义分析
swift 复制代码
#define LOCK_SECTION_START(extra) \
    ".subsection 1\n\t"           \  // 切换到特殊子段
    extra                         \  // 额外的汇编代码
    ".ifndef " LOCK_SECTION_NAME "\n\t" \  // 如果标签未定义
    LOCK_SECTION_NAME ":\n\t"     \  // 定义标签
    ".endif\n"
​
#define LOCK_SECTION_END \
    ".previous\n\t"               // 切换回之前的段

这些宏用于将锁相关的汇编代码放在专门的lock节中,便于内核进行统计分析

2.1.2.down函数详细解析
swift 复制代码
static inline void down(struct semaphore * sem)
{
    might_sleep();  // 检查当前上下文能否睡眠,不能的话打印报错日志(用于调试/检测)
    
    __asm__ __volatile__(
        "# atomic down operation\n\t"
        LOCK "decl %0\n\t"     /* --sem->count */
        "js 2f\n"              // 如果结果为负,跳转到标签2
        "1:\n"                 // 标签1:正常返回点
        
        LOCK_SECTION_START("")
        "2:\tlea %0,%%eax\n\t" // 标签2:信号量不可用
        "call __down_failed\n\t" // 调用失败处理函数
        "jmp 1b\n"             // 处理完成后跳回标签1
        LOCK_SECTION_END
        
        :"=m" (sem->count)     // 输出操作数:内存中的sem->count
        :                      // 无输入操作数
        :"memory","ax");       // 破坏描述符:内存和ax寄存器
}

情况1:成功获取信号量

  1. LOCK decl %0 - 原子性地减少信号量计数,%0就是第一个操作数,即sem->count
  2. 如果结果 ≥ 0,继续执行(不跳转)
  3. js指令是判断当前符号标志位是否为1,为1则跳转,如果上一步操作结果是负数则符号标志位置1
  4. 到达标签1,后续没有指令,函数正常返回

情况2:信号量不可用

  1. LOCK decl %0 - 原子性地减少信号量计数
  2. 如果结果 < 0,跳转到标签2
  3. 将信号量地址加载到eax寄存器
  4. 调用__down_failed函数(通常会阻塞当前进程)
  5. 当信号量可用时,__down_failed返回,跳回标签1
2.1.3.__down_failed实现
swift 复制代码
/* 源码位置:arch/i386/kernel/semaphore.c */
asm(
".section .sched.text\n"
".align 4\n"
".globl __down_failed\n"
"__down_failed:\n\t"
#if defined(CONFIG_FRAME_POINTER)
    "pushl %ebp\n\t"
    "movl  %esp,%ebp\n\t"
#endif
    "pushl %edx\n\t"
    "pushl %ecx\n\t"
    "call __down\n\t"
    "popl %ecx\n\t"
    "popl %edx\n\t"
#if defined(CONFIG_FRAME_POINTER)
    "movl %ebp,%esp\n\t"
    "popl %ebp\n\t"
#endif
    "ret"
);

这段代码主要是调用__down函数

段定义

swift 复制代码
".section .sched.text\n"
  • section: 定义代码段的指令
  • .sched.text: 段名称,专门用于调度的相关代码

对齐指令

swift 复制代码
".align 4\n"
  • 确保后续代码或数据在内存中的起始地址是 4 的倍数(即按 4 字节对齐)

全局声明

swift 复制代码
".globl __down_failed\n"
  • globl (或 global): 声明符号为全局可见
  • __down_failed: 函数名,其他文件可以调用

函数标签

swift 复制代码
"__down_failed:\n\t"
  • __down_failed: 函数入口点标签

函数体 - 条件编译部分

swift 复制代码
#if defined(CONFIG_FRAME_POINTER)
	"pushl %ebp\n\t"
	"movl  %esp,%ebp\n\t"
#endif

当启用帧指针时

  • pushl %ebp\n\t

    • 保存调用者的ebp值到栈中
  • movl %esp,%ebp\n\t

    • 建立新的栈帧:ebp指向当前栈帧底部,栈向下增长
    • 支持调试和栈回溯

函数体 - 寄存器保存

swift 复制代码
	"pushl %edx\n\t"
	"pushl %ecx\n\t"
  • 保存edxecx等寄存器

函数调用

swift 复制代码
	"call __down\n\t"
  • __down: 实际的C函数,处理信号量等待逻辑

函数体 - 寄存器恢复

swift 复制代码
	"popl %ecx\n\t"
	"popl %edx\n\t"
  • 恢复调用前的寄存器值

函数体 - 帧指针恢复

swift 复制代码
#if defined(CONFIG_FRAME_POINTER)
	"movl %ebp,%esp\n\t"
	"popl %ebp\n\t"
#endif

返回指令

arduino 复制代码
	"ret"
  • ret: 返回指令,从栈中弹出返回地址并跳转

2.2.down_interruptible

swift 复制代码
static inline int down_interruptible(struct semaphore * sem)
{
        int result;

        might_sleep();
        __asm__ __volatile__(
                "# atomic interruptible down operation\n\t"
                LOCK "decl %1\n\t"     /* --sem->count */
                "js 2f\n\t"
                "xorl %0,%0\n"
                "1:\n"
                LOCK_SECTION_START("")
                "2:\tlea %1,%%eax\n\t"
                "call __down_failed_interruptible\n\t"
                "jmp 1b\n"
                LOCK_SECTION_END
                :"=a" (result), "=m" (sem->count)
                :
                :"memory");
        return result;
}

down基本类似,不过加了返回值,如果获取到锁会将eax置为0,即"xorl %0,%0\n",然后做为result返回

2.3.down_trylock

swift 复制代码
static inline int down_trylock(struct semaphore * sem)
{
        int result;

        __asm__ __volatile__(
                "# atomic interruptible down operation\n\t"
                LOCK "decl %1\n\t"     /* --sem->count */
                "js 2f\n\t"
                "xorl %0,%0\n"
                "1:\n"
                LOCK_SECTION_START("")
                "2:\tlea %1,%%eax\n\t"
                "call __down_failed_trylock\n\t"
                "jmp 1b\n"
                LOCK_SECTION_END
                :"=a" (result), "=m" (sem->count)
                :
                :"memory");
        return result;
}

down_interruptible一样的处理逻辑

2.4 void up(struct semaphore *sem)

swift 复制代码
static inline void up(struct semaphore * sem)
{
        __asm__ __volatile__(
                "# atomic up operation\n\t"
                LOCK "incl %0\n\t"     /* ++sem->count */
                "jle 2f\n"
                "1:\n"
                LOCK_SECTION_START("")
                "2:\tlea %0,%%eax\n\t"
                "call __up_wakeup\n\t"
                "jmp 1b\n"
                LOCK_SECTION_END
                ".subsection 0\n"
                :"=m" (sem->count)
                :
                :"memory","ax");
}

函数整体功能

arduino 复制代码
static inline void up(struct semaphore * sem)
  • 功能: 原子地增加信号量计数,如果有等待者则唤醒

内联汇编分解

swift 复制代码
__asm__ __volatile__(
        "# atomic up operation\n\t"
        LOCK "incl %0\n\t"     /* ++sem->count */
        "jle 2f\n"             // 如果结果 <= 0,跳转到标签2
        "1:\n"                 // 标签1:正常返回点
        LOCK_SECTION_START("")
        "2:\tlea %0,%%eax\n\t" // 标签2:需要唤醒等待者
        "call __up_wakeup\n\t" // 调用唤醒函数
        "jmp 1b\n"             // 跳回标签1
        LOCK_SECTION_END
        ".subsection 0\n"
        :"=m" (sem->count)     // 输出操作数
        :
        :"memory","ax");       // 破坏描述符

情况1: 没有等待者,直接返回

markdown 复制代码
初始: sem->count >= 0
1. LOCK incl %0  → sem->count++ (变为正数)
2. jle 2f        → 结果 > 0,不跳转
3. 到达标签1,函数返回

情况2: 有等待者,需要唤醒

markdown 复制代码
初始: sem->count < 0 (有等待者)
1. LOCK incl %0  → sem->count++ 
2. jle 2f        → 结果 <= 0,跳转到标签2
3. lea %0,%%eax  → 将信号量地址加载到eax
4. call __up_wakeup → 调用唤醒函数
5. jmp 1b        → 跳回标签1,函数返回
  • down函数将sem->count减一小于0以后需要睡眠
  • up函数将sem->count加一小于等于0以后需要唤醒
  • 等于0要唤醒的原因是之前sem->count是-1,sleepers必然大于0
2.4.1.__up_wakeup
swift 复制代码
asm(
".section .sched.text\n"
".align 4\n"
".globl __up_wakeup\n"
"__up_wakeup:\n\t"
        "pushl %edx\n\t"
        "pushl %ecx\n\t"
        "call __up\n\t"
        "popl %ecx\n\t"
        "popl %edx\n\t"
        "ret"
);

这段代码主要是调用__up函数

段定义

swift 复制代码
".section .sched.text\n"
  • section: 定义代码段的指令
  • .sched.text: 段名称,专门用于调度的相关代码

对齐指令

swift 复制代码
".align 4\n"
  • 确保后续代码或数据在内存中的起始地址是 4 的倍数(即按 4 字节对齐)

全局声明

swift 复制代码
".globl __up_wakeup\n"
  • globl (或 global): 声明符号为全局可见
  • __up_wakeup: 函数名,其他文件可以调用

函数标签

swift 复制代码
"__up_wakeup:\n\t"
  • __up_wakeup: 函数入口点标签

函数体 - 寄存器保存

swift 复制代码
	"pushl %edx\n\t"
	"pushl %ecx\n\t"
  • 保存edxecx等寄存器

函数调用

swift 复制代码
	"call __up\n\t"
  • __up: 实际的C函数,处理信号量等待逻辑

函数体 - 寄存器恢复

swift 复制代码
	"popl %ecx\n\t"
	"popl %edx\n\t"
  • 恢复调用前的寄存器值

函数体 - 帧指针恢复

swift 复制代码
#if defined(CONFIG_FRAME_POINTER)
	"movl %ebp,%esp\n\t"
	"popl %ebp\n\t"
#endif

返回指令

arduino 复制代码
	"ret"
  • ret: 返回指令,从栈中弹出返回地址并跳转

二、might_sleep函数的实现

scss 复制代码
#define time_after(a,b)         \
        (typecheck(unsigned long, a) && \
         typecheck(unsigned long, b) && \
         ((long)(b) - (long)(a) < 0))
#define time_before(a,b)        time_after(b,a)

#ifdef CONFIG_DEBUG_SPINLOCK_SLEEP
void __might_sleep(char *file, int line)
{
#if defined(in_atomic)
        static unsigned long prev_jiffy;        /* ratelimiting */

        if ((in_atomic() || irqs_disabled()) &&
            system_state == SYSTEM_RUNNING) {
                if (time_before(jiffies, prev_jiffy + HZ) && prev_jiffy)
                        return;
                prev_jiffy = jiffies;
                printk(KERN_ERR "Debug: sleeping function called from invalid"
                                " context at %s:%d\n", file, line);
                printk("in_atomic():%d, irqs_disabled():%d\n",
                        in_atomic(), irqs_disabled());
                dump_stack();
        }
#endif
}

#define might_sleep() __might_sleep(__FILE__, __LINE__)

如果在编译内核时开启了CONFIG_DEBUG_SPINLOCK_SLEEP调试配置,那么可以通过这个函数检测是否有非法上下文睡眠

1.宏定义分析

scss 复制代码
#define time_after(a,b)         \
        (typecheck(unsigned long, a) && \
         typecheck(unsigned long, b) && \
         ((long)(b) - (long)(a) < 0))
#define time_before(a,b)        time_after(b,a)

时间比较宏

  • time_after(a,b):检查时间a是否在时间b之后
  • time_before(a,b):检查时间a是否在时间b之前
  • 使用(long)转换确保相减以后是带符号比较的

2.核心函数:__might_sleep

arduino 复制代码
void __might_sleep(char *file, int line)
{
#if defined(in_atomic)
        static unsigned long prev_jiffy;        /* 频率限制 */
        
        // 检查是否在原子上下文或中断禁用状态下
        if ((in_atomic() || irqs_disabled()) &&
            system_state == SYSTEM_RUNNING) {
                
                // 频率限制:1秒内只报告一次
                if (time_before(jiffies, prev_jiffy + HZ) && prev_jiffy)
                        return;
                prev_jiffy = jiffies;
                
                // 输出错误信息
                printk(KERN_ERR "Debug: sleeping function called from invalid"
                                " context at %s:%d\n", file, line);
                printk("in_atomic():%d, irqs_disabled():%d\n",
                        in_atomic(), irqs_disabled());
                dump_stack();  // 打印堆栈跟踪
        }
#endif
}

3.关键检查条件

3.1. 非法上下文检测

scss 复制代码
(in_atomic() || irqs_disabled()) && system_state == SYSTEM_RUNNING

非法情况包括

scss 复制代码
// PREEMPT_MASK: 0x000000ff 0-7   bit
// SOFTIRQ_MASK: 0x0000ff00 8-15  bit
// HARDIRQ_MASK: 0x0fff0000 16-27 bit
// PREEMPT_ACTIVE           28    bit
#define PREEMPT_ACTIVE		0x10000000
#define kernel_locked()         (current->lock_depth >= 0)
# define in_atomic()    ((preempt_count() & ~PREEMPT_ACTIVE) != kernel_locked())
#define irqs_disabled()                 \
({                                      \
        unsigned long flags;            \
        local_save_flags(flags);        \
        !(flags & (1<<9));              \
})
  • kernel_locked

    • >=0说明内核在上锁状态,不上锁时=-1
    • 在当前代码中已经不会对lock_depth递增,因此这个判断永远为0
  • preempt_count() & ~PREEMPT_ACTIVE

    • ~PREEMPT_ACTIVE首先获取计数器的掩码
    • &以后得到的结果如果>=1则说明计数器有值,可能处于硬中断/软中断/内核禁用抢占上下文中
  • ((preempt_count() & ~PREEMPT_ACTIVE) != kernel_locked())

    • 可以认为这个判断就是((preempt_count() & ~PREEMPT_ACTIVE) != 0
    • 所以不等于0就说明当前有可能处于硬中断/软中断/内核禁用抢占上下文中
  • irqs_disabled()

    • EFLAGS 寄存器第 9 位保存着IF的值
    • IF=1:中断启用(IRQs enabled)
    • IF=0:中断禁用(IRQs disabled)
    • 因此irqs_disabled返回1说明中断禁用,返回0说明中断启用
  • system_state == SYSTEM_RUNNING

    • 检查系统是否处于正常运行状态(而非启动、关机或挂起阶段)

3.2频率限制机制

kotlin 复制代码
if (time_before(jiffies, prev_jiffy + HZ) && prev_jiffy)
    return;
  • 防止相同错误频繁打印,最多每秒报告一次
  • HZ是系统时钟频率,通常为1000

4.为什么需要这个检查?

当一个函数需要调用可能导致睡眠的函数时,可以通过这个检查函数来判断当前上下文是不是可以睡眠的,如果是原子上下文,那么它会打印调试日志,从而快速定位非法睡眠的位置

三、__down信号量的阻塞式获取的实现

ini 复制代码
fastcall void __sched __down(struct semaphore * sem)
{
        struct task_struct *tsk = current;
        DECLARE_WAITQUEUE(wait, tsk);
        unsigned long flags;

        tsk->state = TASK_UNINTERRUPTIBLE;
        spin_lock_irqsave(&sem->wait.lock, flags);
        add_wait_queue_exclusive_locked(&sem->wait, &wait);

        sem->sleepers++;
        for (;;) {
                int sleepers = sem->sleepers;

                if (!atomic_add_negative(sleepers - 1, &sem->count)) {
                        sem->sleepers = 0;
                        break;
                }
                sem->sleepers = 1;      /* us - see -1 above */
                spin_unlock_irqrestore(&sem->wait.lock, flags);

                schedule();

                spin_lock_irqsave(&sem->wait.lock, flags);
                tsk->state = TASK_UNINTERRUPTIBLE;
        }
        remove_wait_queue_locked(&sem->wait, &wait);
        wake_up_locked(&sem->wait);
        spin_unlock_irqrestore(&sem->wait.lock, flags);
        tsk->state = TASK_RUNNING;
}

这是Linux内核中信号量的阻塞式获取实现

1.函数整体流程

scss 复制代码
#define __WAITQUEUE_INITIALIZER(name, tsk) {                            \
        .task           = tsk,                                          \  // 关联的任务结构体
        .func           = default_wake_function,                        \  // 默认唤醒函数
        .task_list      = { NULL, NULL } }  
#define DECLARE_WAITQUEUE(name, tsk)                                    \
        wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)
static inline void add_wait_queue_exclusive_locked(wait_queue_head_t *q,
                                                   wait_queue_t * wait)
{
        wait->flags |= WQ_FLAG_EXCLUSIVE;
        __add_wait_queue_tail(q,  wait);
}
fastcall void __sched __down(struct semaphore * sem)
{
    struct task_struct *tsk = current;
    DECLARE_WAITQUEUE(wait, tsk);  // 声明等待队列元素
    unsigned long flags;

    tsk->state = TASK_UNINTERRUPTIBLE;  // 设置进程状态为不可中断睡眠
    spin_lock_irqsave(&sem->wait.lock, flags);  // 获取保护锁
    add_wait_queue_exclusive_locked(&sem->wait, &wait);  // 加入等待队列

1.1.等待队列数据结构

等待队列条目(wait_queue_t)

arduino 复制代码
#define __WAITQUEUE_INITIALIZER(name, tsk) {                            \
        .task           = tsk,                                          \  // 关联的任务结构体
        .func           = default_wake_function,                        \  // 默认唤醒函数
        .task_list      = { NULL, NULL } }  // 链表节点

结构体字段

  • task: 指向等待的进程描述符(struct task_struct *
  • func: 唤醒时调用的函数,默认是default_wake_function
  • task_list: 链表节点,用于链接到等待队列中

创建等待队列条目

scss 复制代码
#define DECLARE_WAITQUEUE(name, tsk)                                    \
        wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

这创建一个名为name的等待队列条目,关联到任务tsk

1.2.添加到等待队列

scss 复制代码
static inline void add_wait_queue_exclusive_locked(wait_queue_head_t *q,
                                                   wait_queue_t * wait)
{
        wait->flags |= WQ_FLAG_EXCLUSIVE;  // 设置为独占等待
        __add_wait_queue_tail(q,  wait);   // 添加到队列尾部
}

关键点

  • WQ_FLAG_EXCLUSIVE: 标记为独占等待者

    • 独占等待者会被逐个唤醒(避免"惊群效应")
    • 非独占等待者会被全部唤醒
  • __add_wait_queue_tail: 添加到队列尾部

1.3.详细执行过程

步骤1: 创建等待队列条目

scss 复制代码
DECLARE_WAITQUEUE(wait, tsk);

展开后相当于:

ini 复制代码
wait_queue_t wait = {
    .task      = current,                    // 当前进程
    .func      = default_wake_function,      // 默认唤醒函数
    .task_list = { NULL, NULL },             // 空链表节点
    .flags     = 0                           // 初始标志
};

步骤2: 设置进程状态

ini 复制代码
tsk->state = TASK_UNINTERRUPTIBLE;
  • TASK_UNINTERRUPTIBLE: 不可中断睡眠

    • 进程不会响应信号(如Ctrl+C)
    • 只有在资源可用时才会被唤醒
    • 适用于必须完成的关键操作

步骤3: 加锁保护

scss 复制代码
spin_lock_irqsave(&sem->wait.lock, flags);
  • 保护等待队列的并发访问
  • irqsave版本还会保存中断状态并关中断,防止中断处理程序破坏数据

步骤4: 加入等待队列

bash 复制代码
add_wait_queue_exclusive_locked(&sem->wait, &wait);

执行后:

  1. wait.flags |= WQ_FLAG_EXCLUSIVE - 标记为独占
  2. wait添加到sem->wait队列的尾部

2.核心循环逻辑

ini 复制代码
sem->sleepers++;
for (;;) {
    int sleepers = sem->sleepers;

    if (!atomic_add_negative(sleepers - 1, &sem->count)) {
        sem->sleepers = 0;
        break;
    }
    
    sem->sleepers = 1;
    spin_unlock_irqrestore(&sem->wait.lock, flags);

    schedule();

    spin_lock_irqsave(&sem->wait.lock, flags);
    tsk->state = TASK_UNINTERRUPTIBLE;
}

变量含义

  • sem->count: 信号量计数值

    • 0: 可用资源数

    • =0: 无可用资源,无等待者
    • <0: 绝对值是等待者数量
  • sem->sleepers: 当前在等待队列中的进程数量估计值

2.1.初始状态

rust 复制代码
sem->sleepers++;  // 当前进程加入等待,sleepers自增1

2.2.关键判断逻辑

ini 复制代码
if (!atomic_add_negative(, &sem->count)) {
    sem->sleepers = 0;
    break;  // 可以获取信号量
}

atomic_add_negative

  • 相加后结果为负则返回1,否则返回0

本质上就是判断当前信号量可不可以用,如果将睡眠进程唤醒能不能获取到信号量

举例:

ini 复制代码
初始状态:sem->count=0,sleepers=0,信号量已被占用,现在来了一个进程想要获取该信号量
 ↓
down  --sem->count   sem->count=-1
 ↓
__down sleepers++    sleepers=1
 ↓
sem->count + (sleepers - 1)  这里把其他睡眠进程减的一都加回到信号量了
 ↓
如果为0说明自己可以获取该信号量,说明已经释放资源

2.3.睡眠路径

scss 复制代码
// 如果无法获取信号量
sem->sleepers = 1;      // 重置为只有我们自己,确保会执行唤醒函数即可
spin_unlock_irqrestore(&sem->wait.lock, flags);

schedule();            // 让出CPU,进入睡眠

// 被唤醒后重新尝试
spin_lock_irqsave(&sem->wait.lock, flags);
tsk->state = TASK_UNINTERRUPTIBLE;  // 确保仍然是睡眠状态

sem->sleepers = 1;

  • 这里将sleepers直接置为1是因为其他睡眠进程的减的一都加回去了
  • 代表有没有睡眠进程,用于判断是否要执行唤醒操作
  • 这也是为什么说sleepers只是一个估计值的原因,通过它并不可以判断有多少个睡眠进程
  • 更准确的判断有多少个睡眠进程的方法是sem->count的绝对值

schedule();

  • 调用schedule();会触发CPU重新选择任务去运行
  • 这里说明当获取不到信号量时进程会选择睡眠
  • spinlock在锁没有释放的时候,CPU会不断执行rep;nop指令,除非进程被抢占或者中断打断

tsk->state = TASK_UNINTERRUPTIBLE;

  • 置为TASK_UNINTERRUPTIBLE的目的是确保不会被中断打断,比如一些信号(CTRL+C)
  • 强调需要获取指定资源以后才能继续往下执行,这里是信号量

2.4.清理工作

scss 复制代码
remove_wait_queue_locked(&sem->wait, &wait);  // 从等待队列移除
wake_up_locked(&sem->wait);  // 唤醒其他等待者
spin_unlock_irqrestore(&sem->wait.lock, flags);  // 释放锁
tsk->state = TASK_RUNNING;  // 恢复运行状态

tsk->state = TASK_RUNNING;

  • 在函数初始将tsk的state置为了TASK_UNINTERRUPTIBLE,用于强调当前需要获取的资源是必要是,这里进行恢复

wake_up_locked

  • 当前上下文环境中已经对sem->wait进行了加锁,所以调用这个不加锁的唤醒函数

四、__down_interruptible可中断加锁函数

ini 复制代码
/* 源码位置:arch/i386/kernel/semaphore.c */
fastcall int __sched __down_interruptible(struct semaphore * sem)
{
	int retval = 0;
	struct task_struct *tsk = current;
	DECLARE_WAITQUEUE(wait, tsk);
	unsigned long flags;

	tsk->state = TASK_INTERRUPTIBLE;
	spin_lock_irqsave(&sem->wait.lock, flags);
	add_wait_queue_exclusive_locked(&sem->wait, &wait);

	sem->sleepers++;
	for (;;) {
		int sleepers = sem->sleepers;

		/*
		 * With signals pending, this turns into
		 * the trylock failure case - we won't be
		 * sleeping, and we* can't get the lock as
		 * it has contention. Just correct the count
		 * and exit.
		 */
		if (signal_pending(current)) {
			retval = -EINTR;
			sem->sleepers = 0;
			atomic_add(sleepers, &sem->count);
			break;
		}

		/*
		 * Add "everybody else" into it. They aren't
		 * playing, because we own the spinlock in
		 * wait_queue_head. The "-1" is because we're
		 * still hoping to get the semaphore.
		 */
		if (!atomic_add_negative(sleepers - 1, &sem->count)) {
			sem->sleepers = 0;
			break;
		}
		sem->sleepers = 1;	/* us - see -1 above */
		spin_unlock_irqrestore(&sem->wait.lock, flags);

		schedule();

		spin_lock_irqsave(&sem->wait.lock, flags);
		tsk->state = TASK_INTERRUPTIBLE;
	}
	remove_wait_queue_locked(&sem->wait, &wait);
	wake_up_locked(&sem->wait);
	spin_unlock_irqrestore(&sem->wait.lock, flags);

	tsk->state = TASK_RUNNING;
	return retval;
}

基本和__down函数类似,但是多了判断当前有没有中断信号的逻辑,如果有的话当前加锁函数会被直接打断并放弃加锁

五、__down_trylock尝试加锁函数

scss 复制代码
/* 源码位置:arch/i386/kernel/semaphore.c */
fastcall int __down_trylock(struct semaphore * sem)
{
	int sleepers;
	unsigned long flags;

	spin_lock_irqsave(&sem->wait.lock, flags);
	sleepers = sem->sleepers + 1;
	sem->sleepers = 0;

	/*
	 * Add "everybody else" and us into it. They aren't
	 * playing, because we own the spinlock in the
	 * wait_queue_head.
	 */
	if (!atomic_add_negative(sleepers, &sem->count)) {
		wake_up_locked(&sem->wait);
	}

	spin_unlock_irqrestore(&sem->wait.lock, flags);
	return 1;
}

功能 : 尝试获取信号量,如果无法立即获取则直接返回,不会阻塞,睡眠

sem->sleepers = 0;

  • 立即清空sleepers,不进入等待队列

sleepers = sem->sleepers + 1;

  • 其他睡眠者不是通过down_trylock函数加锁的,所以它们会始终等待一个信号量
  • down_trylock函数加锁时一旦一开始没有获取到信号量,后面它调用__down_trylock函数执行的唯一目的就是必要时去唤醒其他睡眠的进程,其次还需要把前面减一加回去
  • 因为__down_trylock是非阻塞的,后面不管结果如何它都是返回1,即获取失败

!atomic_add_negative(sleepers, &sem->count)

  • 把前面函数down_trylocksem->count减的一加回去,因为后续函数就退出了,需要恢复原状
  • 这里有一个缺陷,相加以后为0并不代表信号量可用,所以被唤醒进程很有可能还是获取不到信号量

六、wake_up_locked不加锁唤醒函数

scss 复制代码
void fastcall __wake_up_locked(wait_queue_head_t *q, unsigned int mode)
{
        __wake_up_common(q, mode, 1, 0, NULL);
}
#define	wake_up_locked(x)		__wake_up_locked((x), TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE)

调用__wake_up_common函数

  • nr_exclusive传入1,说明每次只唤醒一个睡眠的进程
  • sync传入0,说明是异步唤醒,可以被抢占,延迟调度

七、__up加锁唤醒函数

scss 复制代码
#define wake_up(x)			__wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 1, NULL)
void fastcall __wake_up(wait_queue_head_t *q, unsigned int mode,
                                int nr_exclusive, void *key)
{
        unsigned long flags;

        spin_lock_irqsave(&q->lock, flags);
        __wake_up_common(q, mode, nr_exclusive, 0, key);
        spin_unlock_irqrestore(&q->lock, flags);
}
fastcall void __up(struct semaphore *sem)
{
	wake_up(&sem->wait);
}

给等待队列加锁以后再调用唤醒函数,适用于没有加锁的上下文

八、__wake_up_common实际执行唤醒函数

arduino 复制代码
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
                             int nr_exclusive, int sync, void *key)
{
        struct list_head *tmp, *next;

        list_for_each_safe(tmp, next, &q->task_list) {
                wait_queue_t *curr;
                unsigned flags;
                curr = list_entry(tmp, wait_queue_t, task_list);
                flags = curr->flags;
                if (curr->func(curr, mode, sync, key) &&
                    (flags & WQ_FLAG_EXCLUSIVE) &&
                    !--nr_exclusive)
                        break;
        }
}
  • 遍历等待队列链表
  • 通过list_entry宏获取对应链表节点的wait_queue_t结构体
  • 执行对应的唤醒函数
  • 如果有独占标志的话,则nr_exclusive减为0就停止唤醒
  • 如果把nr_exclusive设为1,则和互斥效果一致

九、总结

linux 2.6.10版本中信号量的实现看起来很冗余复杂,逻辑也很让人费解,其中还有一些不适当的判断,不过实现的基本原理是不会变的,可以搭配高版本信号量的实现代码查看,应该可以体会到其中的韵味

相关推荐
東雪蓮☆4 小时前
MySQL 全量 + 增量备份脚本(RPM 安装)实践与问题解析
linux·运维·mysql
落羽的落羽4 小时前
【Linux系统】快速入门一些常用的基础指令
linux·服务器·人工智能·学习·机器学习·aigc
大白的编程日记.11 小时前
【Linux学习笔记】线程概念和控制(二)
linux·笔记·学习
jerryinwuhan11 小时前
VIM和Linux命令速查表
linux·编辑器·vim
小白银子12 小时前
零基础从头教学Linux(Day 45)
linux·运维·junit·openresty
穷人小水滴12 小时前
笔记本 光驱 的内部结构及用法: 应急系统启动 (恢复) 光盘 (DVD+R/RW)
linux
半梦半醒*13 小时前
nginx反向代理和负载均衡
linux·运维·nginx·centos·tomcat·负载均衡
青草地溪水旁14 小时前
pthread_create详解:打开多线程编程的大门
linux·c/c++
A-刘晨阳15 小时前
Linux安装centos8及基础配置
linux·运维·服务器·操作系统·centos8