在前面一篇博文中,我们分析了i386 CPU的中断机制和内核中有关的初始化,现在我们进一步分析中断的响应过程和服务(和异常的响应机制不同)。我们假设外设驱动都已经完成了初始化,并且已把相应的中断服务程序挂入到特定的中断请求队列中,系统正在用户空间正常运行,并且某个外设已经产生了一次中断请求,该请求通过中断控制器到达了CPU的“中断请求”引线INTR。CPU从中断控制器取得中断向量,然后根据具体的中断向量从中断向量表IDT中找到相应的表项,该表项为一个中断门,然后通过该中断门的设置到达了该通道的总服务程序的入口IRQxx_interrupt,即为interrupt[]中的元素(参考前面一篇文章),这里为了方便说明,再次给出IRQxx_interrupt等的源码,如下:
pushl $vector-256
jmp common_interrupt
/* common_interrupt如下 */
common_interrupt:
SAVE_ALL
movl %esp,%eax
call do_IRQ
jmp ret_from_intr
/* SAVE_ALL如下 */
#define SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__USER_DS), %edx; \
movl %edx, %ds; \
movl %edx, %es;
在SAVE_ALL后,堆栈如图:
接下来将栈顶指针esp存到eax中,然后执行do_IRQ函数,该函数声明如下:
fastcall unsigned int do_IRQ(struct pt_regs *regs)
/* 关键字regparm表示函数到eax寄存器中去找到参数regs的值 */
#define fastcall __attribute__((regparm(3)))
/* 到此可以清楚的看到,regs指向的内容为上图中系统堆栈中的内容 */
struct pt_regs {
long ebx;
long ecx;
long edx;
long esi;
long edi;
long ebp;
long eax;
int xds;
int xes;
long orig_eax;
long eip;
int xcs;
long eflags;
long esp;
int xss;
};
接下来分析do_IRQ()函数,如下:
/**
* do_IRQ执行与一个中断相关的所有中断服务例程.
*/
fastcall unsigned int do_IRQ(struct pt_regs *regs)
{
/* 通过orig_eax读回并屏蔽掉高位,又得到中断号irq */
int irq = regs->orig_eax & 0xff;
#ifdef CONFIG_4KSTACKS
union irq_ctx *curctx, *irqctx;
u32 *isp;
#endif
/**
* irq_enter增加中断嵌套计数
*/
irq_enter();
#ifdef CONFIG_DEBUG_STACKOVERFLOW
/* Debugging check for stack overflow: is there less than 1KB free? */
{
long esp;
__asm__ __volatile__("andl %%esp,%0" :
"=r" (esp) : "0" (THREAD_SIZE - 1));
if (unlikely(esp < (sizeof(struct thread_info) + STACK_WARN))) {
printk("do_IRQ: stack overflow: %ld\n",
esp - sizeof(struct thread_info));
dump_stack();
}
}
#endif
#ifdef CONFIG_4KSTACKS
/**
* 如果thread_union结构大小为4KB,函数切换到硬中断请求栈
* 上一篇文章讲到有三种内核栈,所有的硬中断请求存放在harding_stack数组中
* 所有的软中断请求存放在softirq_stack数组中,每个数组元素为irq_ctx类型的联合体
* hardirq_ctx和softirq_ctx数组使内核快速确定CPU的硬中断请求栈和软中断请求栈,它们包含的指针分别指向相应的irq_ctx元素
*/
curctx = (union irq_ctx *) current_thread_info(); /* 取得当前进程内核栈 */
irqctx = hardirq_ctx[smp_processor_id()]; /* 取得CPU的硬中断请求栈 */
/**
* 当前在使用内核栈,而不是硬中断请求栈.就需要切换栈
*/
if (curctx != irqctx) {
int arg1, arg2, ebx;
/* build the stack frame on the IRQ stack */
isp = (u32*) ((char*)irqctx + sizeof(*irqctx));
/**
* 保存当前进程描述符指针
*/
irqctx->tinfo.task = curctx->tinfo.task;
/**
* 把esp栈指针寄存器的当前值存入irqctx的thread_info(内核oops时使用)
*/
irqctx->tinfo.previous_esp = current_stack_pointer;
/**
* 将中断请求栈的栈顶装入esp,isp即为中断栈顶
* 调用完__do_IRQ后,从ebx中恢复esp
*/
asm volatile(
" xchgl %%ebx,%%esp \n"
" call __do_IRQ \n"
" movl %%ebx,%%esp \n"
: "=a" (arg1), "=d" (arg2), "=b" (ebx)
: "0" (irq), "1" (regs), "2" (isp)
: "memory", "cc", "ecx"
);
} else/* 否则,内核已经在使用硬中断请求栈(发生了中断嵌套),不用切换 */
#endif
/* 该函数见下面分析 */
__do_IRQ(irq, regs);
/**
* 递减中断计数器并检查是否有可延迟函数
*/
irq_exit();
/**
* 结束后,会返回ret_from_intr函数.
*/
return 1;
}
__do_IRQ(irq, regs)函数如下:
fastcall unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs)
{
irq_desc_t *desc = irq_desc + irq;
struct irqaction * action;
unsigned int status;
/**
* 中断发生次数计数.
*/
kstat_this_cpu.irqs[irq]++;
if (desc->status & IRQ_PER_CPU) {
irqreturn_t action_ret;
/*
* No locking required for CPU-local interrupts:
*/
desc->handler->ack(irq);
action_ret = handle_IRQ_event(irq, regs, desc->action);
if (!noirqdebug)
note_interrupt(irq, desc, action_ret);
desc->handler->end(irq);
return 1;
}
/**
* 虽然中断是关闭的,但是还是需要使用自旋锁保护desc
*/
spin_lock(&desc->lock);
/**
* 如果是旧的8259A PIC,ack就是mask_and_ack_8259A,它应答PIC上的中断并禁用这条IRQ线.屏蔽IRQ线是为了确保在这个中断处理程序结束前,
* CPU不进一步接受这种中断的出现.
* __do_IRQ是以禁止本地中断运行,事实上,CPU控制单元自动清eflags寄存器的IF标志.因为中断处理程序是通过IDT中断门调用的.
* 不过,内核在执行这个中断的中断服务例程之前可能会重新激活本地中断.
* 在使用APIC时,应答中断信赖于中断类型,可能是ack,也可能延迟到中断处理程序结束(也就是应答由end方法去做).
* 无论如何,中断处理程序结束前,本地APIC不进一步接收这种中断,尽管这种中断可能会被其他CPU接受.
*/
desc->handler->ack(irq);
/**
* 初始化主IRQ描述符的几个标志.设置IRQ_PENDING标志.也清除IRQ_WAITING和IRQ_REPLAY
* 这几个标志可以很好的解决中断重入的问题.
* IRQ_REPLAY标志是"挽救丢失的中断"所用.在此不详述.
*/
status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
status |= IRQ_PENDING; /* we _want_ to handle it */
action = NULL;
/**
* IRQ_DISABLED和IRQ_INPROGRESS被设置时,什么都不做(action==NULL)
* 即使IRQ线被禁止,CPU也可能执行do_IRQ函数.首先,可能是因为挽救丢失的中断,其次,也可能是有问题的主板产生伪中断.
* 所以,是否真的执行中断代码,需要根据IRQ_DISABLED标志来判断,而不仅仅是禁用IRQ线.
* IRQ_INPROGRESS标志的作用是:如果一个CPU正在处理一个中断,那么它会设置它的IRQ_INPROGRESS.这样,其他CPU上发生同样的中断
* 就可以检查是否在其他CPU上正在处理同种类型的中断,如果是,就什么都不做,这样做有以下好处:
* 一是使内核结构简单,驱动程序的中断服务例程式不必是可重入的.二是可以避免弄脏当前CPU的硬件高速缓存.
*/
if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
action = desc->action;
status &= ~IRQ_PENDING; /* we commit to handling */
status |= IRQ_INPROGRESS; /* we are handling it */
}
desc->status = status;
/**
* 当前面两种情况出现时,不需要(或者是不需要马上)处理中断.就退出
* 或者没有相关的中断服务例程时,也退出.当内核正在检测硬件设备时就会发生这种情况.
*/
if (unlikely(!action))
goto out;
/**
* 这里是需要循环处理的,并不是说调用一次handle_IRQ_event就行了.
*/
for (;;) {
irqreturn_t action_ret;
/**
* 现在打开自旋锁了,那么,其他CPU可能也接收到同类中断,并设置IRQ_PENDING标志.
*/
spin_unlock(&desc->lock);
/**
* 调用中断服务例程.
*/
action_ret = handle_IRQ_event(irq, regs, action);
spin_lock(&desc->lock);
if (!noirqdebug)
note_interrupt(irq, desc, action_ret);
/**
* 如果其他CPU没有接收到同类中断,就退出
* 否则,继续处理同类中断.
*/
if (likely(!(desc->status & IRQ_PENDING)))
break;
/**
* 清除了IRQ_PENDING,如果再出现IRQ_PENDING,就说明是其他CPU上接收到了同类中断.
* 注意,IRQ_PENDING仅仅是一个标志,如果在调用中断处理函数的过程中,来了多次的同类中断,则意味着只有一次被处理,其余的都丢失了.
*/
desc->status &= ~IRQ_PENDING;
}
desc->status &= ~IRQ_INPROGRESS;
out:
/**
* 现在准备退出了,end方法可能是应答中断(APIC),也可能是通过end_8259A_irq方法重新激活IRQ(只要不是伪中断).
*/
desc->handler->end(irq);
/**
* 好,工作已经全部完成了,释放自旋锁吧.注意两个锁的配对使用方法.
*/
spin_unlock(&desc->lock);
return 1;
}
handle_IRQ_event函数如下:
/**
* 执行中断服务例程
*/
fastcall int handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
struct irqaction *action)
{
int ret, retval = 0, status = 0;
/**
* 如果没有设置SA_INTERRUPT,说明中断处理程序是可以在开中断情况下执行的
* 这也是程序中少见的,调用local_irq_enable的地方。
* 一般来说,调用local_irq_enable是危险的,不允许,绝不允许。这里只是例外。
*/
if (!(action->flags & SA_INTERRUPT))
local_irq_enable();
/**
* 一开始,action是irqaction链表的头,irqaction表示一个ISR
*/
do {
/**
* handler是中断服务例程的处理函数。它接收三个参数:
* irq-IRQ号,它允许一个ISR处理几条IRQ。
* dev_id-设备号,注册中断服务例程时指定,此时回传给处理函数。它允许一个ISR处理几个同类型的设备。
* regs-指向内核栈的pt_regs。它允许ISR访问内核执行上下文。可是,哪个ISR会用它呢?
*/
ret = action->handler(irq, action->dev_id, regs);
if (ret == IRQ_HANDLED)
status |= action->flags;
/**
* 一般来说,handler处理了本次中断,就会返回1
* 返回0和1是有用的,这样可以让内核判断中断是否被处理了。
* 如果过多的中断没有被处理,就说明硬件有问题,产生了伪中断。
*/
retval |= ret;
action = action->next;
} while (action);
/**
* 如果中断是随机数的产生源,就添加一个随机因子。
*/
if (status & SA_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
/**
* 退出时,总是会关中断
*/
local_irq_disable();
return retval;
}
上面从do_IRQ()函数返回后,就跳转到ret_from_intr处执行,也就是中断的返回,Linux中断返回机制如图:
源码如下:
ret_from_intr:
/**
* 把当前thread_info半截到ebp中。
*/
GET_THREAD_INFO(%ebp)
/**
* 接下来判断EFLAGS和CS,确定是否运行在用户态,是否是VM模式。
*/
movl EFLAGS(%esp), %eax # mix EFLAGS and CS
movb CS(%esp), %al
testl $(VM_MASK | 3), %eax
/**
* 如果是运行在内核态,并且不是VM模式,就跳到resume_kernel,
* 否则跳转到resume_userspace
*/
jz resume_kernel # returning to kernel or vm86-space
/**
* 恢复用户态程序的流程入口。
*/
ENTRY(resume_userspace)
cli # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
/**
* 检查thread_info的flag
*/
movl TI_flags(%ebp), %ecx
/**
* 如果设置了_TIF_WORK_MASK中任何一位,就表示有等待处理的事情
* 跳到work_pending处理这些挂起的事件。
* 否则调用restore_all回到用户态。
*/
andl $_TIF_WORK_MASK, %ecx # is there any work to be done on
# int/exception return?
jne work_pending
jmp restore_all
#ifdef CONFIG_PREEMPT
/**
* 当从异常或者中断返回时,需要返回到内核,则跳转到此处。
*/
ENTRY(resume_kernel)
/**
* 不知道此处为何需要再加cli,如果是从中断或者异常跳转到这里,那么已经是关中断状态了。
* 也许是还有其他地方跳到这里吧。
*/
cli
/**
* 首先判断内核是否允许抢占,请请记住ebp中保存的是thread_info
*/
cmpl $0,TI_preempt_count(%ebp) # non-zero preempt_count ?
/**
* 当前不允许抢占,就继续执行内核代码。
*/
jnz restore_all
/**
* 否则抢占计数为0,就判断是否有调度需求。
*/
need_resched:
movl TI_flags(%ebp), %ecx # need_resched set ?
/**
* 判断是否有调度需求。
*/
testb $_TIF_NEED_RESCHED, %cl
/**
* 不需要调度,就继续执行内核代码。
*/
jz restore_all
/**
* 虽然有调度需求,但是当前是关中断状态,显然,这是不合理的。
* 这时回到用户态做什么呢?timer中断都可能被关了。系统怎么工作?
*/
testl $IF_MASK,EFLAGS(%esp) # interrupts off (exception path) ?
jz restore_all
/**
* preempt_schedule_irq会设置PREEMPT_ACTIVE标志,并把大内核锁暂时设置为-1。然后开中断并调用schedule。
*/
call preempt_schedule_irq
jmp need_resched
#endif
/**
* 在回到用户态前,如果有挂起的任务,就处理这些挂起的任务。
*/
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
/**
* 好了,运行到这里,说明没有重新调度的要求。
* 或者说有调度要求,但是调度出动后,又回来了。
* 总之,现在是没有调度要求了。在回到用户态前,处理信号。
* 需要注意的是:这个入口有不止从一个地方进入。
* 一是从上面两句转入,二是从更上面的jmp跳入。
* 接下来有两个事件需要处理:一是信号,二是VM86模式。
* 其中VM86模式我们不太关心。信号呢,很复杂的流程,至少需要一章才说清楚。也略过。
*/
work_notifysig: # deal with pending signals and
# notify-resume requests
testl $VM_MASK, EFLAGS(%esp)
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:
pushl %ecx # save ti_flags for do_notify_resume
call save_v86_state # %eax contains pt_regs pointer
popl %ecx
movl %eax, %esp
xorl %edx, %edx
call do_notify_resume
jmp restore_all
restore_all:
RESTORE_ALL
#define RESTORE_ALL \
RESTORE_REGS \
addl $4, %esp; \
1: iret; \
.section .fixup,"ax"; \
2: sti; \
movl $(__USER_DS), %edx; \
movl %edx, %ds; \
movl %edx, %es; \
movl $11,%eax; \
call do_exit; \
.previous; \
.section __ex_table,"a";\
.align 4; \
.long 1b,2b; \
.previous