一、疑问
进程调度时,当被选中的next进程不是current进程时,需要进行上下文切换。
进行上下文切换时,有一些问题不太容易理解,比如:
1、进程上下文切换必然发生在内核态吗?
2、上下文切换后原来的进程(prev)如果恢复执行,从什么地方开始执行?
3、上下文切换后,如何切换到新进程执行?新进程从什么地方开始执行?
5、上下文切换时,堆栈如何切换,如果保证不混乱?
6、A进程执行时被打断调度B进程运行,B进程正常执行过程中被打断调度C进程运行,C运行被打断中调度D运行,以此类推,看似一个无限嵌套,如何恢复到A进程运行,不会一层层返回吧?会不会有问题?
7、上下文切换后,如何恢复到新进程的用户态程序继续执行?
上述问题(可能还有其它疑问~)在理解了进程上下文切换的细节后,就都能回答了。
二、原理
进程上下文切换设计到几个关键的地方,也正是上述疑问所在的地方:
1、进程调度必然经过schedule函数,显然必然发生内核态,那上下文切换也必然发生于内核态了。进程调度通常的时机有:
1)中断/异常/系统调用返回
2)其它,如wakeup()或手工调用schedule
在没有开启内核抢占的环境中(通常如此),仅当被替换进程(prev)处于位于用户态时,才能发生调度(上下文切换)。
呵呵,看似跟“进程调度必然发生内核态”的说法是矛盾的,其实不然,这里的意思是,在prev进程被打断之前,其位于用户态,当其被打断之后(最常见的如时钟中断),当然就进入内核态了,然后在内核态完成进度调度和上下文切换。
2、当进程被打断(比如中断)时,当前的上下文信息(包括eip、CS和其它寄存器信息)会保存在当前的内核栈(或中断栈)中,当中断返回时,如果没有发生调度(不满足调度条件),会恢复之前的上下文信息,即恢复到之前的被打断之前的状态继续执行。(在entry_xx.S的汇编代码中实现)。
3、当进程被打断并产生调度时,最终会进入switch_to宏进行上下文切换,被替换的进程(prev)当前的IP指针会被替换为“标号1(__switch_to函数后的一行代码)”,并被保存在task_struct.thread.ip中,同时会将被选中将执行的进程(next)的ip、堆栈指针已经相关的上下文加载到当前环境中,实现新进程的调度执行。
而当原来的prev进程重新被调度执行时,由于之前保存的IP指针为“标号1”,所以会从“标号1”开始执行,具体见后面的代码分析。
4、新进程(next)的执行分两种情况:
1)经过调度后
经过调度后,会经历switch_to的流程,那么在进程被调度出去时,会保存switch_to宏中的“标号1”到task_struct.thread.ip中,当该进程被重新调度时,过程如3中描述一样,也会从switch_to宏中的“标号1”处开始执行。
2)fork创建之后未经过调度
此时,该进程未经历switch_to的流程,由于在fork时,会将新进程的thread.eip设置成ret_from_fork(参见copy_thread函数),所以此时该进程会从ret_from_fork处(在entry_xx.S的汇编代码中)开始执行。
5、堆栈的具体切换见另一篇文章:kernel 3.10内核源码分析--内核栈及堆栈切换
6、上下文切换后,由于原来的上下文完全被新上下文替换,所以新进程开始执行后,就已经没有原进程的遗留信息后,此时新进程用的是自己的地址空间、堆栈、和其它上下文,原进程被调度出去后,就跟现在的上下文脱离关系了。所以,不存在嵌套的说法,没有问题。
7、如之前所说,进程被中断时,其EIP和CS会自动保存在当前进程的内核栈(或中断栈)中,当新进程被调度执行时,其内核栈(或中断栈)中同样保存之前被调度出去时压入的EIP和CS,此时硬件会自动从内核栈中弹出EIP和CS,并将堆栈切换到用户栈,并恢复到用户态执行。
三、代码分析
进行上下文切换,主要由switch_to宏实现,代码分析如下:
-
/*
-
* 上下文切换,在schedule中调用,current进程调度出去,当该进程被再次调度到时,重新从__switch_to后面开始执行
-
* prev:被替换的进程
-
* next:被调度的新进程
-
* last:当切换回原来的进程(prev)后,被替换的另外一个进程。
-
*/
-
#define switch_to(prev, next, last) \
-
do { \
-
/* \
-
* Context-switching clobbers all registers, so we clobber \
-
* them explicitly, via unused output variables. \
-
* (EAX and EBP is not listed because EBP is saved/restored \
-
* explicitly for wchan access and EAX is the return value of \
-
* __switch_to()) \
-
*/ \
-
unsigned long ebx, ecx, edx, esi, edi; \
-
\
-
asm volatile("pushfl\n\t" /* save flags */ /*将eflags寄存器值压栈*/\
-
"pushl %%ebp\n\t" /* save EBP */ /*将EBP压栈*/\
-
/*将当前栈指针(内核态)保存到prev进程的thread.sp中*/
-
"movl %%esp,%[prev_sp]\n\t" /* save ESP */ \
-
/*将next进程的栈指针(内核态)装载到ESP寄存器中*/
-
"movl %[next_sp],%%esp\n\t" /* restore ESP */ \
-
/*保存"标号1"的地址到prev进程的thread.ip,以便当prev进程重新被调度运行时,可以从"标号1处"重新开始执行*/
-
"movl $1f,%[prev_ip]\n\t" /* save EIP */ \
-
/*
-
* 将next进程的IP(通常都是"标号1"的地址,因为通常都是经历过这里的调度过程的,上一行代码中即保存了这个IP)
-
* 压入当前的(即next进程的)堆栈中。结合后面的jmp指令(注意:不是call指令)一起理解,当__switch_to执行完ret返回时,
-
* 会自动从当前的堆栈中弹出该地址作为函数的返回地址接着执行,如此即可实现新进程的运行。
-
*/
-
"pushl %[next_ip]\n\t" /* restore EIP */ \
-
__switch_canary \
-
/*
-
*jmp到__switch_to函数执行,当此函数返回时,自动跳转到[next_ip]开始执行,实现新进程的调度。注意不是call,jmp指令
-
* 不会自动将当前地址压栈,call会自动压栈
-
*/
-
"jmp __switch_to\n" /* regparm call */ \
-
/*当prev进程再次被调度到时,从这里开始执行*/
-
"1:\t" \
-
/*恢复EBP*/
-
"popl %%ebp\n\t" /* restore EBP */ \
-
/*恢复eflags*/
-
"popfl\n" /* restore flags */ \
-
\
-
/* output parameters */ \
-
/*输出参数*/
-
: [prev_sp] "=m" (prev->thread.sp), \
-
[prev_ip] "=m" (prev->thread.ip), \
-
"=a" (last), \
-
\
-
/* clobbered output registers: */ \
-
"=b" (ebx), "=c" (ecx), "=d" (edx), \
-
"=S" (esi), "=D" (edi) \
-
\
-
__switch_canary_oparam \
-
\
-
/* input parameters: */ \
-
/*输入参数*/
-
: [next_sp] "m" (next->thread.sp), \
-
[next_ip] "m" (next->thread.ip), \
-
\
-
/* regparm parameters for __switch_to(): */ \
-
/*将prev和next分别存入ecx和edx,然后作为参数传入到__switch_to函数中*/
-
[prev] "a" (prev), \
-
[next] "d" (next) \
-
\
-
__switch_canary_iparam \
-
\
-
: /* reloaded segment registers */ \
-
"memory"); \
-
} while (0)
__switch_to函数实现如下:
-
/*入参通过寄存器eax和edx从switch_to宏中传入*/
-
__notrace_funcgraph struct task_struct *
-
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
-
{
-
/*取prev进程的上下文信息*/
-
struct thread_struct *prev = &prev_p->thread,
-
*next = &next_p->thread;
-
/*取当前CPU*/
-
int cpu = smp_processor_id();
-
/*获取当前CPU的TSS对应的tss_struct*/
-
struct tss_struct *tss = &per_cpu(init_tss, cpu);
-
fpu_switch_t fpu;
-
-
-
/* never put a printk in __switch_to... printk() calls wake_up*() indirectly */
-
-
-
fpu = switch_fpu_prepare(prev_p, next_p, cpu);
-
-
-
/*
-
* Reload esp0.
-
*/
-
/*
-
* 由于Linux的具体实现中,TSS不是针对每进程,而是针对每CPU的,即每个CPU对应一个tss_struct,那在进程上下文切换时,
-
* 需要考虑当前CPU上TSS中的内容的更新,其实就是内核栈指针的更新,更新后,当新进程再次进入到内核态执行时,
-
* 才能确保CPU硬件能从TSS中自动读取到正确的内核栈指针(sp0)的值,以保证从用户态切换到内核态时,相应的堆栈切
-
* 换正常。
-
*/
-
/*将next进程的内核栈指针(next->thread->sp0)值更新到当前CPU的TSS中*/
-
load_sp0(tss, next);
-
-
-
/*
-
* Save away %gs. No need to save %fs, as it was saved on the
-
* stack on entry. No need to save %es and %ds, as those are
-
* always kernel segments while inside the kernel. Doing this
-
* before setting the new TLS descriptors avoids the situation
-
* where we temporarily have non-reloadable segments in %fs
-
* and %gs. This could be an issue if the NMI handler ever
-
* used %fs or %gs (it does not today), or if the kernel is
-
* running inside of a hypervisor layer.
-
*/
-
lazy_save_gs(prev->gs);
-
-
-
/*
-
* Load the per-thread Thread-Local Storage descriptor.
-
*/
-
/*
-
* 将next_p进程使用的线程局部存储(TLS)段装入本地CPU的全局描述符表.
-
*/
-
load_TLS(next, cpu);
-
-
-
/*
-
* Restore IOPL if needed. In normal use, the flags restore
-
* in the switch assembly will handle this. But if the kernel
-
* is running virtualized at a non-zero CPL, the popf will
-
* not restore flags, so it must be done in a separate step.
-
*/
-
if (get_kernel_rpl() && unlikely(prev->iopl != next->iopl))
-
set_iopl_mask(next->iopl);
-
-
-
/*
-
* Now maybe handle debug registers and/or IO bitmaps
-
*/
-
if (unlikely(task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV ||
-
task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT))
-
__switch_to_xtra(prev_p, next_p, tss);
-
-
-
/*
-
* Leave lazy mode, flushing any hypercalls made here.
-
* This must be done before restoring TLS segments so
-
* the GDT and LDT are properly updated, and must be
-
* done before math_state_restore, so the TS bit is up
-
* to date.
-
*/
-
/*架构相关处理,半虚拟化中使用*/
-
arch_end_context_switch(next_p);
-
-
-
/*
-
* Restore %gs if needed (which is common)
-
*/
-
if (prev->gs | next->gs)
-
lazy_load_gs(next->gs);
-
-
-
switch_fpu_finish(next_p, fpu);
-
/*将current_task per-CPU变量值更新为next进程信息*/
-
this_cpu_write(current_task, next_p);
-
/*
-
* 这里需要仔细理解。return到哪里?
-
* switch_to宏中,jmp到__switch_to函数之前将"next_ip"压入了当前堆栈,那通常情况下,这里return后,
-
* 会自动从堆栈中弹出next_ip开始执行,而next_ip通常为switch_to宏中保存的"标号1"的地址,即
-
* 这里通常会返回到switch_to宏中__switch_to函数之后的标号1处开始执行。
-
* 但有例外:对于没有产生过进程切换,而是第一次开始执行的进程(刚完成fork开始执行)来说.
-
* 由于没有通过switch_to宏保存next_ip,所以并不会跳回switch_to,而是跳转到ret_from_fork函数的超始
-
* 地址开始执行,因为在fork新进程时,即设置好了该进程的thread.eip设置成了ret_from_fork(参见
-
* copy_thread函数)。
-
*/
-
return prev_p;
-
}
第一次开始执行的进程的thread.eip设置点:
-
do_fork->copy_process->copy_thread
-
int copy_thread(unsigned long clone_flags, unsigned long sp,
-
unsigned long arg, struct task_struct *p)
-
{
-
struct pt_regs *childregs = task_pt_regs(p);
-
struct task_struct *tsk;
-
int err;
-
-
-
p->thread.sp = (unsigned long) childregs;
-
p->thread.sp0 = (unsigned long) (childregs+1);
-
/*内核线程单独处理,其上下文信息单独填写*/
-
if (unlikely(p->flags & PF_KTHREAD)) {
-
/* kernel thread */
-
memset(childregs, 0, sizeof(struct pt_regs));
-
p->thread.ip = (unsigned long) ret_from_kernel_thread;
-
task_user_gs(p) = __KERNEL_STACK_CANARY;
-
childregs->ds = __USER_DS;
-
childregs->es = __USER_DS;
-
childregs->fs = __KERNEL_PERCPU;
-
childregs->bx = sp; /* function */
-
childregs->bp = arg;
-
childregs->orig_ax = -1;
-
childregs->cs = __KERNEL_CS | get_kernel_rpl();
-
childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_BIT1;
-
p->fpu_counter = 0;
-
p->thread.io_bitmap_ptr = NULL;
-
memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));
-
return 0;
-
}
-
/*将当前进程(父进程)的寄存器上下文信息赋给子进程,即子进程此后的上下文信息跟父进程保持一致了。*/
-
*childregs = *current_pt_regs();
-
childregs->ax = 0;
-
if (sp)
-
childregs->sp = sp;
-
/*
-
* 子进程的IP指向ret_from_fork,fork创建的新进程,都要经历这个过程,在调度的上下文切换时,
-
* 其返回到ret_from_fork(entry_32.S汇编代码)中处理,这跟普通进程调度时上下文切换不一样,普通
-
* 进程的IP是在上次上下文切换时(switch_to)中保存的。
-
*/
-
p->thread.ip = (unsigned long) ret_from_fork;
-
task_user_gs(p) = get_user_gs(current_pt_regs());
-
-
-
p->fpu_counter = 0;
-
p->thread.io_bitmap_ptr = NULL;
-
tsk = current;
-
err = -ENOMEM;
-
-
-
memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));
-
-
-
if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) {
-
p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr,
-
IO_BITMAP_BYTES, GFP_KERNEL);
-
if (!p->thread.io_bitmap_ptr) {
-
p->thread.io_bitmap_max = 0;
-
return -ENOMEM;
-
}
-
set_tsk_thread_flag(p, TIF_IO_BITMAP);
-
}
-
-
-
err = 0;
-
-
-
/*
-
* Set a new TLS for the child thread?
-
*/
-
if (clone_flags & CLONE_SETTLS)
-
err = do_set_thread_area(p, -1,
-
(struct user_desc __user *)childregs->si, 0);
-
-
-
if (err && p->thread.io_bitmap_ptr) {
-
kfree(p->thread.io_bitmap_ptr);
-
p->thread.io_bitmap_max = 0;
-
}
-
return err;
-
}
阅读(6397) | 评论(0) | 转发(2) |