最近工作比较闲,可以有挺多的时间来看内核,今天想把对linux 2.6内核中中断机制的一些理解记录一下,然后继续去看IPC方面的源码。
中断和异常的故事首先是从硬件中断控制器开始的。中断控制器以前是用PIC,如8059A,可以支持16个中断线,现在高级了,使用了APIC,可以支持多达256的中断线。
一个外部中断被触发后,CPU会自动关闭中断响应,然后去执行entry.S中的汇编代码。例行公事地进行一些寄存器压栈,获取中断号后,进入do_IRQ()函数。在进入do_IRQ后,首先调用irq_entry()。这个函数的主要工作是对thread_info中的preempt_count中的Hard_IRQ字段进行了递增。这会使in_interrupt()函数返回true,从而标志系统进入中断处理的上下文中。
继续往下,因为中断处理需要一些关键的信息,如中断处理函数的句柄等,而内核通过irq_desc结构体来登记这些信息。怎样去得到它呢?内核维护了一个irq_desc_ptrs的全局数组,通过irq_to_desc函数,将中断号转换为irq_desc结构体。
做完这些后,该是调用hander_irq()了。函数执行需要堆栈的支持,在内核中,异常的堆栈是属于进程的,而softirq 和 hardirq的堆栈是属于CPU的,即每个CPU分别维护了一个softirq 和 hardirq堆栈,可以通过__get_cpu_var这个宏获得。在hander_irq中,在获得hardirq的堆栈以后,我们通过一小段的内嵌汇编,将当前堆栈切换到了hardirq堆栈,并执行irq_desc中所指定的中断服务程序。
还需要指出的是,中断服务程序是在关中断的情况下进行的。为了不影响系统对中断的反应能力,在handlr里面,应该只执行一些关键的不需要耗费太多时间的操作,而把需要大量时间的操作放到softirq或者tasklet中来做。
在执行完hander_irq中,返回到do_IRQ中,调用irq_exit()。首先,他调用了preempt_enable_no_reshued()将preempt_count中的handirq字段减去handirq-1。但是,注意,这里preempt_count没有减为0,最少也是1。因为该字段控制了该进程是否可以被调度,而到目前为止,还在中断处理上下文中,当前进程还没有准备好接受调度,所以不能将preempt_count减为0,但in_interrupt()函数已经会返回false了,因为他是通过判断preempt_count中的handirq和softirq字段是否置位来决定返回true还是false的。
irq_exit()通过判断in_interrupt()来判断中断是否发生了嵌套,如果没有,并且local_softirq_pengding不为零,则调用do_softirq,来执行软中断。值得一提的是,local_softirq_pending也是属于CPU的,是一个用来标识软中断的位图。do_softirq一开始的过程与hander_irq的过程类似。只是它会将堆栈切换到softirq的堆栈中,置preempt_count中的softirq位,并使能中断。所以,softirq的执行过程,是可以被中断的,但不能被调度。即handirq的中断服务程序和softirq中的中断服务程序都不能被阻塞。
那tasklet在那里被执行呢?其实tasklet就是挂在softirq链上的。内核总共有六个softirq,其中优先级最高的和优先级最低的都是用来挂tasklet的。还有,softirq和tasklet 的区别是softirq有可能被多个CPU同时执行,因此,它应该是可重入的;而tasklet的执行是串行的,所以不用考虑重入醒。softirq一般在系统编译时就已经写了,而tasklet可以在系统运行的过程的被添加(比如驱动程序中)。
由于do_softirq的过程中是开中断的,所以一些中断可能会不断的挂softirq(比如时间中断,触发的很频繁),从而导致一些高优先级的进程得不到即时的响应。内核为了处理这个问题,限制了在__do_softirq中softirq可以被服务的次数,比如说10次,而把剩余的softirq交给ksoftirq内核进程来出来。因为ksoftirq是独立的内核线程,所以,它可以被调度。
但需要注意的是,无论是hardirq的中断服务程序或者是softirq的中断服务程序都不能访问用户级的数据。因为handirq是在内核控制路径中执行的,虽然它是与发生该中断时正在执行的线程绑定的,但它是不允许改变该进程的任何数据的,而softirq的中断服务程序,即使是在ksoftirq中被执行的那些,因为内核线程不包含用户地址空间,也导致了,访问不了用户级的数据。
在__do_irq返回以后,内核调用local_irq_disable()又禁止了中断,然后从irq_exit返回,再从do_IRQ中返回到entry.S中,在entry.S中,可能会发生进程的调度,从内核态返回到用户态等一系列的操作,有兴趣的朋友可以自己分析。下图是整个do_IRQ()的函数调用流程: