Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1210662
  • 博文数量: 122
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 4002
  • 用 户 组: 普通用户
  • 注册时间: 2014-02-20 08:27
文章分类
文章存档

2016年(1)

2015年(21)

2014年(100)

分类: LINUX

2015-01-05 16:50:08

一、疑问
进程调度时,当被选中的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宏实现,代码分析如下:

点击(此处)折叠或打开

  1. /*
  2.   * 上下文切换,在schedule中调用,current进程调度出去,当该进程被再次调度到时,重新从__switch_to后面开始执行
  3.   * prev:被替换的进程
  4.   * next:被调度的新进程
  5.   * last:当切换回原来的进程(prev)后,被替换的另外一个进程。
  6.   */
  7. #define switch_to(prev, next, last)    \
  8. do {    \
  9. /*    \
  10. * Context-switching clobbers all registers, so we clobber    \
  11. * them explicitly, via unused output variables.    \
  12. * (EAX and EBP is not listed because EBP is saved/restored    \
  13. * explicitly for wchan access and EAX is the return value of    \
  14. * __switch_to())    \
  15. */    \
  16. unsigned long ebx, ecx, edx, esi, edi;    \
  17. \
  18. asm volatile("pushfl\n\t"    /* save flags */    /*将eflags寄存器值压栈*/\
  19.     "pushl %%ebp\n\t"    /* save EBP */    /*将EBP压栈*/\
  20. /*将当前栈指针(内核态)保存到prev进程的thread.sp中*/
  21.     "movl %%esp,%[prev_sp]\n\t"    /* save ESP */ \
  22.     /*将next进程的栈指针(内核态)装载到ESP寄存器中*/
  23.     "movl %[next_sp],%%esp\n\t"    /* restore ESP */ \
  24.     /*保存"标号1"的地址到prev进程的thread.ip,以便当prev进程重新被调度运行时,可以从"标号1处"重新开始执行*/
  25.     "movl $1f,%[prev_ip]\n\t"    /* save EIP */    \
  26.     /*
  27.          * 将next进程的IP(通常都是"标号1"的地址,因为通常都是经历过这里的调度过程的,上一行代码中即保存了这个IP)
  28.    * 压入当前的(即next进程的)堆栈中。结合后面的jmp指令(注意:不是call指令)一起理解,当__switch_to执行完ret返回时,
  29.    * 会自动从当前的堆栈中弹出该地址作为函数的返回地址接着执行,如此即可实现新进程的运行。
  30.                        */
  31.     "pushl %[next_ip]\n\t"    /* restore EIP */    \
  32.     __switch_canary    \
  33.     /*
  34.          *jmp到__switch_to函数执行,当此函数返回时,自动跳转到[next_ip]开始执行,实现新进程的调度。注意不是call,jmp指令
  35.          * 不会自动将当前地址压栈,call会自动压栈
  36.          */
  37.     "jmp __switch_to\n"    /* regparm call */    \
  38.     /*当prev进程再次被调度到时,从这里开始执行*/
  39.     "1:\t"    \
  40.     /*恢复EBP*/
  41.     "popl %%ebp\n\t"    /* restore EBP */    \
  42.     /*恢复eflags*/
  43.     "popfl\n"    /* restore flags */    \
  44. \
  45.     /* output parameters */    \
  46.     /*输出参数*/
  47.     : [prev_sp] "=m" (prev->thread.sp),    \
  48.       [prev_ip] "=m" (prev->thread.ip),    \
  49.       "=a" (last),    \
  50. \
  51.       /* clobbered output registers: */    \
  52.       "=b" (ebx), "=c" (ecx), "=d" (edx),    \
  53.       "=S" (esi), "=D" (edi)    \
  54.       \
  55.       __switch_canary_oparam    \
  56. \
  57.       /* input parameters: */    \
  58.       /*输入参数*/
  59.     : [next_sp] "m" (next->thread.sp),    \
  60.       [next_ip] "m" (next->thread.ip),    \
  61.       \
  62.       /* regparm parameters for __switch_to(): */    \
  63.       /*将prev和next分别存入ecx和edx,然后作为参数传入到__switch_to函数中*/
  64.       [prev] "a" (prev),    \
  65.       [next] "d" (next)    \
  66. \
  67.       __switch_canary_iparam    \
  68. \
  69.     : /* reloaded segment registers */    \
  70. "memory");    \
  71. } while (0)

__switch_to函数实现如下:

点击(此处)折叠或打开

  1. /*入参通过寄存器eax和edx从switch_to宏中传入*/
  2. __notrace_funcgraph struct task_struct *
  3. __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
  4. {
  5. /*取prev进程的上下文信息*/
  6. struct thread_struct *prev = &prev_p->thread,
  7. *next = &next_p->thread;
  8. /*取当前CPU*/
  9. int cpu = smp_processor_id();
  10. /*获取当前CPU的TSS对应的tss_struct*/
  11. struct tss_struct *tss = &per_cpu(init_tss, cpu);
  12. fpu_switch_t fpu;


  13. /* never put a printk in __switch_to... printk() calls wake_up*() indirectly */


  14. fpu = switch_fpu_prepare(prev_p, next_p, cpu);


  15. /*
  16. * Reload esp0.
  17. */
  18. /*
  19.  * 由于Linux的具体实现中,TSS不是针对每进程,而是针对每CPU的,即每个CPU对应一个tss_struct,那在进程上下文切换时,
  20.  * 需要考虑当前CPU上TSS中的内容的更新,其实就是内核栈指针的更新,更新后,当新进程再次进入到内核态执行时,
  21.  * 才能确保CPU硬件能从TSS中自动读取到正确的内核栈指针(sp0)的值,以保证从用户态切换到内核态时,相应的堆栈切
  22.  * 换正常。
  23.  */
  24. /*将next进程的内核栈指针(next->thread->sp0)值更新到当前CPU的TSS中*/
  25. load_sp0(tss, next);


  26. /*
  27. * Save away %gs. No need to save %fs, as it was saved on the
  28. * stack on entry. No need to save %es and %ds, as those are
  29. * always kernel segments while inside the kernel. Doing this
  30. * before setting the new TLS descriptors avoids the situation
  31. * where we temporarily have non-reloadable segments in %fs
  32. * and %gs. This could be an issue if the NMI handler ever
  33. * used %fs or %gs (it does not today), or if the kernel is
  34. * running inside of a hypervisor layer.
  35. */
  36. lazy_save_gs(prev->gs);


  37. /*
  38. * Load the per-thread Thread-Local Storage descriptor.
  39. */
  40. /*
  41.  * 将next_p进程使用的线程局部存储(TLS)段装入本地CPU的全局描述符表.
  42.  */
  43. load_TLS(next, cpu);


  44. /*
  45. * Restore IOPL if needed. In normal use, the flags restore
  46. * in the switch assembly will handle this. But if the kernel
  47. * is running virtualized at a non-zero CPL, the popf will
  48. * not restore flags, so it must be done in a separate step.
  49. */
  50. if (get_kernel_rpl() && unlikely(prev->iopl != next->iopl))
  51. set_iopl_mask(next->iopl);


  52. /*
  53. * Now maybe handle debug registers and/or IO bitmaps
  54. */
  55. if (unlikely(task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV ||
  56.     task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT))
  57. __switch_to_xtra(prev_p, next_p, tss);


  58. /*
  59. * Leave lazy mode, flushing any hypercalls made here.
  60. * This must be done before restoring TLS segments so
  61. * the GDT and LDT are properly updated, and must be
  62. * done before math_state_restore, so the TS bit is up
  63. * to date.
  64. */
  65. /*架构相关处理,半虚拟化中使用*/
  66. arch_end_context_switch(next_p);


  67. /*
  68. * Restore %gs if needed (which is common)
  69. */
  70. if (prev->gs | next->gs)
  71. lazy_load_gs(next->gs);


  72. switch_fpu_finish(next_p, fpu);
  73. /*将current_task per-CPU变量值更新为next进程信息*/
  74. this_cpu_write(current_task, next_p);
  75. /*
  76.  * 这里需要仔细理解。return到哪里?
  77.  * switch_to宏中,jmp到__switch_to函数之前将"next_ip"压入了当前堆栈,那通常情况下,这里return后,
  78.  * 会自动从堆栈中弹出next_ip开始执行,而next_ip通常为switch_to宏中保存的"标号1"的地址,即
  79.  * 这里通常会返回到switch_to宏中__switch_to函数之后的标号1处开始执行。
  80.  * 但有例外:对于没有产生过进程切换,而是第一次开始执行的进程(刚完成fork开始执行)来说.
  81.  * 由于没有通过switch_to宏保存next_ip,所以并不会跳回switch_to,而是跳转到ret_from_fork函数的超始
  82.  * 地址开始执行,因为在fork新进程时,即设置好了该进程的thread.eip设置成了ret_from_fork(参见
  83.  * copy_thread函数)
  84.  */
  85. return prev_p;
  86. }

第一次开始执行的进程的thread.eip设置点:

点击(此处)折叠或打开

  1. do_fork->copy_process->copy_thread
  2. int copy_thread(unsigned long clone_flags, unsigned long sp,
  3. unsigned long arg, struct task_struct *p)
  4. {
  5. struct pt_regs *childregs = task_pt_regs(p);
  6. struct task_struct *tsk;
  7. int err;


  8. p->thread.sp = (unsigned long) childregs;
  9. p->thread.sp0 = (unsigned long) (childregs+1);
  10. /*内核线程单独处理,其上下文信息单独填写*/
  11. if (unlikely(p->flags & PF_KTHREAD)) {
  12. /* kernel thread */
  13. memset(childregs, 0, sizeof(struct pt_regs));
  14. p->thread.ip = (unsigned long) ret_from_kernel_thread;
  15. task_user_gs(p) = __KERNEL_STACK_CANARY;
  16. childregs->ds = __USER_DS;
  17. childregs->es = __USER_DS;
  18. childregs->fs = __KERNEL_PERCPU;
  19. childregs->bx = sp;    /* function */
  20. childregs->bp = arg;
  21. childregs->orig_ax = -1;
  22. childregs->cs = __KERNEL_CS | get_kernel_rpl();
  23. childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_BIT1;
  24. p->fpu_counter = 0;
  25. p->thread.io_bitmap_ptr = NULL;
  26. memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));
  27. return 0;
  28. }
  29. /*将当前进程(父进程)的寄存器上下文信息赋给子进程,即子进程此后的上下文信息跟父进程保持一致了。*/
  30. *childregs = *current_pt_regs();
  31. childregs->ax = 0;
  32. if (sp)
  33. childregs->sp = sp;
  34. /*
  35.  * 子进程的IP指向ret_from_fork,fork创建的新进程,都要经历这个过程,在调度的上下文切换时,
  36.  * 其返回到ret_from_fork(entry_32.S汇编代码)中处理,这跟普通进程调度时上下文切换不一样,普通
  37.  * 进程的IP是在上次上下文切换时(switch_to)中保存的。
  38.  */
  39. p->thread.ip = (unsigned long) ret_from_fork;
  40. task_user_gs(p) = get_user_gs(current_pt_regs());


  41. p->fpu_counter = 0;
  42. p->thread.io_bitmap_ptr = NULL;
  43. tsk = current;
  44. err = -ENOMEM;


  45. memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));


  46. if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) {
  47. p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr,
  48. IO_BITMAP_BYTES, GFP_KERNEL);
  49. if (!p->thread.io_bitmap_ptr) {
  50. p->thread.io_bitmap_max = 0;
  51. return -ENOMEM;
  52. }
  53. set_tsk_thread_flag(p, TIF_IO_BITMAP);
  54. }


  55. err = 0;


  56. /*
  57. * Set a new TLS for the child thread?
  58. */
  59. if (clone_flags & CLONE_SETTLS)
  60. err = do_set_thread_area(p, -1,
  61. (struct user_desc __user *)childregs->si, 0);


  62. if (err && p->thread.io_bitmap_ptr) {
  63. kfree(p->thread.io_bitmap_ptr);
  64. p->thread.io_bitmap_max = 0;
  65. }
  66. return err;
  67. }

阅读(6294) | 评论(0) | 转发(2) |
给主人留下些什么吧!~~