[linux_内核相关] x86_64下进程切换schedule理解

一、背景

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的进程相关信息了。

发布了140 篇原创文章 · 获赞 28 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/qq_16097611/article/details/84249382