之前一篇博客中是关于被动调度的系统调用返回部分,这篇博客将接着写被动调度的中断返回部分。
分析基于内核版本2.6.12.6
Linux进程的调度主要分为主动调度和被动调度两大类。整个linux运行过程中,被动调度分为用户态抢占调度和内核态抢占调度。
用户态抢占调度发生在当系统调用、中断处理、异常处理等返回用户态时,或者进程的时间片用完时。
这篇博客就是写用户态抢占调度的中断处理返回部分的,是看代码的一个总结,写下来希望能够加深理解。
中断处理返回
当从中断返回时是一个调度点,下面分析从中断返回相关的代码。
ENTRY(common_interrupt) /*中断公共入口*/ interrupt do_IRQ /* 0(%rsp): oldrsp-ARGOFFSET */ ret_from_intr: popq %rdi cli //关中断 subl $1,%gs:pda_irqcount #ifdef CONFIG_DEBUG_INFO movq RBP(%rdi),%rbp #endif leaq ARGOFFSET(%rdi),%rsp exit_intr: GET_THREAD_INFO(%rcx) //获取当前内核栈的thread_info的地址,保存到rcx寄存器中 testl $3,CS-ARGOFFSET(%rsp) //读取栈中的cs,判断中断是否发生在用户态 je retint_kernel //中断发生在内核态,返回内核态 |
common_interrupt函数是中断的公共入口,它首先使用用宏interrupt来调用do_IRQ函数处理中断。ret_from_intr标签开始就是执行完中断之后,中断返回相关的代码。
先来看下interrupt宏的实现代码:
/* 0(%rsp): interrupt number */ .macro interrupt func CFI_STARTPROC simple /*用在每个函数的开始,用于初始化一些内部数据结构*/ CFI_DEF_CFA rsp,(SS-RDI) CFI_REL_OFFSET rsp,(RSP-ORIG_RAX) CFI_REL_OFFSET rip,(RIP-ORIG_RAX) cld /*清方向标志, 在串指令操作期间,方向标志为DI和SI寄存器选择递增方式或递减方式。如果D=1.则寄存器内容自动地递减;如果D=0,则寄存器内容自动地递增*/ SAVE_ARGS /*保存现场*/ leaq -ARGOFFSET(%rsp),%rdi # arg1 for handler testl $3,CS(%rdi) /*判断中断发生时CPU是否运行于用户态*/ je 1f /*是内核态*/ swapgs /*切换gs寄存器的用户态值和内核值*/ 1: addl $1,%gs:pda_irqcount # RED-PEN should check preempt count movq %gs:pda_irqstackptr,%rax cmoveq %rax,%rsp pushq %rdi # save old stack call \func /*调用func函数*/ .endm |
SAVE_ARGS宏是将寄存器压栈,也就是所谓的“保存现场”。运行SAVE_ARGS宏之后系统堆栈的示意图如下:
代码段寄存器cs的最低两位代表着中断发生时cpu的运行级别CPL,0表示内核态,3表示用户态。如果最低两位为非0,则说明中断发生于用户空间。此宏中判断中断发生时cpu是否运行于用户态,如果是内核态则直接跳转到标签1处执行,否则先调用swapgs命令切换gs寄存器的用户态值和内核值。Testl指令将$3和CS(%rdi)相与,je就是当相与结果为0,也就是为内核态。
接下来就是将当前cpu的pda的pda_irqcount字段加1,并将pda中的pda_irqstackptr字段赋给rsp,即中断栈帧。最后使用call命令调用中断处理函数func,也就是do_IRQ函数。do_IRQ函数的参数为pt_regs结构,参数是通过rdi寄存器传入的。
从ret_from_intr开始处理中断返回,如果中断发生在内核态,则返回内核态,跳转到retint_kernel执行;如果中断发生在用户态,则返回用户态,跳转到retint_with_reschedule执行。
retint_kernel将在内核态抢占调度部分分析,这里是分析中断处理返回时用户态抢占的情况,因此接下来看看retint_with_reschedule代码。
/* Interrupt came from user space */ /* * Has a correct top of stack, but a partial stack frame * %rcx: thread info. Interrupts off. */ retint_with_reschedule: movl $_TIF_WORK_MASK,%edi retint_check: movl threadinfo_flags(%rcx),%edx andl %edi,%edx jnz retint_careful //判断是否还有其他工作需要做,有的话跳转到retint_careful执行 retint_swapgs: swapgs //当处理器离开内核时,使用swapgs命令在gs寄存器的内核与用户态值之间切换 retint_restore_args: cli RESTORE_ARGS 0,8,0 iret_label: iretq |
首先判断是否还有其他工作需要处理,有其他工作处理的情况下跳转到retint_careful执行。没有其他工作处理则首先调用swapgs命令在gs寄存器的内核与用户态值之间切换,为离开内核做准备。然后调用RESTORE_ARGS宏恢复之前SAVE_ARGS宏保存现场所压栈的那些寄存器。最后就是调用iretq离开中断。
下面看下当还有其他工作需要处理时retint_careful的执行情况。
/* edi: workmask, edx: work */ retint_careful: bt $TIF_NEED_RESCHED,%edx //检查是否需要重新调度 jnc retint_signal //不需要重新调度,跳转到retint_signal sti //开中断 pushq %rdi call schedule popq %rdi GET_THREAD_INFO(%rcx) cli //关中断 jmp retint_check |
首先检查是否需要重新调度,不需要重新调度就跳转到retint_signal执行。需要重新调度的话先开中断,然后调用schedule函数执行任务的调度。当调度的任务执行完之后,重新获得cpu时,跳转到retint_check处再次检查是否有工作需要处理。