本文转载自微信公众号《LinuxKernelVoyager》,作者LinuxKernelVoyager。转载本文请联系Linux内核航海者公众号。4、进程上下文切换之前选择一个合适的进程作为下一个进程,然后做重要的上下文切换动作,保存上一个进程的“上下文”和恢复下一个进程的“上下文”,主要包括进程地址空间切换和处理器状态开关。注意:这里的context其实是指进程运行时最小寄存器的集合。如果切换的下一个进程不是同一个进程,switch:__scheduleif(likely(prev!=next)){...context_switch//进程上下文切换}4.1进程地址空间切换进程地址空间切换就是切换虚拟地址空间,切换后,当前进程访问自己的虚拟地址空间(包括用户地址空间和内核地址空间),本质上是切换了页表基地址寄存器。进程地址空间切换使进程有独占系统内存的错觉,因为切换地址空间后,当前进程可以访问其海量的虚拟地址空间(内核地址空间为各进程共享,用户地址空间为每个进程私有),而实际上层物理地址空间只有一份。源码分析如下:context_switch->/*|*kernel->kernellazy+transferactive|*user->kernellazy+mmgrab()active|*|*kernel->userswitch+mmdrop()active|*user->userswitch|*/if(!next->mm){//tokernelenter_lazy_tlb(prev->active_mm,next);next->active_mm=prev->active_mm;if(prev->mm)//fromusermmgrab(prev->active_mm));elseprev->active_mm=NULL;}else{//touser...switch_mm_irqs_off(prev->active_mm,next->mm,next);if(!prev->mm){//fromkernel/*willmmdrop()infinish_task_switch().*/rq->prev_mm=prev->active_mm;prev->active_mm=NULL;}}上面的代码是判断下一个进程是否是内核线程,如果是就不用切换地址空间(实际上是指User地址空间),因为内核线程始终运行在内核态并访问内核地址空间,而内核地址空间是所有进程共享的。在arm64架构中,通过ttbr1_el1访问内核地址空间,其主内核页表在内核初始化时已经被填充,也就是我们常说的swapper_pg_dir页表,后续所有对内核地址空间的访问,无论是内核线程还是用户任务,都是通过swapper_pg_dir页表访问的,而swapper_pg_dir页表地址在内核初始化时已经加载到ttbr1_el1中。需要说明一下:这里会“借用”prev->active_mm处理,借用的目的是避免切换属于同一个进程的地址空间。例如:Ua->Ka->Ua,Ua代表用户进程,Ka代表内核线程,在进行这样的切换时,Ka借用了Ua地址空间,Ua->Ka不需要做地址空间切换,而Ka->理论上Ua需要switch地址空间,但是由于switch还是Ua地址空间,所以没有真正的switch(判断Ka->active_mm==Ua->active_mm),当然switch是同一个进程中多线程的情况下,这个留给大家去思考。再来看真正的地址空间切换:switch_mm_irqs_off(prev->active_mm,next->mm,next);->switch_mm//arch/arm64/include/asm/mmu_context.h->if(prev!=next)__switch_mm(next);->check_and_switch_context(next)->...//asidprocessing->cpu_switch_mm(mm->pgd,mm)->cpu_do_switch_mm(virt_to_phys(pgd),mm)->unsignedlongttbr1=read_sysreg(ttbr1_el1);unsignedlongasid=ASID(mm);unsignedlongttbr0=phys_to_ttbr(pgd_phys);...write_sysreg(ttbr1,ttbr1_el1);//设置为ttbr1_el1isb();write_sysreg(ttbr0,ttbr0_el1);//设置mm->pgd为ttbr0_el1上面的代码是真正的地址空间切换,实际的切换很简单,没有那么复杂和神秘,设置页表基址寄存器即可,当然还涉及到ASID的设置,防止频繁的无效tlb。主要工作就是设置下一个进程的ASID为ttbr1_el1,设置mm->pgd为ttbr0_el1,仅此而已!注:1、ttbr0_el1写入的值是进程pgd页表的物理地址。2.虽然做了这样的切换,但是此时不能访问下一个用户地址空间,因为它还在主调度器的上下文中,属于内核态,访问内核空间。一旦返回用户态,下一个进程就可以正常访问自己地址空间的内容了:要访问用户空间的一个虚拟地址va,首先通过va和ttbr1_el1中记录的asid查询tlb,如果找到对应的表项,它将获得pa进行访问。如果在tlb中没有找到,就用ttbr0_el1遍历自己的多级页表,找到对应的表项,就得到pa进行访问。如果对内核地址空间的访问发生中断异常等,可以直接通过ttbr1_el1完成访问。访问未建立页表映射的合法VA时,发生缺页异常建立映射关系,填写属于进程自身的各级页表,然后访问。地址无法访问,发生页面错误杀死进程等。4.2处理器状态切换切换下一个进程的执行流程,保存上一个进程的执行状态,恢复下一个进程的执行状态。处理器状态切换及后者使进程产生独占系统cpu的错觉,使系统中的每个任务可以并发(多个任务运行在多个cpu上)或分时复用(多个任务运行在一个cpu上)cpu资源。下面给出代码:context_switch->(last)=__switch_to((prev),(next))->fpsimd_thread_switch(next)//浮点寄存器切换...last=cpu_switch_to(prev,next);Processorstateswitching会做浮点寄存器的切换,最后调用cpu_switch_to做真正的切换。cpu_switch_to//arch/arm64/kernel/entry.SSYM_FUNC_START(cpu_switch_to)movx10,#THREAD_CPU_CONTEXTaddx8,x0,x10movx9,spstpx19,x20,[x8],#16//storecallee-savedregistersstpx21,x22,[x4x],#3,2stp复制代码,[x8],#16stpx25,x26,[x8],#16stpx27,x28,[x8],#16stpx29,x9,[x8],#16strlr,[x8]addx8,x1,x10ldpx19,x20,[x8],#16//restorecallee-savedregistersldpx21,x22,[x8],#16ldpx23,x24,[x8],#16ldpx25,x26,[x8],#16ldpx27,x28,[x8],#16ldpx29,x9,[x8],#16ldrlr,[x8]movsp,x9msrsp_el0,x1ptrauth_keys_install_kernelx1,x8,x9,x10scs_savex0,x8scs_loadx1,x8retSYM_FUNC_END(cpu_switch_to)这里x0是prev进程的进程描述符(structtask_struct)的地址,x1是下一个地址。它会将prev进程的x19-x28,fp,sp,lr保存到prev进程的tsk.thread.cpu_context中,而将next进程的这些寄存器值从tsk.thread中恢复到对应的寄存器中。下一个进程的cpu_context。这里也将sp_el0设置为下一个进程描述符,目的是为了通过current宏找到当前任务。需要注意的是:movsp,x9执行的是切换进程内核栈的操作。ldrlr,[x8]设置链接寄存器,然后ret时lr会恢复到pc中,从而真正完成执行流程的切换。4.3精美图下面是进程切换图(以arm64处理器为例),上一个进程切换到下一个进程。5.进程被重新调度当进程被重新调度时,从原来的调度站点恢复执行。5.1关于lr地址的设置1)如果被切换的下一个进程是刚刚fork出来的进程,那么它并不真正存在这些调度上下文,那么lr是什么?这是在fork期间设置的:do_fork...copy_thread//arch/arm64/kernel/process.c->memset(&p->thread.cpu_context,0,sizeof(structcpu_context));p->thread.cpu_context.pc=(unsignedlong)ret_from_fork;p->thread.cpu_context.sp=(unsignedlong)childregs;设置为ret_from_fork的地址,当然这里也设置了sp等调度上下文(这里进程切换保存的寄存器称为调度上下文)。SYM_CODE_START(ret_from_fork)blschedule_tailcbzx19,1f//notakernelthreadmovx0,x20blrx191:get_current_tasktskbret_to_userSYM_CODE_END(ret_from_fork)刚刚fork的进程从执行cpu_switch_to的ret指令返回,lr加载到pc。然后执行到ret_from_fork:这里先调用schedule_tail清理之前的进程,然后判断是否是内核线程,如果是内核线程的执行函数,如果是用户任务,通过返回用户态ret_to_user。2)如果是之前切换过的进程,lr就是cpu_switch_to调用的下一条指令的地址(这里其实就是__schedule函数中调用barrier()的指令地址)。5.2关于__switch_to的参数和返回值switch_to(prev,next,prev)>((last)=__switch_to((prev),(next)))这里处理器状态切换时,传递和返回两个参数一个参数:prev和next很好理解就是上一个进程(当前进程)和下一个进程的task_struct结构指针,那么last是什么?一句话:返回的last是当前重调度进程的上一个进程的task_struct结构体指针。例如:A->B->千山万水->D->A上面的切换过程:A切换到B然后经过千山万水再从D->A。此时,A重调度时,最后一个是D的task_struct结构体指针,获取当前重调度进程的上一个进程就是回收上一个进程的资源,见后面的分析。5.3当finish_task_switch进程被重新调度时,不管是不是新fork的进程,都会转到finish_task_switch函数。我们看看它做了什么:主要工作是:检查并回收上一个进程的资源,并为当前进程恢复执行做一些准备工作。finish_task_switch->finish_lock_switch->raw_spin_unlock_irq//使能本地中断->if(mm)mmdrop(mm)//借来的mm现在归还->if(unlikely(prev_state==TASK_DEAD)){//之前A进程isdeadput_task_stack(prev);//如果内核栈释放task_struct中的内核栈put_task_struct_rcu_user(prev);//释放上一个进程的task_struct占用的内存}可以看到当进程被重调度是的:重新启用本地中断,当进程被重调度时,本地cpu中断被重新打开!!!如果有借mm的情况,现在归还如果之前的是内核线程,进程地址空间切换时“借”的“某进程的mm_struct现在切换到下一个进程,应该归还.return是将借用的mm_struct的引用计数减1,如果引用计数为0,则释放mm_struct占用的内存。对于最后一个死进程,现在回收最后一个资源。注意这里引用计数是递减的,当引用计数到0时就会释放。6.总结主调度器可以说是Linux内核进程管理的核心组件,进程管理的其他部分诸如因为抢占、唤醒、睡眠等都是围绕它进行操作的。调度不能发生在原子上下文中,也就是调用主调度器,但是可以设置抢占标志,使得调度发生在最近的抢占点,比如中断时唤醒高优先级进程的场景。主调度器的工作就是让出CPU。内核中很多场景都可以直接或间接调用它,大致分为两种情况:主动调度和抢占式调度。主调度器做两件事:选择下一个进程和进程上下文切换。选择下一个进程解决了选择合适的高优先级进程的问题。进程上下文切换又分为地址空间切换和处理器状态切换。前者让进程有单独占用系统内存的错觉,后者让进程有单独占用系统cpu的错觉,让系统的各个进程有条不紊地共享内存和cpu。资源。
