从中断和异常返回

我们通过考察中断和异常处理程序的终止阶段来结束本章。尽管终止阶段的主要目的很清楚,即恢复某个程序的执行;但是,在这样做之间,还需要考虑几个问题:

内核控制路径并发执行的数量

如果仅仅只有一个,那么CPU必须切换到用户态。

挂起进程的切换请求

如果有任何请求,内核就必须执行进程调度,否则,把控制权还给当前进程。

挂起的信号

如果一个信号发送到当前进程,就必须处理它。

单步执行模式

如果调试程序正在跟踪当前进程的执行,就必须在进程切换回到用户态之前恢复单步执行。

需要使用一些标志来记录挂起进程切换的请求,挂起信号和单步执行,这些标志被存放在thread_info描述符的flags字段中,这个字段也存放其它与从中断和异常返回无关的标志。表4-15完整地列出了中断和异常返回相关的标志。


从技术上说,完成所有这些事情的内核汇编语言代码并不是一个函数,因为控制权从不返回到调用它的函数。它只是一个代码片段,有两个不同的入口点,分别叫做ret_form_intr()ret_from_exception()。正如其名所暗示的,中断处理程序结束时,内核进入ret_from_intr(),而当异常处理程序结束时,它进入ret_form_exception()。为了描述起来更容易一些,我们将把这两个入口点当做函数来讨论。

4-6是关于两个入口点的完整的流程图。灰色的框图涉及实现内核抢占的汇编指令,如果你只想了解不支持抢占的内核都做了些什么,就可以忽略这些灰色的框图。在流程图中,入口点ret_from_exception()ret_from_intr()看起来非常相似,它们唯一区别是:如果内核在编译时选择了支持内核抢占,那么从异常返回时要立刻禁用本地中断。

流程图大致给出了恢复执行被中断的程序所必需的步骤。现在,我们要通过讨论汇编语言代码来详细描述这个过程。


入口点


ret_from_intr()ret_from_exception()入口点本质上相当于下面这段汇编代码:

ret_form_exception:

cli

ret_from_intr:

movl$-8192,%ebp

andl%esp,%ebp

movl0x30(%esp),%eax

movb0x2c(%esp),%al

testl%0x00020003,%eax

jnzresum_userspace

mpmresume_kernel

回忆前面对handle_IRQ_event()描述的第3步,在中断返回时,本地中断是被禁用的。因此,只有在从异常返回时才使用cli这条汇编指令。

内核把当前thread_info描述符的地址装载到ebp寄存器。

接下来,要根据发生中断或异常时压入栈中的cseflags寄存器的值,来确定被中断的程序在中断发生时是否运行在用户态,或确定是否设置了eflasgVM标志。在任何一种情况下,代码的执行就跳转到resume_userspace处。否则,代码的执行就跳转到resume_kernel处。

恢复内核控制路径

如果被恢复的程序运行在内核态,就执行resume_kernel处的汇编语言代码:

resume_kernel:

cli

cmpl$0,0x14(%esp)

jzneed_resched

restore_all:

pop1$ebx

pop1$ecx

pop1$edx

pop1$esi

pop1$edi

pop1$ebp

pop1$eax

pop1$ds

pop1$es

addl$4,%esp

iret

如果thread_info描述符的preempt_count字段为0,则内核跳转到need_resched处,否则,被中断的程序重新开始执行。函数用中断和异常开始时保存的值装载寄存器,然后通过执行iret指令结束其控制。


检查内核抢占

执行检查内核抢占的代码是,所有没执行完的内核控制路径都不是中断处理程序,否则preempt_count字段的值就会是大于0的。但是正如在本章”中断和异常处理程序的嵌套执行”一节所强调的,最多可能有两个异常相关的内核控制路径。

need_resched:

movl0x8(%ebp),%ecx

testb$(1<<TIF_NEED_RESCHED),%cl

jzrestore_all

testl$0x00000200,0x30(%esp)

jzrestore_all

callpreempt_schedule_irq

jmpneed_resched

如果current->thread_infoflags字段中的TIF_NEED_RESCHED标志为0,说明没有需要切换的进程,因此程序跳转到restore_all处。如果正在被恢复的内核控制路径是在禁用本地CPU的情况下运行,那么也跳转到restore_all处。在这种情况下,进程切换可能破坏内核数据结构。

如果需要进行进程切换,就调用preempt_schedule_irq()函数:它设置preempt_count字段的PREEMPT_ACTIVE标志,把大内核锁计数器暂时置为-1。打开本地中断并调用schedule()以选择另一个进程来运行。当前面的进程要恢复时,preempt_schedule_irq()使大内核计数器的值恢复为以前的值,清除PREENPT_ACTIVE标志并禁用本地中断。但当前进程的TIF_NEED_RESCHED标志被设置,将继续调用schedule()函数。


恢复用户态程序

如果恢复的程序原来运行在用户态,就跳转到resum_user_space处:

resume_userspace:

cli

movl0x0(%ebp),%ecx

andl$0x0000ff6e,%ecx

jerestore_all

jmpwork_pending

禁止本地中断之后检测current->thread_infoflags字段的值。如果只设置了TIF_SYSCALL_TRACE,TIF_SYSCALL_AUDITTIF_SINGLESTEP标志,就不做任何其它的事情,只是跳转到restore_all,从而恢复用户态程序。


检测调度标志


thread_infodescriptor描述符的flags表示在恢复被中断的程序前,需要完成额外的工作。

work_pending:

testb$(1<<TIF_NEED_RESCHED),%cl

jzwork_notifysing

work_resched:

callschedule

cli

jmpresume_usersapce

如果进程切换请求被挂起,就调用schedule()选择另一个进程投入运行。当前面的进程要恢复时,就跳回到resume_userspace处。


处理挂起信号、虚拟8086模式和单步执行


除了处理进程切换请求,还有其它的工作需要处理:

work_notifysig:

movl%esp,%eax

testl$0x00020000,0x30(%esp)

jeif

work_notifysig_v86:

pushl%ecx

callsave_v86_state

popl%ecx

movl%eax,%esp

l:

xorl%edx,%edx

calldo_notify_resume

jmprestore_all

如果用户态程序eflags寄存器的VM控制标志被设置,就调用save_v86_state()函数在用户态地址空间建立虚拟8086模式的数据结构。然后,就调用do_notify_resume()函数处理挂起信号和单步执行。最后,跳转到restore_all标记处,恢复被中断的程序。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值