一、背景
schedule作为内核进程切换的入口,用于选择一个新的进程进行调度。
二、函数内部重要方法
schedule
|---------->__schedule(false)
|---------------->pick_next_task(进程选择策略,从中选择一个的进程)
|---------------->context_switch(进程上下文切换)
|------------------->switch_mm_irqs_off(进行页切换,如更新cr3寄存器)
|------------------->switch_to(进行内核栈切换和硬件寄存器的切换)
三、重点函数理解
3.1 switch_mm_irqs_off(页切换)
todo.
3.1 switch_to
该函数完成两个进程间的切换,函数定义:
#define switch_to(prev, next, last) \
asm volatile(SAVE_CONTEXT \
"movq %%rsp,%P[threadrsp](%[prev])\n\t" /* save RSP */ \
"movq %P[threadrsp](%[next]),%%rsp\n\t" /* restore RSP */ \
"call __switch_to\n\t" \
"movq "__percpu_arg([current_task])",%%rsi\n\t" \
__switch_canary \
__retpoline_fill_return_buffer \
"movq %P[thread_info](%%rsi),%%r8\n\t" \
"movq %%rax,%%rdi\n\t" \
"testl %[_tif_fork],%P[ti_flags](%%r8)\n\t" \
"jnz ret_from_fork\n\t" \
RESTORE_CONTEXT \
: "=a" (last) \
__switch_canary_oparam \
: [next] "S" (next), [prev] "D" (prev), \
[threadrsp] "i" (offsetof(struct task_struct, thread.sp)), \
[ti_flags] "i" (offsetof(struct thread_info, flags)), \
[_tif_fork] "i" (_TIF_FORK), \
[thread_info] "i" (offsetof(struct task_struct, stack)), \
[current_task] "m" (current_task) \
__switch_canary_iparam \
: "memory", "cc" __EXTRA_CLOBBER)
首先一些要点说明:
prev:放弃cpu的进程,task_struct结构体地址
next:获得cpu的进程,task_struct结构体地址
last:实际放弃cpu而让next获得cpu的进程,task_struct结构体地址
[next] "S" (next) :意思是将next的值放到rsi寄存器中,而在内联汇编中用[next]符号来代替
[prev] "D" (prev):意思是将prev的值放倒rdi寄存器中,而在内联汇编中用[prev]符号来代替
"=a" (last) :意思是当内联汇编结束后,将rax寄存器的值存放到当前last值中,其中last表示当前context_switch的局部变量(依赖于寄存器rbp的值)
rbp:用于查询局部变量,因为其不随push和pop而改变其地址
rsp:用于push和pop
该函数可以分为五个部分说明,这里A代表prev进程,B代表next进程:
1)SAVE_CONTEXT:
#define SAVE_CONTEXT "pushq %%rbp ; movq %%rsi,%%rbp\n\t"
这里简单表示两个进程的内核栈中的数据,由于switch_to是宏定义,因此其不会改变函数context_switch的rbp和rsp的值,这时候,A_prev(表示A进程中的局部变量prev)和A_next(表示A进程的局部变量next)保存在自己的内核栈中,此时执行完SAVE_CONTEXT后,当前的rbp压栈(即当前rbp的值是A进程中调用context_switch时的rbp值,也就定义为A_cs_rbp值)即值A_cs_rbp值压栈,rsp向下移动,并且rbp=rsi=A_next,rdi=A_prev。
同理B进程让出cpu的时候,也是通过调用context_switch进行进程切换的,同理其内核栈也会保存B_prev(表示B进程中的局部变量prev)和B_next(表示B进程的局部变量next),还有调用完SAVE_CONTEXT后的rbp压栈操作B_cs_rbp(B进程调用context_switch时的rbp值,也就定义为B_cs_rbp值)。
2) save rsp,restore rsp:
"movq %%rsp,%P[threadrsp](%[prev])\n\t" /* save RSP */ \
"movq %P[threadrsp](%[next]),%%rsp\n\t" /* restore RSP */ \
将当前的rsp的值保存到A的task_struct.thread.rsp中,用于以后的回复操作。所以反过来便是将B的task_struct.rsp值设置到当前的rsp中,这样后序的push和pop操作是在B的内核栈中进行。但是记住当前的rbp未正确设置成B内核的值。
3)call __switch_to:
"call __switch_to\n\t"
该函数主要是进行硬件上下切换(即一些寄存器等),所以这里直接跳过,只需要查看其最后返回的是prev_p,因此该函数结束后,变化的情况除了B进程的硬件相关的寄存器恢复外,便是将A_prev(A中的prev进程task_struct的地址)存放到rax寄存器中了。
4)mav rax,rdi:
"movq %%rax,%%rdi\n\t"
将rdi寄存器的值设置成rax进程器的值,即存储A_prev的值
5)RESTORE_CONTEXT:
#define RESTORE_CONTEXT "movq %%rbp,%%rsi ; popq %%rbp\t"
上面这个图不只是RESTORE_CONTEXT这一步,还有几步才最终得到。所以下面讲解下。
首先RESTORE_CONTEXT是将rbp的值设置到rsi中,然后popq %rbp中(记住pop与rsp相关),因此这时候的rsp已经是指向B进程的内核栈了,因此此时的rbp=B_cs_rbp的值了,因此此时可以通过rbp来得到局部变量B_prev(这里没写错,图的A_prev是后面几步得到)和B_next了。而该内敛汇编后后面有一句为:
: "=a" (last)
还记得的话,是将rax的值存放到last中,那么此时last的值便是rax寄存器的值,也就是A_prev的值。又因为context_switch函数中调用switch_to的函数为:
switch_to(prev, next, prev);
而这时候rbp已经正确设置为B进程的内核栈,那么原来的B_prev也就被改变等于last,也就是A_prev的值了。这样便完成了进程的内核栈和硬件上下问切换了。
注:为什么需要last这个变量。
1.首先我们可以看到上面的所有讲解都没有涉及到rip的保存问题,那么新进程是返回到哪里进行代码执行呢?当进程完成内核栈和硬件上下文切换(还有前面的页切换)后,新的进程之前是由于进行了schedule->__schdule(false)->switch_to而使得内核栈切换了,而栈中在每个函数调用都会保存要返回的地址信息。因此内核栈被改变间接也就改变了原来进程的调用路径。因此新的进程被唤醒后,则会从schedule->_schdule(false)->switch_to原路返回,最终的差异便是在schedule函数中由哪个函数调用其,便会返回到哪个函数上。
2.有一点是需要明确的,虽然一个进程调用了switch_to(prev,next,last)后,那么在调用该函数的时候prev便是当前进程(舍弃cpu),next便是需要得到cpu的进程。当prev进程被另一个进程唤醒的时候,理论是要返回到当前的switch_to(prev,next,last)的,那么如果该进程的内核栈的值都没有被改变,那么此时prev还是自己,但是后序需要对prev(舍弃cpu)的进程进行清理工作。因此这是不行的。所以便需要将实际舍弃cpu的进程相关信息(task_struct)地址存放到last中,这样再将last的值重新改变prev的值,那么这时候便可以得到实际舍弃cpu的进程相关信息了。