本文主要记录S3C6410/ARM1176JZF-S架构下Linux(kernel 2.6.35)内核如何进行进程切换。
进程切换是操作系统进程调度的基础,首先要能够实现切换,接下来才谈得上“多进程”、“多线程”以及调度算法等更高级的话题。(这里在说“进程切换”的时候提到多线程,并不是把概念搞混淆了。在内核里谈切换的时候,Linux并不区分进程与线程,因为这里只有task,一个进程里如果有多个线程,每一个都是一个task。内核实际上切换的就是task。所以,来自同一个进程的不同线程的task和来自不同进程的task对于内核来说并没有区别。)
Linux进程切换的核心代码是函数context_switch(),此函数的骨干内容如下:
static inline void
context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next)
{
switch_mm(oldmm, mm, next);
switch_to(prev, next, prev);
}
#define switch_to(prev,next,last) \
do { \
last = __switch_to(prev,task_thread_info(prev), task_thread_info(next)); \
} while (0)
其中prev是当前进程/切出进程的task_struct指针,next是下一进程/切入进程的task_struct指针。context_switch()主要做两件事情,一件是切换页表,另一件是切换进程上下文。分别由一个函数来实现,下面分别讲解。
switch_mm()的作用是切换切换进程的页表,要做的最重要的事情就是把下一进程的二级页表地址pgd(物理地址)设置到CPU的CP15控制器。进程的页表pgd可以分为两部分来看,0~3G空间部分是用户空间,采用二级映射,每个进程各不相同;3G~4G空间部分是内核空间,采用一级映射,每个进程都相同,其实每个进程的这一块页表内容都是从内核的页表拷贝来的。切换页表的主要目的是切换用户空间的页表,内核空间部分都一样,不需要切换。所以,如果next是一个内核线程的话,并不会调用switch_mm()。
下面是经过简化的switch_mm()汇编代码:
@ r0 = pgd_phys, * r1 = context_id
@
ENTRY(cpu_v6_switch_mm)
mov r2, #0
orr r0, r0, #TTB_FLAGS_UP
mcr p15, 0, r2, c7, c5, 6 @ flush BTAC/BTB
mcr p15, 0, r2, c7, c10, 4 @ drain write buffer
mcr p15, 0, r0, c2, c0, 0 @ write Translation Table Base Register 0
mcr p15, 0, r1, c13, c0, 1 @ set context ID
mov pc, lr
ENDPROC(cpu_v6_switch_mm)
其中第8行是最核心的一行,它把pgd的值设置给CP15的C2寄存器,C2即是”Translation Table Base Register 0“(地址转换表基地址寄存器)。
switch_mm()调用完之后,用户空间的内容已经是新的进程了,但这时内核空间还属于老的进程,因为CPU还在老进程的内核栈上面运行。下面要做的就是赶紧把内核空间空间也切换到新进程中去,这就是switch_to()所要做的。
switch_to()的作用有两个:一是要把当前所运行的进程(切出进程)的现场(包括各个通用寄存器、SP和PC)保存好;二是切换到新进程(切入进程),即取出此前已保存的新进程的现场,并从上次保存的地方继续运行。注意,这里所说的的现场是内核空间的现场,用户空间的现场在中断刚刚发生时已经保存过。
下面是经过简化的switch_to()汇编代码:
@ r0 = previous task_struct, r1 = previous thread_info, r2 = next thread_info
@
ENTRY(__switch_to)
@ thread_info + TI_CPU_SAVE hold saved cpu context, registers value is stored
@ now ip hold the address of the context of previous process
add ip, r1, #TI_CPU_SAVE
@ now r3 hold TP value of next process
ldr r3, [r2, #TI_TP_VALUE]
@ store current regs to prev thread_info
stmia ip!, {r4 - sl, fp, sp, lr} @ Store most regs on
@ store CPU_DOMAIN of next to r6
ldr r6, [r2, #TI_CPU_DOMAIN]
@ set tp value and domain to cp15
mcr p15, 0, r3, c13, c0, 3 @ yes, set TLS register
mcr p15, 0, r6, c3, c0, 0 @ Set domain register
@ now r4 hold the address of the next context
add r4, r2, #TI_CPU_SAVE
@ put next context to registers
ldmia r4, {r4 - sl, fp, sp, pc} @ Load all regs saved previously
ENDPROC(__switch_to)
我为每一行都加了注释,应该比较容易理解了。
其中第10行和第19行是比较核心的代码,它们分别是保存当前cpu context以及恢复上一次保存的cpu context。这里所说的”上一次“指的是当前进程在上一次处于内核态的时候,当时在离开内核态(切出)的时候,保存了现场。
这里所说的cpu context是由结构体cpu_context所表示的,内容如下。
struct cpu_context_save {
__u32 r4;
__u32 r5;
__u32 r6;
__u32 r7;
__u32 r8;
__u32 r9;
__u32 sl;
__u32 fp;
__u32 sp;
__u32 pc;
__u32 extra[2];
};
在switch_to()的第10行,当前正在运行的SVC模式下的各寄存器(包括r4-r9, sp, lr等等)都被保存起来。
在switch_to()的第19行,r4指向的是下一进程的cpu_context结构地址,这一行执行完后,cpu context中所保存的内容就被读进各个寄存器,sp和pc都被更新,现在CPU已经不在刚刚的那个内核栈上了。
第10行和第19行的寄存器列表有一处区别:第10行的最后一个寄存器是lr,即调用__switch_to()的返回地址;而第19行的最后一个寄存器是pc。这就是说,在切换的时候,当前进程在切回来的时候会从__switch_to()的下一条指令开始执行,这正是内核所需要的。