2012年(11)
分类: LINUX
2012-09-15 21:16:39
中断控制器上有很多lines连接各种设备,还有一根line是连接CPU,用于通知CPU中断的发生。当CPU收到中断控制器发来的通知后,跳转到一个特定的位置去执行中断服务有关的代码,CPU要读取中断控制器的状态寄存器以知道是哪一根line发生了中断。每个驱动module需要使用某一个中断线都要先申请一个中断号,每一个中断号都对应一根中断line,中断号和中断控制器上的line(该line对应中断状态寄存器中的比特位)通常是一个偏移关系。CPU最终根据中断号执行相应的中断处理程序。
异常与中断的区别在于,异常通常是软件造成的中断,像之前系统调用时用的“int $0x80”指令,或是发生除数为0的情况,但因为是软件造成的,所以称为异常。
• 申请中断号
中断服务程序称为interrupt handler或是interrupt service routine(ISR)。因为中断服务程序打断了其他process,这个对时间要求很critical,所以通常分为两个步骤,上半部(top halves)和下半部(bottom halves)。上半部通常处理紧急的事物,比如操作硬件,将数据从硬件的buffer拷贝至内存等,而下半部主要是处理数据。注册一个中断服务程序的方法为:
int request_irq(unsigned int irq,irq_handler_t handler, unsigned long flags, const char *name, void *dev)
irq是申请的中断号,handler是中断处理函数,flags是标志位,dev用于区分不同的设备,name将显示在/proc/interrupts中。
一个常见的flag是SA_SHARED,它说明相应的中断控制器上的line被多个设备公用,所以同样的IRQ会有多个ISR。另一个flag是SA_INTERRUPT,若这种中断被触发,它会屏蔽当前CPU上的所有中断line,而普通的中断只会屏蔽自己的中断line(即,没有SA_INTERRUPT的中断,在ISR的过程中还可能被其他中断打断。)
request_irq应该放在模块中open函数的最后!因为申请好之后,就有可能被触发,此时设备与驱动应已经准备好处理中断。(若一个设备被open多次是否应避免?)
下图显示了一个系统中的中断情况:
最左边的是中断号,最右边的是中断控制器型号,当中的是中断次数。若在申请中断号之前不知道中断号是多少,那在申请之前可以探测一下,探测的原理是屏蔽系统中现有的所有中断号并使能未用的所有中断号,然后触发设备的中断,看系统中哪个中断号被触发了。Kernel有专门的语句帮助实现中断号的探测,LDD的第10章显示了这样的例子。
unsigned long probe_irq_on(void);//返回未使用的中断号
int probe_irq_off(unsigned long);
• 中断服务程序
typedef irqreturn_t (*irq_handler_t)(int, void *);
中断处理程序的返回值为IRQ_NONE或者IRQ_HANDLED,前者表示中断处理程序检测到了中断,但是中断源不是它对应的设备(共享中断发生后,每个ISR会检测各自设备的interrupt status register,从而判断中断源是不是它自己);后者表示中断源是它自己,并且已经处理了中断。也可以通过IRQ_RETVAL(val)来返回。
中断处理程序可以使不重入的,因为在isr的执行过程中,自己的interrupt line是被屏蔽的,但其他的interrupt lines还是使能的,这样做的目的是避免递归中断(nested interrupt)。
中断处理程序不是process(尽管current可以访问,但只表示被打断的process),而且运行它的时候,内核处于中断上下文(interrupt context),因为不是process,所以没有进程描述符task_struct,所以不能进程调度(包括那些可能会sleep的函数都不能执行)。而且中断处理程序可能会关闭中断,如果在其中sleep的话,中断就一直关着了(包括系统时间中断都不能响应,整个系统就瘫痪了)。
中断处理程序中也需要栈,历来都是使用被打断process的内核栈。内核栈比较小,只有8KB,两个page。因为每个process都需要两个page的内核栈,而内核栈又是要连续而且非交换内存,这对内存的需求量太大。所以2.6之后给每个CPU都分配了一个中断栈,大小为4KB,中断处理程序不再使用被打断的process的内核栈。
和之前描述的一样,当CPU收到中断信号的之后会跳转到特定的位置去执行特别的代码,这个特定的位置叫做entry point。这些特别的代码在图中有展示,他们的大体工作是:
保存被打断的process的现场(入栈)
do_IRQ()获得中断号并屏蔽对应的interrupt line
若对应的中断处理程序是有效的而且没有执行,则调用handle_IRQ_event()
handle_IRQ_event()执行该IRQ上的所有中断处理程序
恢复被打断的process的现场(出栈),但是这也不一定,因为中断返回可能会发生schedule(),这根据need_resched、preempt_count等变量来判断。
LDD的第10章展示了一个ISR的框架:
为了避免竞争,有时需要禁用中断,操作函数有
local_irq_disable() Disables local interrupt delivery,只针对本CPU
local_irq_enable() Enables local interrupt delivery
local_irq_save() Saves the current state of local interrupt and then disables it
local_irq_restore() Restores local interrupt delivery to the given state
disable_irq() Disables the given interrupt line,操作中断控制器吧?
enable_irq() Enables the given interrupt line
内核可以通过in_interrupt()知道它是否处于中断上下文。与in_ interrupt ()相对应的是in_atomic(),通过其可以知道是否需要原子化操作。比如在中断上下文和在获得spin_lock中,都属于处于“atomic”的区域。处于“atomic”区域的时候是可能有进程地址空间的!
• 下半部
下半部的是为了推延中断处理工作,这样可以让ISR尽快返回,中断处理函数(上半部)中通知数据的来临,下半部处理数据。下半部处于所有中断都使能的情况下。
一般来说下半部的事情都是靠内核来实现,若要让用户来实现呢?比如有些时候,用户进程读写buffer会在驱动中挂起(读buffer空或者写buffer满),中断只是是通知buffer符合条件了,这种时候就唤醒挂起的进程。LDD中还说了中断驱动的设计:
有三种下半部的方式:softirq、tasklets、workqueue
softirq只能在编译器静态注册,而tasklets可以动态注册。tasklets基于softirq的,tasklets可以看做是一种轻量级的softirq。请注意softirq和soft interrupt的区别,前者翻译成软中断,后者是软件中断,软中断不是中断,它是下半部,尽管它处于中断上下文。
系统中有如下这些softirq
softirq会在某些特定时刻得以执行:
中断或异常返回(包括system calls)
ksoftirqd内核线程
schedule会检查是否有处于pending软中断并执行
内核在这些时刻会检查softirq的状态,并依次执行处于pending状态的softirq,softirq之间不会互相抢占,能够抢占(打断)softirq也就是中断了。同一个softirq可以在不同CPU上同时执行。中断或异常返回时的执行流程为:
do_IRQ
generic_handle_irq //中断isr
irq_exit
invoke_softirq //软中断得以执行
do_softirq
h->action
wakeup_softirqd //内核线程ksoftirqd
一个softirq在运行的时候(其实“运行”指的是 softirq的handler在运行),同一个softirq可能被中断处理程序再次触发(设置为pending),其它CPU可以运行这个softirq。也就是同一个softirq可以在不同CPU上同时运行,这对数据的保护提出了要求(同步、防竞争),通常softirq的handler中都会用每个CPU专用的数据,对于共享数据,若是采用lock的方式,那么一个CPU在操作数据,另一个CPU就需要等待,这样同一个softirq在不同CPU可以同时运行的性质就没有意义,还不如考虑tasklets,后者是同一个tasklet不会在不同CPU上同时运行。
列举两个softirq handler的例子:
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
这两个是网络系统中驱动上一层收发包的程序。
触发softirq(设置为pending)的方法为,这样下一次do_softirq()的时候相应的softirq handler会得以执行:
raise_softirq(NET_TX_SOFTIRQ);
tasklets是基于softirq的,有两种tasklet,分别对应HI_SOFTIRQ和TASKLET_SOFTIRQ,前者的优先级更高。两种tasklet都有各自的链表———tasklet_vec和tasklet_hi_vec,这都是每个CPU专有的数据,链表中保存着tasklet_struct,每个tasklet_struct都代表一个tasklet。
触发tasklet(将其加入链表,这和softirq的pending有区别,所有的tasklet_vec是一种softirq,所有的tasklet_hi_vec是另一种softirq)使用tasklet_schedule()或tasklet_hi_schedule(),这两个函数所做的事情为:
检查该tasklet是否已经处于scheduled(pending)状态,若是,则马上返回(所以同种tasklet执行以前即使被触发多次,也只会执行一次);
将该tasklet加入tasklet_vec或tasklet_hi_vec
触发TASKLET_SOFTIRQ或HI_SOFTIRQ
总之,tasklet被设计成同类型的不能在SMP上同时运行;softirq被设计成同类型的可以在SMP上同时运行。所以softirq的设计更为复杂。同一个tasklet在执行或pending时,不能(实际是不会加入到tasklet链表)再次标记为pending(schedule或raise),但是softirq是可以的。
softirq可能运行的很频繁,为避免其他process难以获得运行机会,当内核检测到过多的softirq被触发时,通过ksoftirqd进程来处理softirq,该进程的优先级很低(nice值为19)。ksoftirqd的实现为:
workqueue也是下半部的一种方式,但是它处于进程上下文所以可以sleep,它的本质是利用内核线程来处理下半部任务,但是若是开发人员自己开发内核线程过于麻烦,所以就有了workqueue。workqueue的内核线程是events,和ksoftirq一样,每个CPU都有这样一个process。相关的数据结构如下,操作函数略:
三种下半部的对比
再提一下,相同的tasklet不会在不同CPU上同时运行,但是不同的tasklet仍然可能在不同的CPU上同时运行,若是不同的tasklet之间共享数据,需要进行保护。但同一个softirq是可以在不同CPU上同时运行的。
若是处于进程上下文的代码与下半部共享数据,也需要进行保护,这就需要禁用下半部和获取锁,控制函数为local_bh_disable()和local_bh_enable()。
若是中断处理程序与下半部共享数据,那么在下半部处理数据的时候,需要禁用中断和获取锁。
workqueue因为处于进程上下文,共享数据的保护方式与普通process一样。