- 了解进程切换流程;
1.context_switch代码分析
context_switch函数用来完成具体的进程切换。代码调用流程:
kernel/sched/core.c:
schedule()
->__schedule(false);
->context_switch
context_switch函数分析:
static inline struct rq * context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)------------------(1)
{
struct mm_struct *mm, *oldmm;
mm = next->mm;
oldmm = prev->active_mm;-------------------(2)
/* 如果next是内核线程,则线程使用prev所使用的地址空间
* schedule( )函数把该线程设置为懒惰TLB模式
* 内核线程并不拥有自己的页表集(task_struct->mm = NULL)
* 它使用一个普通进程的页表集
* 不过,没有必要使一个用户态线性地址对应的TLB表项无效
* 因为内核线程不访问用户态地址空间。
*/
if (!mm) {---------------------------(3)
/* 内核线程的active_mm为上一个进程的mm
* 注意此时如果prev也是内核线程,
* 则oldmm为NULL, 即next->active_mm也为NULL */
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count); /* 增加mm的引用计数 */
/* 通知底层体系结构不需要切换虚拟地址空间的用户部分
* 这种加速上下文切换的技术称为惰性TBL */
enter_lazy_tlb(oldmm, next);-----------------(4)
} else
/* 不是内核线程, 则需要切切换虚拟地址空间 */
switch_mm(oldmm, mm, next); ---------------(5)
/* 如果prev是内核线程或正在退出的进程
* 就重新设置prev->active_mm
* 然后把指向prev内存描述符的指针保存到运行队列的prev_mm字段中
*/
if (!prev->mm) {------------------------(6)
prev->active_mm = NULL;
rq->prev_mm = oldmm;
}
switch_to(prev, next, prev);------------------(7)
/* switch_to之后的代码只有在
* 当前进程再次被选择运行(恢复执行)时才会运行
* 而此时当前进程恢复执行时的上一个进程可能跟参数传入时的prev不同
* 甚至可能是系统中任意一个随机的进程
* 因此switch_to通过第三个参数将此进程返回
*/
/* 路障同步, 一般用编译器指令实现
* 确保了switch_to和finish_task_switch的执行顺序
* 不会因为任何可能的优化而改变 */
barrier();
/* 进程切换之后的处理工作 */
return finish_task_switch(prev);
}
-
(1) 一旦调度器算法确定了pre task和next task,那么就可以调用context_switch函数实际执行进行切换的工作了,参数传递情况:
- rq:在多核系统中,进程切换总是发生在各个cpu core上,参数rq指向本次切换发生的那个cpu对应的run queue
- prev:将要被剥夺执行权利的那个进程
- next:被选择在该cpu上执行的那个进程
-
(2) next是马上就要被切入的进程(简称B进程),prev是马上就要被剥夺执行权利的进程(简称A进程)。mm变量指向B进程的地址空间描述符,oldmm变量指向A进程的当前正在使用的地址空间描述符(active_mm)。
- 对于normal进程,其task_struct的mm和active_mm相同,都是指向其进程地址空间。
- 对于内核线程而言,其task_struct的mm成员为NULL(内核线程没有进程地址空间),但是,内核线程被调度执行的时候,总是需要一个进程地址空间,而active_mm就是指向它借用的那个进程地址空间。
-
(3)如果mm为空,说明B进程是内核线程,这时只能借用A进程当前正在使用的那个地址空间(prev->active_mm)。注意:这里不能借用A进程的地址空间(prev->mm),因为A进程也可能是一个内核线程,不拥有自己的地址空间描述符。
-
(4)如果要切入的B进程是内核线程,那么调用体系结构相关的代码enter_lazy_tlb,标识该cpu进入lazy tlb mode。如果要切入的进程实际上是内核线程,那么暂时不需要flush TLB,因为内核线程不会访问usersapce,所以那些无效的TLB entry也不会影响内核线程的执行。在这种情况下,为了性能,会进入lazy tlb mode。
-
(5)如果要切入的B进程是内核线程,那么由于是借用当前正在使用的地址空间,因此没有必要调用switch_mm进行地址空间切换,只有要切入的B进程是一个普通进程的情况下(有自己的地址空间)才会调用switch_mm,真正执行地址空间切换。
-
(6)如果切出的A进程是内核线程,那么其借用的那个地址空间(active_mm)已经不需要继续使用了(内核线程A被挂起了,根本不需要地址空间了)。除此之外,还设定了run queue上一次使用的mm struct(rq->prev_mm)为oldmm。
-
(7)一次进程切换,表面上看起来涉及两个进程,实际上涉及到了三个进程。switch_to和一般的调用函数不同,当A进程在CPUa调用它切换到B进程的时候,switch_to一去不回,直到在某个cpu上(称之CPUx)完成从X进程(就是last进程)到A进程切换的时候,switch_to返回到A进程的现场。switch_to完成了具体prev到next进程的切换,当switch_to返回的时候,说明A进程再次被调度执行了。
2.ARM64的进程地址空间切换
对于ARM64这个cpu arch,每一个cpu core都有两个寄存器来指示当前运行在该CPU core上的进程(线程)实体的地址空间。这两个寄存器分别是ttbr0_el1(用户地址空间)和ttbr1_el1(内核地址空间)。由于所有的进程共享内核地址空间,因此所谓地址空间切换也就是切换ttbr0_el1而已。每一个进程都有自己独立的一组用于翻译用户空间虚拟地址的Translation table,这些信息保存在内存描述符中,具体位于struct mm_struct中的pgd成员中。以pgd为起点,可以遍历该内存描述符的所有用户地址空间的Translation table。具体代码如下:
static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next,
struct task_struct *tsk)----------------(1)
{
unsigned int cpu = smp_processor_id();
if (prev == next)--------------------(2)
return;
if (next == &init_mm) {-----------------(3)
cpu_set_reserved_ttbr0();
return;
}
check_and_switch_context(next, cpu);
}
-
(1)prev是要切出的地址空间,next是要切入的地址空间,tsk是将要切入的进程。
-
(2)要切出的地址空间和要切入的地址空间是一个地址空间的话,那么切换地址空间也就没有什么意义了。
-
(3)在ARM64中,地址空间的切换主要是切换ttbr0_el1,对于swapper进程的地址空间,其用户空间没有任何的mapping,而如果要切入的地址空间就是swapper进程的地址空间的时候,将(设定ttbr0_el1指向empty_zero_page)。
-
(4)check_and_switch_context中有很多TLB、ASID相关的操作,最终该函数会调用arch/arm64/mm/proc.S文件中的cpu_do_switch_mm将要切入进程的L0 Translation table物理地址(保存在内存描述符的pgd成员)写入ttbr0_el1。
refer to
- https://blog.csdn.net/gatieme/article/details/51872659
- http://www.wowotech.net/process_management/context-switch-arch.html
- http://www.wowotech.net/process_management/context-switch-tlb.html