【深入剖析Linux内核】Linux内核之旅——(二)内核抢占与中断返回
1、上下文
一般来说,CPU在任何时刻都处于以下三种情况之一:
(1)运行于用户空间,执行用户进程;
(2)运行于内核空间,处于进程上下文;
(3)运行于内核空间,处于中断上下文。
应用程序通过系统调用陷入内核,此时处于进程上下文。现代几乎所有的CPU体系结构都支持中断。当外部设备产生中断,向CPU发送一个异步信号,CPU调用相应的中断处理程序来处理该中断,此时CPU处于中断上下文。
在进程上下文中,可以通过current关联相应的任务。进程以进程上下文的形式运行在内核空间,可以发生睡眠,所以在进程上下文中,可以使作信号量(semaphore)。实际上,内核经常在进程上下文中使用信号量来完成任务之间的同步,当然也可以使用锁。
中断上下文不属于任何进程,它与current没有任何关系(尽管此时current指向被中断的进程)。 由于没有进程背景,在中断上下文中不能发生睡眠,否则又如何对它进行调度。所以在中断上下文中只能使用锁进行同步,正是因为这个原因,中断上下文也叫做原子上下文(atomic context) (关于同步以后再详细讨论)。在中断处理程序中,通常会禁止同一中断,甚至会禁止整个本地中断,所以中断处理程序应该尽可能迅速,所以又把中断处理分成上部和下部(关于中断以后再详细讨论)。
2、上下文切换
上下文切换,也就是从一个可执行进程切换到另一个可执行进程。上下文切换由函数context_switch()函数完成,该函数位于kernel/sched.c中,它由进程调度函数schedule()调用。

这是一个C语言函数声明句
该函数实现了从一个任务切换到另一个任务的能力
其参数包括当前运行队列中的一个运行头节点
前驱任务节点以及目标任务节点
返回值表示新任务所在的位置
当条件MM不成立时,
active_mm字段被设置为old mm;
对old mm的计数器执行原子级递增;
将进入懒加载TLB状态;
否则,
执行切换逻辑,
if (unlikely( ! prev -> mm)) {
prev -> active_mm = NULL;
WARN_ON(rq -> prev_mm);
rq -> prev_mm = oldmm;
}
Here, we simply perform a switch on the registers' states and the stack. The function call is implemented as a switch_to operation that takes three arguments: prev's current value, next's desired value, and a previous value of prev.
return prev;
}

switch_mm()负责将逻辑地址空间映射至新的进程中;而switch_to则负责最终的进程切换过程,在此过程中它会保存原进程的所有寄存器状态,并使新进程恢复相应的寄存器设置,并启动新的程序运行。无论在何种情况下,内核要想实现任务切换操作,则总是使用schedule()函数来完成相应的切换步骤。
2.2、用户抢占
当内核即将返回到用户空间时, 内核会检查need_resched标志位是否被设置了, 如果标志位被设置了, 则会调用schedule()函数, 此时将导致用户的抢占. 通常情况下, 用户抢占会发生以下几种情况:
(1) 从系统调用返回到用户空间;
(2) 从中断(异常)处理程序返回到用户空间.
自2.6版本起内核已实现抢占机制,在非抢占式系统中该机制可无条件执行直至完成(但当进程运行于内核态时是无法被抢占的;值得注意的是,在系统调用服务例程中由于资源等待导致核心切换的情况被视为计划性进程切换)。然而,在由中断引发的进程切换中二者存在区别:前者称为强制性进程切换。
为了支持此类机制,默认情况下核心引入了一个预emption计数字段(preempt_count),其初始值设为0;每当获取锁时计数值加1;而每次释放锁计数值减1。当预emption计数为零表示核心可被安全地进行抢占;若预emption计数大于零则表示无法进行核心间的抢占操作。该字段对应三个不同的计数器(见软中断一节),即以下任一情况出现都会使预emption计数大于零:
(1) 在执行中断处理程序期间调用irq_enter函数会增加相应的中断计数器数值;
#define irq_enter() (preempt_count() += HARDIRQ_OFFSET)
(2) 可延迟函数被禁用(其常见于执行软中断或tasklet操作中),由函数local_bh_disable负责实现;
(3) 通过将抢占计数器设置为正值来明确禁止内核进行抢占操作,则由函数preempt_disable负责实现。
当从中断返回到内核空间后,默认情况下会检查两个条件:即是否(preempt_count等于零并且need_resched被设置),以及如果处于用户空间只需检查是否(preempt_count等于零并且need_reshed被设置)即可。一旦发现(preempt_count等于零并且need_reshed被设置),则会执行调度函数schedule()以完成任务的重新调度一般而言, 内核进行抢占操作通常包括以下几种情况:
(1) 当从中断(异常)返回到内核空间时, 如果(preempt_count等于零并且need_reshed被设置)(请参考相关章节以获取详细信息);
(2) 在异常处理程序中(尤其是系统调用)会调用preempt_enable以释放内核进行抢占操作的权利。

// incinclude/linux/preempt.h
#define preempt_enable()
do
{
// 先将抢占计数器减一
preempt_enable_no_resched();
// 然后检查是否有必要执行内核抢占调度操作
preempt_check_resched();
} while (0)

(3) 启用可延迟函数时,即调用local_bh_enable()时发生;

// kernel/softirq.c
void local_bh_enable( void )
{
WARN_ON(irqs_disabled());
/*
- Preemption must remain disabled until the entire soft IRQ processing is completed.
*/
// the soft IRQ counter value is decremented by one.
preempt_count() -= SOFTIRQ_OFFSET - 1 ;
if (unlikely(inerrupt status && soft IRQ pending)) {
call the soft interrupt handler; // SIG handling
// Decrement the preempt count timer by one.
dec_preempt_count();
}
// 检查是否需要进行内核抢占调度
preempt_check_resched();
}
// include/linux/preempt.h
#define verify_thread_scheduling_check() \
do { \
// 确认need_resched
if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) \
// 执行抢占调度验证
verify Thread Scheduling Process; \
} while ( false )
asm linking void __sched preempt_schedule( void )
/*
- 若预 empt_count非零或本地中断已关闭,
- 则我们不想抢占当前任务,请跳过此操作。。
*/
// 检查是否允许抢占,本地中断关闭,或者预 empt计数器值不为零时不允许抢占
if (unlikely(ti->preempt_count || irqs_disabled())) {
// 跳过此操作
return;
}
need_resched:
ti -> preempt_count = PREEMPT_ACTIVE;
// 启动调度
schedule();
ti -> preempt_count = 0 ;
/* we could miss a preemption opportunity between schedule and now */
barrier();
if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))
goto need_resched;
}

当内核任务被阻塞时,则会触发调用 schedule() 函数的行为;这种情况被视为内核主动释放其占用的 CPU 资源。
4.1从中断返回点
中断返回点标记为 ret_from-intr:

从中断/异常返回

从中断返回时,有两种情况,一是返回内核态,二是返回用户态。
5.1.1、返回内核态

#ifdef CONFIG_PREEMPT
/返回内核空间,先检查preempt_count,再检查need_resched/
ENTRY(resume_kernel)
/能否抢占资源,取决于preempt_count是否为零/
cmpl $ 0 ,TI_preempt_count(%ebp) # non-zero preempt_count ?
jnz restore_all #如果不能抢占,则恢复被中断时的状态
need_resched:
movl TI_flags(%ebp), %ecx # need_resched set ?
testb _TIF_NEED_RESCHED, %cl #是否需要重新调度
jz restore_all #不需要重新调度
testl IF_MASK,EFLAGS(%esp) # 发生异常则不调度
jz restore_all
#将最大值赋值给preempt_count,表示不允许再次被抢占
movl PREEMPT_ACTIVE,TI_preempt_count(%ebp)
sti
call schedule #调度函数
cli
movl 0 ,TI_preempt_count(%ebp) #preempt_count还原为0
#跳转到need_resched,判断是否又需要发生被调度
jmp need_resched
#endif

5.1.2、返回用户态

/返回到用户空间并仅检查need_resched/
ENTRY(resume_userspace) #当中断或异常发生时,任务处于用户空间
cli #确保我们不会错过任何中断
#设置need_resched/sigpending标志位于采样与iret之间
movl TI_flags(%ebp), %ecx
andl $_TIF_WORK_MASK, %ecx #是否有任何在int/exception返回前的工作要做?
jne work_pending #还有其他工作要做吗?
jmp restore_all #所有工作完成后则恢复处理器状态
#恢复处理器状态
restore_all:
RESTORE_ALL
perform work that needs to be done immediately before resumption
ALIGN
#完成其它工作
work_pending:
testb $_TIF_NEED_RESCHED, %cl #检查是否需要重新调度
jz work_notifysig #不需要重新调度
#需要重新调度
work_resched:
call schedule #调度进程
cli # make sure we don ' t miss an interrupt
setting need_resched or sigpending
between sampling and the iret
movl TI_flags(%ebp), %ecx
/检查是否还有其它的事要做/
andl $_TIF_WORK_MASK, %ecx # is there any work to be done other
than syscall tracing?
jz restore_all #没有其它的事,则恢复处理器状态
testb $_TIF_NEED_RESCHED, %cl
jnz work_resched #如果need_resched再次置位,则继续调度
#VM和信号检测
work_notifysig: # deal with pending signals and
notify-resume requests
testl $VM_MASK, EFLAGS(%esp) #检查是否是VM模式
movl %esp, %eax
jne work_notifysig_v86 # returning to kernel-space or
vm86-space
xorl %edx, %edx
#进行信号处理
call do_notify_resume
jmp restore_all
ALIGN
work_notifysig_v86:
; 将 ti_flags 值保存到 do_notify_resume 指令中
eax = ti_flags
; 调用 save_v86_state 函数以保存状态信息
call save_v86_state
; 返回栈中的值到 %ecx 寄存器
popl %ecx
; 将返回地址保存到堆栈中并准备下一次循环
movl do_notify_resume, %esp
; 清除 edx 寄存器的内容以便后续使用
xorl edx, $edx
; 调用 do_notify_resume 程序以执行信号处理任务
call do_notify_resume #执行信号处理#
; 返回到主程序执行下一个循环 iteration#跳转回主程序执行下一个循环#

5.2、从异常返回
异常返回点为ret_from_exception:
#处理异常返回
ALIGN
exception Handling: preemptive Stop /*/相当于CLI,在中断返回时,在handle_IRQ_event已关闭中断的情况下无需此步骤/
6、从系统调用返回

#系统调用入口
Function entry point for system calls
pushl %eax # save original eax register
SAFE thread operation mode activation
Obtain thread information via GET_THREAD_INFO(%ebp)
Test for syscall_trace and syscall_audit flags in TI_flags(%ebp)
If the test result is non-zero, proceed to compare number of system calls with %eax register value
Jump to badsys section if comparison result is zero
call the corresponding function in sys_call_table using %eax and a word size of 4
Store return value from the called function into EAX register block
system call return address handling:
Ensure that interrupts are not overlooked by executing cli instruction
Prepare for rescheduling or handle sigpending between sampling and iret execution
Copy current work state to TI_flags(%ebp) register block
If there is remaining work to be done, proceed to exit work block section
Restore registers and memory state by executing RESTORE_ALL block
执行其他任务
syscall_exit_work:
; 判断系统调用追踪、审计以及单步执行功能是否启用
testb (_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT|_TIF_SINGLESTEP), %cl
jz work_pending ; 若这些功能未启用,则直接跳转至work_pending阶段以完成调度与信号处理
sti # 可以让do_syscall_trace函数直接执行而不必先调用schedule函数
movl %esp, %eax
movl 1, %edx ; 系统调用追踪相关代码
call do_syscall_trace ; 调用do_syscall_trace函数以完成相关操作
jmp resume_userspace ; 返回用户空间并继续后续操作

整个中断、异常和系统调用返回流程如下:

http://www.cnblogs.com/hustcat/archive/2009/08/31/1557507.html
