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:成功获取信号量
LOCK decl %0
- 原子性地减少信号量计数,%0
就是第一个操作数,即sem->count
- 如果结果 ≥ 0,继续执行(不跳转)
js
指令是判断当前符号标志位是否为1,为1则跳转,如果上一步操作结果是负数则符号标志位置1- 到达标签1,后续没有指令,函数正常返回
情况2:信号量不可用
LOCK decl %0
- 原子性地减少信号量计数- 如果结果 < 0,跳转到标签2
- 将信号量地址加载到
eax
寄存器 - 调用
__down_failed
函数(通常会阻塞当前进程) - 当信号量可用时,
__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"
- 保存
edx
、ecx
等寄存器
函数调用
"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"
- 保存
edx
、ecx
等寄存器
函数调用
"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);
执行后:
wait.flags |= WQ_FLAG_EXCLUSIVE
- 标记为独占- 将
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_trylock
给sem->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
版本中信号量的实现看起来很冗余复杂,逻辑也很让人费解,其中还有一些不适当的判断,不过实现的基本原理是不会变的,可以搭配高版本信号量的实现代码查看,应该可以体会到其中的韵味