一、问题
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)主流程
-
/*从中断返回*/
-
ret_from_intr:
-
/*将当前进程的thread_info结构体的指针存入%ebp帧寄存器中*/
-
GET_THREAD_INFO(%ebp)
-
#ifdef CONFIG_VM86
-
/* 取中断之前寄存器EFLAGS的高16位和段寄存器CS的内容构成的32位长整数放入eax中,其目的是检验:
-
* 1.中断之前CPU是否运行于VM86模式
-
* (EFLAGS的高16位中的第二位用来标识CPU运行在VM86模式下)
-
* 2.中断之前CPU运行于用户空间还是系统空间
-
*(CS的低两位代表着中断发生时CPU的运行级别CPL。若是CS的低两位为1,表示中断发生于用户空间,)
-
*/
-
movl PT_EFLAGS(%esp), %eax # mix EFLAGS and CS
-
movb PT_CS(%esp), %al
-
andl $(X86_EFLAGS_VM | SEGMENT_RPL_MASK), %eax
-
#else
-
/*
-
* We can be coming here from child spawned by kernel_thread().
-
*/
-
movl PT_CS(%esp), %eax
-
andl $SEGMENT_RPL_MASK, %eax
-
#endif
-
// 判断是否返回用户态或者v8086模式,如果不是,则转入resume_kernel,否则进入resume_userspace
-
cmpl $USER_RPL, %eax
-
jb resume_kernel # not returning to v8086 or userspace
2)返回用户态
-
// 如果是返回用户态
-
ENTRY(resume_userspace)
-
LOCKDEP_SYS_EXIT
-
/*
-
* 前面已经关了中断了,这次再关的原因是,还有其它流程会自己跳转
-
* 到这里,比如system_call
-
*/
-
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don
3)调度和信号处理:
-
work_pending:
-
# 返回用户态时,只需要判断need_resched是否置位,不需要判断preempt_count
-
# 如果need_resched置位,则发生调度,否则跳转到work_notifysig
-
testb $_TIF_NEED_RESCHED, %cl
-
# 进行信号处理
-
jz work_notifysig
-
work_resched:
-
# 需要调度,调用schedule函数
-
call schedule
-
# 调度返回,注意:到这里已经是新的进程上下文了,后面有机会处理信号
-
LOCKDEP_SYS_EXIT
-
# 关中断
-
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
-
# setting need_resched or sigpending
-
# between sampling and the iret
-
# 关闭trace irq功能
-
TRACE_IRQS_OFF
-
movl TI_flags(%ebp), %ecx
-
# 再次确认是否还有其它事情处理
-
andl $_TIF_WORK_MASK, %ecx # is there any work to be done other
-
# than syscall tracing?
-
# 如果没有,则恢复上下文
-
jz restore_all
-
# 如果有,再次检查是否需要调度,如果需要,则再次跳转到work_resched进行重新调度
-
testb $_TIF_NEED_RESCHED, %cl
-
jnz work_resched
-
# 如果不需要调度,则继续到work_notifysig,进行信号处理了,也就是说如果这里发生调度,也是有机会处理信号的。
-
-
work_notifysig: # deal with pending signals and
-
# notify-resume requests
-
#ifdef CONFIG_VM86
-
testl $X86_EFLAGS_VM, PT_EFLAGS(%esp)
-
movl %esp, %eax
-
jne work_notifysig_v86 # returning to kernel-space or
-
# vm86-space
-
1:
-
#else
-
movl %esp, %eax
-
#endif
-
# 开trace
-
TRACE_IRQS_ON
-
# 开中断(前面关了),意味着信号处理是开中断执行的,还是中断优先级高
-
ENABLE_INTERRUPTS(CLBR_NONE)
-
# 再次判断CS低两位,当为1时,表示中断/异常之前处于用户态,否则为内核态,据此再次确认是否需要返回内核态
-
movb PT_CS(%esp), %bl
-
andb $SEGMENT_RPL_MASK, %bl
-
cmpb $USER_RPL, %bl
-
# 返回内核态
-
jb resume_kernel
-
# edx清零
-
xorl %edx, %edx
-
# 调用C函数,其中进行通知链即信号的处理
-
call do_notify_resume
-
# 信号处理完后,重新跳转到resume_userspace,此时如果没有新的信号产生,则会在前面就通过restore_all恢复了,不会再到这里了
-
jmp resume_userspace
-
-
#ifdef CONFIG_VM86
-
ALIGN
-
work_notifysig_v86:
-
pushl_cfi %ecx # save ti_flags for do_notify_resume
-
call save_v86_state # %eax contains pt_regs pointer
-
popl_cfi %ecx
-
movl %eax, %esp
-
jmp 1b
-
#endif
-
END(work_pending)
4)返回内核态
-
/*如果配置了内核抢占*/
-
#ifdef CONFIG_PREEMPT
-
ENTRY(resume_kernel)
-
/*
-
* 前面已经关了中断了,这次再关的原因是,还有其它流程会自己跳转
-
* 到这里,比如system_call
-
*/
-
DISABLE_INTERRUPTS(CLBR_ANY)
-
/*判断是否可以抢占*/
-
cmpl $0,TI_preempt_count(%ebp) # non-zero preempt_count ?
-
/*抢占计数非0,不能抢占,则不产生调度,直接恢复上下文*/
-
jnz restore_all
-
/*可以抢占,则需要调度*/
-
need_resched:
-
/*判断need_resched是否被设置*/
-
movl TI_flags(%ebp), %ecx # need_resched set ?
-
testb $_TIF_NEED_RESCHED, %cl
-
/*没设置need_resched,则不需要调度,直接恢复上下文*/
-
jz restore_all
-
/*
-
*判断发生中断时(因为PT_EFLAGS(%esp)中保存的是进入中断时的EFLAGS值,这是由CPU硬件自动压栈的,中断走中断门,会自动关中断,异常走陷阱门,不自动关中断)是否关中断了,
-
*如果关了,表示是异常上下文(Fixme:应该是中断吧),则直接恢复上下文。
-
*/
-
testl $X86_EFLAGS_IF,PT_EFLAGS(%esp) # interrupts off (exception path) ?
-
jz restore_all
-
/*如果没关中断,表示为中断上下文?则调用preempt_schedule_irq,进行调度*/
-
call preempt_schedule_irq
-
jmp need_resched
-
END(resume_kernel)
2、异常返回(ret_from_exception)
异常返回跟中断返回流程基本一致,差别主要在于异常返回时,需要先关一次中断。因为Linux实现中,异常使用的是陷阱门,通过时不会自动关中断;而中断使用的是中断门,通过时会自动关中断。
-
/*从异常返回*/
-
ret_from_exception:
-
/*
-
* 这里为什么要关中断?而从中断返回不需要? 因为异常走的是陷阱门,
-
* 默认是不关中断执行的,而中断走的是中断门,默认是关中断执行的?
-
*
-
*/
-
/*关中断*/
-
preempt_stop(CLBR_ANY)
-
/*从中断返回*/
-
ret_from_intr:
-
...
3、fork返回(ret_from_fork)
fork返回的后半部分处理跟异常/中断返回一致,前面一部分有单独的处理:包括调用schedule_tail和跳转syscall_exit进行相关处理
-
#fork返回,单独处理
-
ENTRY(ret_from_fork)
-
CFI_STARTPROC
-
pushl_cfi %eax
-
#进行调度收尾处理,包括回收DEAD(X)状态的进程
-
call schedule_tail
-
#获取thread_info放入ebp中
-
GET_THREAD_INFO(%ebp)
-
popl_cfi %eax
-
#重设kernel eflags
-
pushl_cfi $0x0202 # Reset kernel eflags
-
popfl_cfi
-
#跳转到syscall_exit进行系统调用退出相关的处理。
-
jmp syscall_exit
-
CFI_ENDPROC
-
END(ret_from_fork)
阅读(8251) | 评论(0) | 转发(1) |