进程—进程调度(1)
上下文切换
进程可以调度,但必须保证每个进程都可以顺序的执行,而一个进程执行所需的全部信息可由进程的PCB(task_struct)维护,所以在进程发生切换的时候可以将当前进程的运行状态信息(快照)保存到它的PCB中(这样就能在下一次调度程序选择到它时接着上一状态继续执行),将马上要执行的进程的运行状态信息(在PCB中)恢复,这样就可以合理的完成调度,这个过程就叫上下文切换。
中断
上下文切换是在内核中完成的,对用户透明,所以在上下文切换的时候必须先陷入内核(一般是通过时钟中断和系统调用)。上下文的切换需要硬件的支持。当前进程正在运行,当中断发生时,中断硬件将程序计数器、程序状态字、有时还有一个或多个寄存器(进程的运行状态信息)压入当前进程的内核堆栈中,PC随即跳转到中断服务程序入口(根据硬件向量法或软件查询法得到中断服务程序入口地址)去执行中断服务程序。注意,这些工作都是硬件完成的,在这些工作完成的同时完成了一次堆栈切换(从进程的用户堆栈切换到进程的内核堆栈,由中断硬件完成)。然后,PC的控制权转移到了软件(中断服务程序),一般地,中断服务程序有一个自己的中断堆栈(就像进程有自己的内核堆栈一样),为了不破坏内核堆栈,在执行中断服务程序的过程中又会发生一次堆栈切换(从内核堆栈切换到中断堆栈,这次的切换是软件完成的),进入到中断上下文之中,随后中断服务程序会调用处理特定中断请求的中断处理例程,让中断处理例程运行在中断上下文之中。中断处理例程完成中断处理之后返回到中断服务程序,返回的过程又会涉及到一次堆栈切换(由中断堆栈切换到内核堆栈,这次的切换也是软件完成的),最后,中断服务程序执行中断返回指令,由中断硬件将内核堆栈中的状态信息恢复到相应的寄存器中,在恢复寄存器的同时清空内核堆栈中保存的状态信息,系统从内核态返回到用户态,这里又发生了一次堆栈切换(从内核堆栈切换到用户堆栈,由中断硬件完成)。
发生上下文切换的中断过程
中断服务程序调用中断处理例程,中断处理例程在执行的过程中调用了调度程序进行调度,这时,调度程序会检查是否需要发生上下文切换(不是每次中断都会发生上下文的切换,例如某个时钟中断就可能不会导致上下文切换),发现需要发生上下文切换。接下来,要完成不同进程之间的内核栈的切换。为什么要切换内核栈呢?因为
restore_all
需要从内核栈中恢复中断现场,每个进程都有一个内核堆栈,它们互不相同,里面保存了各自的中断现场,所以要想恢复进程A的中断现场就得先切换到进程A的内核堆栈中去。参考《Linux内核代码情景分析》第三章
ENTRY(ret_from_intr) ... jne ret_with_reschedule //需要返回用户态 ... ret_with_reschedule: cmpl $0,need_resched(%ebx) jne reschedule cmpl $0,sigpending(%ebx) jne signal_return restore_all: RESTORE_ALL reschedule: call SYMBOL_NAME(schedule)//完成任务的调度以及内核堆栈的切换 ... jmp ret_from_intr #define RESTORE_ALL \ popl %ebx; \ popl %ecx; \ popl %edx; \ popl %esi; \ popl %edi; \ popl %ebp; \ popl %eax; \ 1: popl %ds; \ 2: popl %es; \ addl $4,%esp; \ 3: iret; \ /* 忽略信号处理等流程,可以看到,中断返回用户态的流程大概是 ret_from_intr-->[jne]ret_with_reschedule-->[jne]reschedule-->[call]schedule -->[jmp]ret_from_intr-->[jne]ret_with_reschedule-->restore_all。 除了schedule是用call指令被调用之外,其他的流程都是通过跳转指令到达,所以不会返回。 在第一次执行ret_from_intr处的代码时,跳转条件满足,流程跳到ret_with_reschedule,一直 到[call]schedule,因为schedule是函数调用,所以会在内核堆栈中构造返回地址,调整%ebp, %esp。这样,在schedule返回时,pc能跳到返回地址【call SYMBOL_NAME(schedule)的下一条指 令】,内核堆栈恢复到调用schedule之前的状态,流程回到call SYMBOL_NAME(schedule)的下一 条指令,跳来跳出,最后会跳到restore_all,恢复中断现场。 */
内核堆栈的切换由内联汇编代码switch_to完成。
参考《深入理解Linux内核》中文第三版.第三章和第七章
static inline task_t * context_switch(runqueue_t *rq, task_t *prev, task_t *next) { struct mm_struct *mm = next->mm; struct mm_struct *oldmm = prev->active_mm; /* 内核进程的task_struct->mm为NULL, task_struct->active_mm指向之前进程的active_mm 用户进程的task_struct->mm不为空,task_struct->active_mm指向自己的mm 如果next是一个内核进程,那么他就是用prev的active_mm,不进行cr3的切换,使用prev进程 的页目录。 如果next是一个用户进程,那么他需要将cr3切换,使cr3寄存器指向自己的页目录 */ /* 可以去include\asm-i386\mmu_context.h中查看enter_lazy_tlb函数和switch_mm函数 */ if (unlikely(!mm)) { //next是一个内核进程 next->active_mm = oldmm; atomic_inc(&oldmm->mm_count); enter_lazy_tlb(oldmm, next); } else //next是一个用户进程 switch_mm(oldmm, mm, next); /* static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk) ... //Re-load page tables load_cr3(next->pgd); //切换cr3 ... } */ ... /* Here we just switch the register state and the stack. */ switch_to(prev, next, prev); //完成内核堆栈的切换 return prev; }
#define switch_to(prev,next,last) do { \ unsigned long esi,edi; \ asm volatile("pushfl\n\t" \ "pushl %%ebp\n\t" \ "movl %%esp,%0\n\t" /* save ESP */ \ "movl %5,%%esp\n\t" /* restore ESP */ \ "movl $1f,%1\n\t" /* save EIP */ \ "pushl %6\n\t" /* restore EIP */ \ "jmp __switch_to\n" \ "1:\t" \ "popl %%ebp\n\t"