持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
前言
📫作者简介:小明java问道之路,专注于研究 Java/ Liunx内核/ C++及汇编/计算机底层原理/源码,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计与演进、系统优化与稳定性建设。
📫热衷分享,喜欢原创~ 关注我会给你带来一些不一样的认知和成长。
🏆InfoQ签约作者、CSDN专家博主/后端领域优质创作者/内容合伙人、阿里云专家/签约博主、51CTO专家🏆
🔥如果此文还不错的话,还请👍关注 、点赞 、收藏三连支持👍一下博主~
本文导读
本文深入Linux内核源码,从核心源码入口讲起,详细对信号量、互斥量的内核代码讲解。
其中对P-V操作实现逐行剖析,Linux内核并发控制原理的锁实现和原理在后续文章中一一讲解,本文深入浅出Linux中断控制的实现原理。
一、释放信号量 V 操作原理解析
唤醒信号量通过对 semaphore 中的 counter 进行加1,同样通过 LOCK 前缀保证指令的原子性,根据返回值是否小于或等于0来判断是否有线程在等待,如果有线程等待,那么调用_up_wakeup 函数唤醒等待信号量的线程。
static inline void up(struct semaphore*sem) {
_asm___volatile_(
LOCK "incl %O" // 原子性实现++sem->count
"jle 2f" // 如果小于或等于0,则跳到2标志处,调用__up_wakeup函数唤醒等待任务
"1:"
LOCK SECTION_START ("")
"2:call__up_wakeup"
"jmp 1b"
LOCK_SECTION_END
".subsection 0"
:"=m” (sem->count)"
:"c" (sem)"
:" memory ");
}
// 汇编代码保存影响的寄存器,然后调用_up函数唤醒任务
asm(
".text"
".align 4"
".globl___up_wakeup"
"__up_wakeup: "
"pushl %eax"
"pushl %edx"
"pushl %ecx"
"call_ up" // 调用__up 函数
"popl %ecx"
"popl %edx"
"popl %eax"
"ret");
我们可以看到up()函数,原子性实现++sem->count,如果小于或等于0,则跳到2标志处,调用__up_wakeup函数唤醒等待任务,汇编代码保存影响的寄存器,然后调用_up函数唤醒任务,最终调用_up唤醒。
下面看下__up唤醒的源码实现。
二、_up唤醒函数源码实现
__up直接调用wake_up,通过唤醒宏定义,调用唤醒函数实现__wake_up,获取自旋锁,调用__wake_up_common函数唤醒任务,注意这里传入的是sync为0,释放自旋锁的过程
void __up(struct semaphore *sem) {
wake_up( & sem -> wait); // 直接调用wake_up
}
//唤醒宏定义
# define wake_up(x) __wake_up((x),TASK_UNINTERRUPTIBLE |TASK_INTERRUPTIBLE,1)
// 唤醒函数实现
void __wake_up(wait_queue_head_t *q, unsigned int mode, int nr_exclusive) {
unsigned long flags;
spin_lock_irqsave(&q->lock, flags); // 获取自旋锁
// 调用__wake_up_common函数唤醒任务,注意这里传入的是sync为0
_wake_up_common(q, mode, nr_exclusive, 0);
spin_unlock_irqrestore(&q->lock, flags); // 释放自旋锁
}
// 唤醒操作_wake_up_common 函数的实现原理。
static void _wake_up_common(wait_queue_head_*qunsigned int mode, int r_exclusive, int sync) {
struct list_head *tmp,*next;
list_for_each_safe(tmp, next, & q -> task_list){ // 遍历等待列表
wait_queue_t * curr;
unsigned flags;
//获取当前任务节点wait_queue_I
curr = list_entry(tmp, wait_queue_, task_list);
flags = curr -> flags//获取当前等待标志位
// 调用唤醒函数
// 如果成功唤醒任务、 当前任务标志位 WQ_FLAG_EXCLUSIVE、
// nr_exclusive 互斥数量,自减为0,那么退出循环
if (curr -> func(curr, mode, sync) && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
// 默认唤醒函数 default wake function。其实现过程如下
int default_wake_function(walt_queue_curr, unsigned mode, int sync) {
task_t * p = curr -> task; // 获取当前任务
return try_to_wake_up(p, mode, sync); // 调用try_to_wake_up函数唤醒
}
我们可以看到是通过调用try_to_wake_up函数唤醒,其中涉及Linux调度器,且该方法涉及大量实现,这里我们给出源码的相关原理
首先获取到当前CPU的执行队列 runqueue,并且关闭中断,保存当前状态信息,如果当前状态信息和传入状态不为0并且如果当前任务不属于任何优先级队列,执行
任务重调度实现:如果是对称多处理器结构,那么需要通过跨CPU 调用,触发目标CPU调度器的调度工作。
信号量代码逻辑较为简单,对于 P-V 操作,我们通过原子性对 counter 变量操作,然后其放入信号量的等待队列中,或者将其从等待队列中取出。
由于是多线程操作阻塞队列,因此需要把自旋锁来保护阻塞队列。接着判断任务是否处于任务就绪队列 runqueue 中,如果在队列中,则设置标志为 TASKRUNNING 状态,否将入 runqueue 中。这里 runqueue 是每个 CPU 都拥有的任务就绪调度队列。