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

2016年(1)

2015年(21)

2014年(100)

分类: LINUX

2015-01-15 15:47:33

一、问题
1、内核调度与中断/异常/系统调用的关系如何?
2、信号处理与中断/异常/系统调用的关系如何?
3、内核抢占与中断/异常/系统调用的关系如何?

4、内核线程的调度有何特别之处?中断/异常/系统调用返回时,内核线程会发生调度吗?

 这些问题都需要分析清楚中断/异常的返回流程,才能解答。

二、中断/异常的返回流程
1、中断/异常的返回内核软件流程
中断、异常(包括系统调用)和fork返回的处理流程如下:

 

关键点:
1)中断返回和异常返回的流程基本一致,差别主要在于异常返回时,需要先关一次中断。因为Linux实现中,异常使用的是陷阱门,通过时不会自动关中断;而中断使用的是中断门,通过时会自动关中断。
2)中断/异常(包括系统调用)返回时,是进行调度(schedule)的重要时机点,其中,中断(时钟中断)返回时调度依赖的最主要的时机点,时钟中断处理函数中不会直接进行调度,只是根据相应的调度算法,决定是否需要调度,以及调度的next task,如果需要调度,则设置NEED_RESCHED标记。调度(schedule)的实际执行是在中断返回的时候,检查NEED_RESCHED标记,如果设置则进行调度。
3)信号处理是在当前进程从内核态返回用户态时进行的,在发生中断、异常(包括系统调用)、或fork时,都有可能从内核态返回用户态,都是处理信号的时机。注意:只有current进程的信号才能在此时得到处理。其它非正在运行的进程的信号无法处理。
4)关于内核抢占。中断/异常发生在内核态时,也就是说中断/异常返回时,需要返回内核态,走resume_kernel流程,此时,如果内核支持内核抢占,则此时是个关键的调度时机点,如果内核不支持抢占,则不会发生调度。也就是说:如果当前进程上下文处于内核态,当不支持内核抢占时,则无论进程的优先级和时间片如何,都是不能发生调度的,只能在返回用户态时,才能发生调度。从这点可以看出,当不支持内核抢占时,Linux的实时性很差(开启内核抢占后稍好),当在内核态(中断、软中断、其它内核流程)执行时间或流程太长时,可能导致进程调度饥饿,极端情况下,当在内核态发生死锁时,会直接导致整个系统因无法调度而死锁,当然针对这种情况(softlockup),内核提供了专门的watchdog机制来检测。

5)关于内核线程的调度,跟普通线程相比,从原理和机制上看,没有特别之处。但关键的不同在于:内核线程始终运行在内核态,当没有开启内核抢占时,设想当一个内核线程被中断/异常打断,此时从中断/异常返回时会发生调度吗?答案是不会,因为当前进程的上下文处于内核态,在没有开启内核抢占的情况下,是不会发生调度行为的,除非该内核线程主动调用schedule()释放CPU控制权。也就是说,内核线程触发主动调用schedule,否则会一直占用CPU。所以在编写内核线程时,需要在相关任务处理结束后,主动调用schedule,这点需要注意。
 

2、中断/异常返回时硬件完成的处理流程
中断或异常返回时,必然会执行iret指令,然后将控制器交回给之前被中断打断的进程,硬件自动完成如下操作:
1)从当前栈(内核栈)中弹出cs、eip和eflag,并load到相应的寄存器中寄存器。(如之前有硬件错误码入栈,需要先弹出这个错误码)。 
2)权限检查。比对ISR的CPL是否等于cs中的低两位的值。如果是,iret终止返回;否则,转入下一步。 
3)从当前栈(内核栈)中弹出之前压入的用户态堆栈相关的ss和esp,并load到相应寄存器,至此,即完成了从内核栈到用户栈的切换。 
4)后续处理。主要包括:检查ds、es、fs及gs段寄存器,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么,清相关的段寄存器。目的是为了防止用户态的程序利用内核以前所用的段寄存器,以防止恶意用户程序利用其访问内核地址空间。


三、代码分析
中断、异常(包括系统调用)、fork返回时,会分别跳转到entry_32.S汇编代码中的ret_from_intr、ret_from_exception、ret_from_fork标号处执行。相应代码分析如下:
1、中断返回(ret_from_intr)
1)主流程

点击(此处)折叠或打开

  1. /*从中断返回*/
  2. ret_from_intr:
  3. /*将当前进程的thread_info结构体的指针存入%ebp帧寄存器中*/
  4. GET_THREAD_INFO(%ebp)
  5. #ifdef CONFIG_VM86
  6. /* 取中断之前寄存器EFLAGS的高16位和段寄存器CS的内容构成的32位长整数放入eax中,其目的是检验:
  7.  * 1.中断之前CPU是否运行于VM86模式
  8.  * (EFLAGS的高16位中的第二位用来标识CPU运行在VM86模式下)
  9.  * 2.中断之前CPU运行于用户空间还是系统空间
  10.  *(CS的低两位代表着中断发生时CPU的运行级别CPL。若是CS的低两位为1,表示中断发生于用户空间,)
  11.         */
  12. movl PT_EFLAGS(%esp), %eax    # mix EFLAGS and CS
  13. movb PT_CS(%esp), %al
  14. andl $(X86_EFLAGS_VM | SEGMENT_RPL_MASK), %eax
  15. #else
  16. /*
  17. * We can be coming here from child spawned by kernel_thread().
  18. */
  19. movl PT_CS(%esp), %eax
  20. andl $SEGMENT_RPL_MASK, %eax
  21. #endif
  22. // 判断是否返回用户态或者v8086模式,如果不是,则转入resume_kernel,否则进入resume_userspace
  23. cmpl $USER_RPL, %eax
  24. jb resume_kernel    # not returning to v8086 or userspace

2)返回用户态

点击(此处)折叠或打开

  1. // 如果是返回用户态
  2. ENTRY(resume_userspace)
  3. LOCKDEP_SYS_EXIT
  4. /*
  5. * 前面已经关了中断了,这次再关的原因是,还有其它流程会自己跳转
  6. * 到这里,比如system_call
  7. */
  8.      DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don

3)调度和信号处理:

点击(此处)折叠或打开

  1. work_pending:
  2.     # 返回用户态时,只需要判断need_resched是否置位,不需要判断preempt_count
  3.     # 如果need_resched置位,则发生调度,否则跳转到work_notifysig
  4.     testb $_TIF_NEED_RESCHED, %cl
  5.     # 进行信号处理
  6.     jz work_notifysig
  7. work_resched:
  8.     # 需要调度,调用schedule函数
  9.     call schedule
  10.     # 调度返回,注意:到这里已经是新的进程上下文了,后面有机会处理信号
  11.     LOCKDEP_SYS_EXIT
  12.     # 关中断
  13.     DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt
  14.                     # setting need_resched or sigpending
  15.                     # between sampling and the iret
  16.     # 关闭trace irq功能
  17.     TRACE_IRQS_OFF
  18.     movl TI_flags(%ebp), %ecx
  19.     # 再次确认是否还有其它事情处理
  20.     andl $_TIF_WORK_MASK, %ecx    # is there any work to be done other
  21.                     # than syscall tracing?
  22.     # 如果没有,则恢复上下文
  23.     jz restore_all
  24.     # 如果有,再次检查是否需要调度,如果需要,则再次跳转到work_resched进行重新调度
  25.     testb $_TIF_NEED_RESCHED, %cl
  26.     jnz work_resched    
  27.     # 如果不需要调度,则继续到work_notifysig,进行信号处理了,也就是说如果这里发生调度,也是有机会处理信号的。

  28. work_notifysig:                # deal with pending signals and
  29.                     # notify-resume requests
  30. #ifdef CONFIG_VM86
  31.     testl $X86_EFLAGS_VM, PT_EFLAGS(%esp)
  32.     movl %esp, %eax
  33.     jne work_notifysig_v86        # returning to kernel-space or
  34.                     # vm86-space
  35. 1:
  36. #else
  37.     movl %esp, %eax
  38. #endif
  39.     # 开trace
  40.     TRACE_IRQS_ON
  41.     # 开中断(前面关了),意味着信号处理是开中断执行的,还是中断优先级高
  42.     ENABLE_INTERRUPTS(CLBR_NONE)
  43.     # 再次判断CS低两位,当为1时,表示中断/异常之前处于用户态,否则为内核态,据此再次确认是否需要返回内核态
  44.     movb PT_CS(%esp), %bl
  45.     andb $SEGMENT_RPL_MASK, %bl
  46.     cmpb $USER_RPL, %bl
  47.     # 返回内核态
  48.     jb resume_kernel
  49.     # edx清零
  50.     xorl %edx, %edx
  51.     # 调用C函数,其中进行通知链即信号的处理
  52.     call do_notify_resume
  53.     # 信号处理完后,重新跳转到resume_userspace,此时如果没有新的信号产生,则会在前面就通过restore_all恢复了,不会再到这里了
  54.     jmp resume_userspace

  55. #ifdef CONFIG_VM86
  56.     ALIGN
  57. work_notifysig_v86:
  58.     pushl_cfi %ecx            # save ti_flags for do_notify_resume
  59.     call save_v86_state        # %eax contains pt_regs pointer
  60.     popl_cfi %ecx
  61.     movl %eax, %esp
  62.     jmp 1b
  63. #endif
  64. END(work_pending)

4)返回内核态

点击(此处)折叠或打开

  1. /*如果配置了内核抢占*/
  2. #ifdef CONFIG_PREEMPT
  3. ENTRY(resume_kernel)
  4. /*
  5. * 前面已经关了中断了,这次再关的原因是,还有其它流程会自己跳转
  6. * 到这里,比如system_call
  7. */
  8. DISABLE_INTERRUPTS(CLBR_ANY)
  9. /*判断是否可以抢占*/
  10. cmpl $0,TI_preempt_count(%ebp)    # non-zero preempt_count ?
  11. /*抢占计数非0,不能抢占,则不产生调度,直接恢复上下文*/
  12. jnz restore_all
  13. /*可以抢占,则需要调度*/
  14. need_resched:
  15. /*判断need_resched是否被设置*/
  16. movl TI_flags(%ebp), %ecx    # need_resched set ?
  17. testb $_TIF_NEED_RESCHED, %cl
  18. /*没设置need_resched,则不需要调度,直接恢复上下文*/
  19. jz restore_all
  20. /*
  21. *判断发生中断时(因为PT_EFLAGS(%esp)中保存的是进入中断时的EFLAGS值,这是由CPU硬件自动压栈的,中断走中断门,会自动关中断,异常走陷阱门,不自动关中断)是否关中断了,
  22. *如果关了,表示是异常上下文(Fixme:应该是中断吧),则直接恢复上下文。
  23. */
  24. testl $X86_EFLAGS_IF,PT_EFLAGS(%esp)    # interrupts off (exception path) ?
  25. jz restore_all
  26. /*如果没关中断,表示为中断上下文?则调用preempt_schedule_irq,进行调度*/
  27. call preempt_schedule_irq
  28. jmp need_resched
  29. END(resume_kernel)

2、异常返回(ret_from_exception)
异常返回跟中断返回流程基本一致,差别主要在于异常返回时,需要先关一次中断。因为Linux实现中,异常使用的是陷阱门,通过时不会自动关中断;而中断使用的是中断门,通过时会自动关中断。

点击(此处)折叠或打开

  1. /*从异常返回*/
  2. ret_from_exception:
  3. /*
  4. * 这里为什么要关中断?而从中断返回不需要? 因为异常走的是陷阱门,
  5. * 默认是不关中断执行的,而中断走的是中断门,默认是关中断执行的?
  6. *
  7. */
  8. /*关中断*/
  9. preempt_stop(CLBR_ANY)
  10. /*从中断返回*/
  11. ret_from_intr:
  12. ...

3、fork返回(ret_from_fork)
fork返回的后半部分处理跟异常/中断返回一致,前面一部分有单独的处理:包括调用schedule_tail和跳转syscall_exit进行相关处理

点击(此处)折叠或打开

  1. #fork返回,单独处理
  2. ENTRY(ret_from_fork)
  3. CFI_STARTPROC
  4. pushl_cfi %eax
  5. #进行调度收尾处理,包括回收DEAD(X)状态的进程
  6. call schedule_tail
  7. #获取thread_info放入ebp中
  8. GET_THREAD_INFO(%ebp)
  9. popl_cfi %eax
  10. #重设kernel eflags
  11. pushl_cfi $0x0202    # Reset kernel eflags
  12. popfl_cfi
  13. #跳转到syscall_exit进行系统调用退出相关的处理。
  14. jmp syscall_exit
  15. CFI_ENDPROC
  16. END(ret_from_fork)


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