持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情
前言
📫作者简介:小明java问道之路,专注于研究 Java/ Liunx内核/ C++及汇编/计算机底层原理/源码,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计与演进、系统优化与稳定性建设。
📫热衷分享,喜欢原创~ 关注我会给你带来一些不一样的认知和成长。
🏆InfoQ签约作者、CSDN专家博主/后端领域优质创作者/内容合伙人、阿里云专家/签约博主、51CTO专家🏆
🔥如果此文还不错的话,还请👍关注 、点赞 、收藏三连支持👍一下博主~
本文导读
本文深入Linux内核源码,从核心源码入口讲起,详细对信号量、互斥量的内核代码讲解。
其中对P-V操作实现逐行剖析,Linux内核并发控制原理的锁实现和原理在后续文章中一一讲解,本文深入浅出Linux中断控制的实现原理。
一、Linux内核P-V原语
获取信号量 P 操作,通过内联汇编原子性对 counte 操作。首先通过 decl 同时根据是否是多处理器加 lock 前缀,保证了单条指令的原子性,然后根据递减后的值是否为负数来判断获取信号量是否成功,如果失败,那么需要将线程进行睡眠,此时调用 _down_failed 函数完成此操作。
下面我们来看,如何 获取信号量 P 操作原理解析与# void__down最终实现函数。
二、获取信号量 P 操作原理解析
此时调用 _down_failed 函数完成此操作,具体实现原理如下。
通过 lock 前缀实现原子性的-sem->count操作,decl指令相当于对操作数自减
static inline void down(struct semaphore*sem) {
_asm__volatile_( // 通过 lock 前缀实现原子性的-sem->count操作,decl指令相当于对操作数自减
LOCK "decl %O" // 如果减完后发现sign标志位为1,则表明count值为负,往前跳到标号2处,调用 __down_failed处理,否则获取成功,直接退出
"js 2f"
"1: "
LOCK_SECTION_START("")
"2:call__down_failed"
"jmp 1b"
// 这里采用了 LOCK SECTION START 和LOCK SECTION END 宏定义,将call
// __down_failed 和 jmp 1b的汇编代码放到.textlock段中
// 所以如果执行完 __down_failed 方法后调用jmp 1b
// 会回到 LOCK SECTION START之前的段中,即退出down方法
LOCK SECTION END:
: "=m" (sem -> count)
:"c" (sem)
:"memory");
}
// 通过汇编声明了 __down_failed的代码地址
asm(
".text"
".align 4" // 4字节对齐
".globl___down_failed"
"__down failed:"
#if defined(CONFIG_FRAME_POINTER) // 如果定义了栈帧指针,那么开辟新的方法帧
"pushl %ebp"
"movl %esp, %ebp"
#endif
// 保存影响的寄存器值,因为随后要调用_down 函数, 可能会影响 eax、edx、ecx 寄存器,
// 所以这里需要先对其进行保存,在方法返回后再还原
"pushl %eax"
"pushl %edx"
"pushl %ecx"
"call __down" // 调用 __down来执行当counter为0时的操作
"popl %ecx" // 调用返回后恢复保存的寄存器
"popl %edx"
"popl %eax"
#if defined(CONFIG_FRAME_POINTER) //还原方法帧
"movl %ebp,%esp"
"popl %ebp"
#endif
"ret"
);
我们最终是调用函数 __ down 来执行最终的 __down_failed 操作。
三、void__down最终实现函数
下面是 void__down 函数源码,通过current 宏获取当前任务结构体,获取到了任务PCB,初始化wait_queuet,也就是等待线程代表,宏定义为;wait_queue_t name = {.task = tsk, .func = defauft_wake_function, .tasklist = {NULL, NULL}}
设置任务状态为TASK_UNINTERRUPTIBLE,表明不可中断的阻塞,获取自旋锁,将等待任务节点插入等待链表的队尾处,增加等待计数,循环等待释放信号量,对等待线程减1后与当前信号量的counter值相加
1、任务状态TASK_RUNNING
如果结果等于0则结束循环,这里等于0的条件就是等待信号量足够容纳更多的线程,所以不需要阻塞,设置等待任务数为1,释放自旋锁,唤醒调度器执行其他任务,当前任务就被阻塞在了等待队列里
当任务重新被唤醒时,将重新获取自旋锁,重新设置任务状,唤醒等待任务,释放自旋锁,设置当前任务状态为TASK_RUNNING。
2、Liunx内核实现原理
void__down(struct semaphore *sem) {
// 通过current 宏获取当前任务结构体,获取到了任务PCB
struct task_struct *tsk = current;
// 初始化wait_queue t,也就是等待线程代表
// 宏定义为;wait_queue_t name = {.task = tsk, .func = defauft_wake_function, .tasklist = {NULL, NULL}}
DECLARE_WAITQUEUE(wait, tsk);
unsigned long flags;
tsk->state = TASK UNINTERRUPTIBLE; // 设置任务状态为TASK_UNINTERRUPTIBLE,表明不可中断的阻塞
spin_lock_irqsave(&sem -> waitlock, flags); // 获取自旋锁
add_wait_queue_exclusive_locked(&sem -> wait, &wait); // 将等待任务节点插入等待链表的队尾处
sem -> sleepers++; // 增加等待计数
for (; ; ) { // 循环等待释放信号量
int sleepers = sem -> sleepers;
// 对等待线程减1后与当前信号量的counter值相加
// 如果结果等于0则结束循环,这里等于0的条件就是等待信号量足够容纳更多的线程,所以不需要阻塞
if (!atomic_add_negative(sleepers - 1, & sem -> count)){
sem -> sleepers = 0;
break;
}
sem -> sleepers = 1; // 设置等待任务数为1
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; // 设置当前任务状态为TASK_RUNNING
}
上面代码我们可以看到,使用了自旋锁、P-V操作,并增加了阻塞队列实现信号量。如果读者对Linux进程调度原理不清楚,这里面方法 schedule ,其作用就是释放 CPU 的控制权,交给调度程序
然后由调度程序切换到其他进程执行,直到信号量释放后,再由其他进程将其状态设置为 RUNNABLE 后,交由调度进程重新调度执行。