第八周 进程的切换和系统的一般执行过程
1. 进程切换在内核中的实现
linux中进程切换是很常见的一个操作,而这个操作是在内核中实现的。这里我们来分析下内核中是如何实现这个操作的。
linux内核中的线程切换主要通过schedule()这个函数实现。实现的时机有以下三个时机
-
中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
-
内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
-
用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
进程的切换时需要保存要切换进程的相关信息,但是这与中断是不同的,因为中断是在一个进程当中,而切换进程需要在不同的进程间切换。进程切换时需要保存的数据如下
-
用户地址空间:包括程序代码,数据,用户堆栈等
-
控制信息:进程描述符,内核堆栈等
-
硬件上下文(注意中断也要保存硬件上下文只是保存的方法不同)
进程切换时涉及的代码如下
-
schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换
-
next = pick_next_task(rq, prev);//进程调度算法都封装这个函数内部
-
context_switch(rq, prev, next);//进程上下文切换
-
switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程
其运行的流程图如下:
2. 实验以及代码分析
利用之前几周的menu代码进行实验,这里分别在schedule, pick_next_task, context_switch函数上设置断点,又通过观看context_switch(rq, prev, next)发现switch_to这个宏定义位于context_switch中。以下是实验截图,可以看到依次调用了在schedule, pick_next_task, context_switch函数之中。
这里进程保存数据主要位于switch_to这个宏定义中,代码如下
1 #define switch_to(prev, next, last) \ 2 do { \ 3 /* \ 4 * Context-switching clobbers all registers, so we clobber \ 5 * them explicitly, via unused output variables. \ 6 * (EAX and EBP is not listed because EBP is saved/restored \ 7 * explicitly for wchan access and EAX is the return value of \ 8 * __switch_to()) \ 9 */ \ 10 unsigned long ebx, ecx, edx, esi, edi; \ 11 \ 12 asm volatile("pushfl\n\t" /* save flags */ \ 13 "pushl %%ebp\n\t" /* save EBP */ \ 14 "movl %%esp,%[prev_sp]\n\t" /* save ESP */ \ 15 "movl %[next_sp],%%esp\n\t" /* restore ESP */ \ 16 "movl $1f,%[prev_ip]\n\t" /* save EIP */ \ 17 "pushl %[next_ip]\n\t" /* restore EIP */ \ 18 __switch_canary \ 19 "jmp __switch_to\n" /* regparm call */ \ 20 "1:\t" \ 21 "popl %%ebp\n\t" /* restore EBP */ \ 22 "popfl\n" /* restore flags */ \ 23 \ 24 /* output parameters */ \ 25 : [prev_sp] "=m" (prev->thread.sp), \ 26 [prev_ip] "=m" (prev->thread.ip), \ 27 "=a" (last), \ 28 \ 29 /* clobbered output registers: */ \ 30 "=b" (ebx), "=c" (ecx), "=d" (edx), \ 31 "=S" (esi), "=D" (edi) \ 32 \ 33 __switch_canary_oparam \ 34 \ 35 /* input parameters: */ \ 36 : [next_sp] "m" (next->thread.sp), \ 37 [next_ip] "m" (next->thread.ip), \ 38 \ 39 /* regparm parameters for __switch_to(): */ \ 40 [prev] "a" (prev), \ 41 [next] "d" (next) \ 42 \ 43 __switch_canary_iparam \ 44 \ 45 : /* reloaded segment registers */ \ 46 "memory"); \ 47 } while (0)
这里可以看到switch_to代码主要实现了进程间的切换中的保存工作。这里使用do while这种形式的宏定义主要是为了兼容C语言中以;结尾的格式。从switch_to这个宏定义的三个参数当中可以看到pre是指向前一个进程,next是指向后一个进程。12—22行主要实现的是对pre进程的EBP,ESP,EIP压入堆栈,然后将next的值赋给EBP,ESP,EIP。这里需要注意是JMP指令,这里不使用call指令是因为要保持next_ip始终位于栈顶,这样ret后正好可以将next_ip取出。
3. 总结
linux内核中实现进程的切换主要通过保存进程相关的信息实现,这里需要注意进程切换中内核级进程的切换和用户态进程切换的不同。内核态可以直接调用schedule函数并不需要陷入中断这个过程。而用户态则需要陷入内核态才能实现进程的切换。从switch_to这个函数当中也可以验证我们进程切换时会保存相关信息的推断。