深入理解Linux内核之主调度器(下)
扫描二维码
随时随地手机看文章
4.进程上下文切换
接前文:深入理解Linux内核之主调度器(上)前面选择了一个合适进程作为下一个进程,接下来做重要的上下文切换动作,来保存上一个进程的“上下文”恢复下一个进程的“上下文”,主要包括进程地址空间切换和处理器状态切换。注:这里的上下文实际上是指进程运行时最小寄存器的集合。如果切换的next进程不是同一个进程,才进行切换:
__schedule
i f (likely(prev != next)) {
...
context_switch //进程上下文切换
}
4.1 进程地址空间切换
进程地址空间切换就是切换虚拟地址空间,使得切换之后,当前进程访问的是属于自己的虚拟地址空间(包括用户地址空间和内核地址空间),本质上是切换页表基地址寄存器。进程地址空间切换让进程产生独占系统内存的错觉,因为切换完地址空间后,当前进程可以访问属于它的海量的虚拟地址空间(内核地址空间各个进程共享,用户地址空间各个进程私有),而实际上物理地址空间只有一份。下面给出源代码分析:context_switch
->
/*
¦* kernel -> kernel lazy transfer active
¦* user -> kernel lazy mmgrab() active
¦*
¦* kernel -> user switch mmdrop() active
¦* user -> user switch
¦*/
if (!next->mm) { // to kernel
enter_lazy_tlb(prev->active_mm, next);
next->active_mm = prev->active_mm;
if (prev->mm) // from user
mmgrab(prev->active_mm);
else
prev->active_mm = NULL;
} else { // to user
...
switch_mm_irqs_off(prev->active_mm, next->mm, next);
if (!prev->mm) { // from kernel
/* will mmdrop() in finish_task_switch(). */
rq->prev_mm = prev->active_mm;
prev->active_mm = NULL;
}
}
以上代码是判断是否next进程是内核线程,如果是则不需要进行地址空间切换(实际上指的是用户地址空间),因为内核线程总是运行在内核态访问的是内核地址空间,而内核地址空间是所有的进程共享的。在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按理来说需要做地址空间切换,但是由于切换的还是Ua 地址空间,所以也不需要真正的切换(判断了Ka->active_mm == Ua->active_mm ),当然还包括切换的是同一个进程的多个线程的情况,这留给大家思考。下面来看下真正的地址空间切换: 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)
-> ... //asid处理
-> cpu_switch_mm(mm->pgd, mm)
->cpu_do_switch_mm(virt_to_phys(pgd),mm)
-> unsigned long ttbr1 = read_sysreg(ttbr1_el1);
unsigned long asid = ASID(mm);
unsigned long ttbr0 = phys_to_ttbr(pgd_phys);
...
write_sysreg(ttbr1, ttbr1_el1); //设置asid到ttbr1_el1
isb();
write_sysreg(ttbr0, ttbr0_el1); //设置mm->pgd 到ttbr0_el1
上面代码是做真正的地址空间切换,实际的切换很简单,并没有那么复杂和玄乎,仅仅设置页表基地址寄存器即可,当然这里还涉及到了为了防止频繁无效tlb的ASID的设置。主要做的工作就是设置next进程的ASID到ttbr1_el1, 设置mm->pgd 到ttbr0_el1,仅此而已!需要注意的是:1.写到ttbr0_el1的值是进程pgd页表的物理地址。2.虽然做了这样的切换,但是这个时候并不能访问到next的用户地址空间,因为还处在主调度器上下文中,属于内核态,访问的是内核空间。而一旦返回了用户态,next进程就能正常访问自己地址空间内容:- 访问一个用户空间的虚拟地址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);
处理器状态切换会做浮点寄存器等切换,最终调用cpu_switch_to做真正切换。cpu_switch_to //arch/arm64/kernel/entry.S
SYM_FUNC_START(cpu_switch_to)
mov x10, #THREAD_CPU_CONTEXT
add x8, x0, x10
mov x9, sp
stp x19, x20, [x8], #16 // store callee-saved registers
stp x21, x22, [x8], #16
stp x23, x24, [x8], #16
stp x25, x26, [x8], #16
stp x27, x28, [x8], #16
stp x29, x9, [x8], #16
str lr, [x8]
add x8, x1, x10
ldp x19, x20, [x8], #16 // restore callee-saved registers
ldp x21, x22, [x8], #16
ldp x23, x24, [x8], #16
ldp x25, x26, [x8], #16
ldp x27, x28, [x8], #16
ldp x29, x9, [x8], #16
ldr lr, [x8]
mov sp, x9
msr sp_el0, x1
ptrauth_keys_install_kernel x1, x8, x9, x10
scs_save x0, x8
scs_load x1, x8
ret
SYM_FUNC_END(cpu_switch_to)
这里传递过来的是x0为prev进程的进程描述符(struct task_struct)地址, x1为next的进程描述符地址。会就将prev进程的 x19-x28,fp,sp,lr保存到prev进程的tsk.thread.cpu_context中,next进程的这些寄存器值从next进程的tsk.thread.cpu_context中恢复到相应寄存器。这里还做了sp_el0设置为next进程描述符的操作,为了通过current宏找到当前的任务。需要注意的是:- mov sp, x9 做了切换进程内核栈的操作。
- ldr lr, [x8] 设置了链接寄存器,然后ret的时候会将lr恢复到pc从而真正完成了执行流的切换。
4.3 精美图示
这里给出了进程切换的图示(以arm64处理器为例),这里从prev进程切换到next进程。5.进程再次被调度
当进程重新被调度的时候,从原来的调度现场恢复执行。5.1 关于lr地址的设置
1)如果切换的next进程是刚fork的进程,它并没有真正的这些调度上下文的存在,那么lr是什么呢?这是在fork的时候设置的:do_fork
...
copy_thread //arch/arm64/kernel/process.c
->memset(