当前位置: 首页 > news >正文

Linux中信号量semaphore的实现

文章目录

  • 一、Linux信号量`semaphore`的实现
    • 1.信号量结构体定义
      • 1.1.atomic_t定义
      • 1.2.wait_queue_head_t定义
    • 2.信号量相关函数及实现
      • 2.1.`void down(struct semaphore *sem)`
        • 2.1.1.宏定义分析
        • 2.1.2.down函数详细解析
        • 2.1.3.__down_failed实现
      • 2.2.`down_interruptible`
      • 2.3.`down_trylock`
      • 2.4 `void up(struct semaphore *sem)`
        • 2.4.1.`__up_wakeup`
  • 二、might_sleep函数的实现
    • 1.宏定义分析
    • 2.核心函数:__might_sleep
    • 3.关键检查条件
      • 3.1. 非法上下文检测
      • 3.2频率限制机制
    • 4.为什么需要这个检查?
  • 三、__down信号量的阻塞式获取的实现
    • 1.函数整体流程
      • 1.1.等待队列数据结构
      • 1.2.添加到等待队列
      • 1.3.详细执行过程
    • 2.核心循环逻辑
      • 2.1.初始状态
      • 2.2.关键判断逻辑
      • 2.3.睡眠路径
      • 2.4.清理工作
  • 四、__down_interruptible可中断加锁函数
  • 五、`__down_trylock`尝试加锁函数
  • 六、`wake_up_locked`不加锁唤醒函数
  • 七、`__up`加锁唤醒函数
  • 八、`__wake_up_common`实际执行唤醒函数
  • 九、总结

参考博客

内核堆栈跟踪函数dump_stack的实现: https://blog.csdn.net/weixin_51019352/article/details/152317302

自旋锁spinlock的实现: https://blog.csdn.net/weixin_51019352/article/details/152059679

内核等待队列(Wait Queue)机制: https://blog.csdn.net/weixin_51019352/article/details/152242298

进程唤醒函数实现default_wake_function: https://blog.csdn.net/weixin_51019352/article/details/152241056

一、Linux信号量semaphore的实现

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

1.信号量结构体定义

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

/* 源码位置: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定义

/* 源码位置: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定义

/* 源码位置:include/linux/list.h */
struct list_head {struct list_head *next, *prev;
};
/* 源码位置: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)

#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.宏定义分析
#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函数详细解析
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"             // 处理完成后跳回标签1LOCK_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实现
/* 源码位置: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函数

段定义

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

对齐指令

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

全局声明

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

函数标签

"__down_failed:\n\t"
  • __down_failed: 函数入口点标签

函数体 - 条件编译部分

#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指向当前栈帧底部,栈向下增长

    • 支持调试和栈回溯

函数体 - 寄存器保存

	"pushl %edx\n\t""pushl %ecx\n\t"
  • 保存edxecx等寄存器

函数调用

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

函数体 - 寄存器恢复

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

函数体 - 帧指针恢复

#if defined(CONFIG_FRAME_POINTER)"movl %ebp,%esp\n\t""popl %ebp\n\t"
#endif

返回指令

	"ret"
  • ret: 返回指令,从栈中弹出返回地址并跳转

2.2.down_interruptible

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

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)

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");
}

函数整体功能

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

内联汇编分解

__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"             // 跳回标签1LOCK_SECTION_END".subsection 0\n":"=m" (sem->count)     // 输出操作数::"memory","ax");       // 破坏描述符

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

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

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

初始: 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
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函数

段定义

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

对齐指令

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

全局声明

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

函数标签

"__up_wakeup:\n\t"
  • __up_wakeup: 函数入口点标签

函数体 - 寄存器保存

	"pushl %edx\n\t""pushl %ecx\n\t"
  • 保存edxecx等寄存器

函数调用

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

函数体 - 寄存器恢复

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

函数体 - 帧指针恢复

#if defined(CONFIG_FRAME_POINTER)"movl %ebp,%esp\n\t""popl %ebp\n\t"
#endif

返回指令

	"ret"
  • ret: 返回指令,从栈中弹出返回地址并跳转

二、might_sleep函数的实现

#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.宏定义分析

#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

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. 非法上下文检测

(in_atomic() || irqs_disabled()) && system_state == SYSTEM_RUNNING

非法情况包括

// 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频率限制机制

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

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

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

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

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.函数整体流程

#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)

#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: 链表节点,用于链接到等待队列中

创建等待队列条目

#define DECLARE_WAITQUEUE(name, tsk)                                    \wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

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

1.2.添加到等待队列

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: 创建等待队列条目

DECLARE_WAITQUEUE(wait, tsk);

展开后相当于:

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

步骤2: 设置进程状态

tsk->state = TASK_UNINTERRUPTIBLE;
  • TASK_UNINTERRUPTIBLE: 不可中断睡眠
    • 进程不会响应信号(如Ctrl+C)
    • 只有在资源可用时才会被唤醒
    • 适用于必须完成的关键操作

步骤3: 加锁保护

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

步骤4: 加入等待队列

add_wait_queue_exclusive_locked(&sem->wait, &wait);

执行后:

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

2.核心循环逻辑

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.初始状态

sem->sleepers++;  // 当前进程加入等待,sleepers自增1

2.2.关键判断逻辑

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

atomic_add_negative

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

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

举例:

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

2.3.睡眠路径

// 如果无法获取信号量
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.清理工作

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可中断加锁函数

/* 源码位置: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尝试加锁函数

/* 源码位置: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不加锁唤醒函数

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加锁唤醒函数

#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实际执行唤醒函数

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

http://www.dtcms.com/a/434903.html

相关文章:

  • 广州网站推广公司建筑工程公司是干嘛的
  • ESP32驱动DHT11温湿度传感器详解
  • flask做的网站 网址做网站推广有什么升职空间
  • 网站上线过程做美团网站多少钱
  • 微信分享网站短链接怎么做公司的介绍怎么写
  • 算法分析:时间和空间复杂度
  • 第6章串数组:稀疏矩阵的十字链表表示
  • 【STM32项目开源】基于STM32的工地环境监测系统
  • 手机登录网站怎么建设如何做一个网站代码
  • 解决django.db.utils.OperationalError: attempt to write a readonly database错误
  • CAN-超时计数器(Timeout Counter)
  • 网站建设策划有哪些建设网站用英文怎么说
  • 报告派研读:2025年光学光电子深度报告
  • 技术演进中的开发沉思-121Linux命令篇:系统设置命令(下)
  • 深入理解 JavaScript 闭包与作用域
  • 【操作系统-Day 38】LRU的完美替身:深入解析时钟(Clock)页面置换算法
  • Linux 入门指南:从零掌握基础文件与目录操作命令
  • 高职院校高水平专业建设网站wordpress的windows
  • 网络原理-HTTPS
  • 马鞍山网站建设文如何查网站注册信息
  • 郑州机械网站建设memcached wordpress 慢 卡
  • Java数据结构:ArrayList与顺序表2
  • python系统设计2-选题
  • 做网站表示时间的控件用哪个wordpress 新窗口打开文章
  • Phase 与 Invisibility 的区别
  • MATLAB学习文档(二十三)
  • 基于php网站开发手机官网
  • 2018 年真题配套词汇单词笔记(考研真相)
  • Portainer实战:轻松搭建Docker可视化管理系统
  • PostgreSql FDW 与 DBLINK 区别