持续创造,加快生长!这是我参加「日新方案 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 后,交由调度进程从头调度履行。