Linux中断:软中断及tasklet

软中断及tasklet

内核中断不是很紧急的在必要的情况下可以延迟一段时间。
一个中断处理程序的几个中断服务例程之间是串行执行的,并且通常在一个中断的处理程序结束前,不应该再次出现这个中断。相反,可延迟中断可以在开中断的情况下执行。把可延迟中断从中断处理程序中抽出来有助于使内核保持较短的响应时间。这对于那些期望他们的中断能在几毫秒内得到处理的”急迫”应用来说非常重要。
软中断和tasklet有密切的关系,tasklet是在软中断之上实现的。事实上,出现在内核代码中的术语”软中断(softirq)”常常表示可延迟函数的所有种类。另外一种被广泛使用的术语是”中断上下文”:表示内核当前正在执行一个中断处理程序或一个可延迟的函数。

软中断的分配是静态的(即在编译时定义),而tasklet的分配和初始化可以在运行时进行(例如:安装一个内核模块时)。
软中断(即便是同一种类型的软中断)可以并发地运行在多个CPU上。因此,软中断是可重入函数而且必须明确地使用自旋锁保护其数据结构。tasklet不必担心这些问题,因为内核对tasklet的执行进行了更加严格的控制。相同类型的tasklet总是被串行地执行,换句话说就是:不能在两个CPU上同时运行相同类型的tasklet。
但是,类型不同的tasklet可以在几个CPU上并发执行。tasklet的串行化使tasklet函数不必是可重入的,因此简化了设备驱动程序开发者的工作。

一般而言,在可延迟函数上可以执行四中操作:
1. 初始化: 定义一个新的可延迟函数;这个操作通常在内核自身初始化或者加载模块时进行
2. 激活(activation):标记一个可延迟函数为”挂起”(在可延迟函数的下一轮调度中执行)。激活可以在任何时候进行(即使正在处理终端)
3. 屏蔽(masking):有选择地屏蔽一个可延迟函数,这样,即使它被激活,内核也不执行它。
4. 执行(execution):执行一个挂起的可延迟函数和同类型的其他所有挂起的可延迟函数;执行是在特定的时间进行的。

激活和执行不知何故总是捆绑在一起:由给定CPU激活的一个可延迟函数必须在同一个CPU上执行。没有什么明显的理由说明这条规则对系统性能是有益的。把可延迟函数绑定在激活CPU上从理论上说可以更好地利用CPU的硬件高速缓存。毕竟,可以想象,激活的内核线程访问的一些数据结构,可延迟函数也可能会使用。然而,当可延迟函数运行时,因为它的执行可以延迟一段时间,因此相关高速缓存很可能就不再在高速缓存中了。此外,把一个函数绑定在一个CPU上总是一种潜在”危险的”操作,因为一个CPU可能忙死而其他CPU又无所事事。

软中断

Linux 2.6使用有限个软中断。在很多场合,tasklet是足够用的,且更容易编写,因为tasklet不必是可重入的。
如下表所示,目前只定义了六种软中断。

软中断 下标 说明
HI_SOFTIRQ 0 处理高优先级的tasklet
TIMER_SOFTIRQ 1 和时钟中断相关的tasklet
NET_TX_SOFTIRQ 2 把数据包传送到网卡
NET_RX_SOFTIRQ 3 从网卡接收数据包
SCSI_SOFTIRQ 4 SCSI命令的后台中断处理
TASKLET_SOFTIRQ 5 处理常规tasklet

一个软中断的下标决定了它的优先级:低下标意味着高优先级,因为软中断函数将从下标0开始执行。

软中断中所使用的数据结构

表示软中断的主要数据结构是softirq_vec数组,该数组包含类型为softirq_action的32个元素,一个软中断的优先级是相应的softirq_action元素在数组内的下标。如表4-9所示,只有数组的前六个元素被有效地使用。softirq_action数据结构包括两个字段;指向软中断函数的一个action指针和指向软中断函数需要的通过数据结构的data指针。
另外一个关键的字段是32位的preempt_count字段,用它来跟踪内核抢占和内核控制路径的嵌套,该字段存放在每个进程描述符的thread_info字段中。如表4-10所示,preempt_count字段的编码表示三个不同的计数器和一个标志。

表4-10:preempt_count的字段

描述
0~7 抢占计数器(max value = 255)
8~15 软中断计数器(max value = 255)
16~27 硬终端计数器(max value = 4096)
28 PREEMPT_ACTIVE标志

第一个计数器记录显式禁用本地CPU内核抢占的次数,值等于0表示允许内核抢占。第二个计数器表示可延迟函数被禁用的程度。第三个计数器表示在本地CPU上中断处理程序的嵌套数。
给preempt_count字段起这个名字的理由是很充分的:当内核代码明确不允许发生抢占或当内核下在中断上下文中运行是,必须禁用内核的抢占功能。因此,为了确定是否能够抢占当前进程,内核快速检查preempt_count字段中的相应值是否等于0。在第五章”内核抢占”一节将深入讨论内核抢占。
宏in_interrupt()检查current_thread_info()->preempt_count字段的硬中断计数器和软中断计数器,只要这两个计数器中的一个值为正数,该宏就产生一个非零值否则产生一个零值。如果内核不使用多内核栈,则该宏只检查当前进程的thread_info描述符的preempt_count字段。但是,如果内核使用多内核栈,则该宏可能还要检查本地CPU的irq_ctx联合体中thread_info描述符的preempt_count字段。在这种情况下,由于该字段总是正数值,所以宏返回非零值。
实现软中断的最后一个关键的数据结构是每个CPU都有的32位掩码,它存放在irq_cpustat_t数据结构的__softirq_pending字段中。为了获取或设置位掩码的值,内核使用宏local_softirq_pending(),它选择本地CPU的软中断位掩码。

处理软中断

open_softirq()函数处理软中断的初始化。它使用三个参数;软中断下标、指向要执行的软中断函数的指针及指向可能由软中断函数使用的数据结构的指针。open_softirq()限制自己初始化softirq_vec数组中适当的元素。
raise_softirq()函数用来激活软中断,它接受软中断下标nr做为参数,执行下面的操作:
执行local_irq_save宏以保存eflags寄存器IF标志的状态值并禁用本地CPU上的中断。
把软中断标记为挂起状态,这是通过设置本地CPU的软中断掩码中与下标nr相关位来实现的。
如果in_interrupt()产生为1的值,则跳转到第5步。这种情况说明:要么已经在中断上下文中调用了raise_softirq(),要么当前禁用了软中断。
否则,就在需要的时候去调用wakeup_softirqd()以唤醒本地CPU的ksoftirqd内核线程。
执行local_irq_restore宏,恢复在第1步保存的IF标志的状态值。
应该周期性地检查活动的软中断,检查是在内核代码的几个点上进行的。这在下列几种情况下进行。
当内核调用local_bh_enable()函数激活本地CPU的软中断时。
当do_IRQ()完成了I/O中断的处理是或调用irq_exit()宏时。
如果系统使用I/OAPIC,则当smp_apic_timer_interrupt()函数处理完本地定时器中断时。
在多处理器系统中,当CPU处理完被CALL_FUNCTION_VECTOR处理器间中断所触发的函数时。
当一个特殊的ksoftirqd/n内核线程被唤醒时。

do_softirq()函数

如果在这样的一个检查点检测到挂起的软中断,内核就调用do_softirq()来处理它们。这个函数执行下面的操作。
如果in_interrup()产生的值是1,则函数返回。这种情况说明要么在中断上下文中调用了do_softirq()函数,要么当前禁用软中断。
执行local_irq_save以保存IF标志的状态值,并禁止本地CPU上的中断。
如果thread_union的结构大小为4KB,那么在需要情况下,它切换到软中断请求栈。这一步与前面”I/O中断处理”一节do_IRQ()的第2步很相似,当然这里面使用的是数组softirq_ctx而不hardirq_ctx。
调用__do_softirq()函数。
如果在上面第3步成功切换到软中断请求栈,则把最初的栈指针恢复到esp寄存器中,这样就切换回到以前使用的异常栈。
执行local_irq_restore以恢复在第2步保存的IF标志的状态值并返回。

__do_softirq()函数

__do_softirq()函数读取本地CPU的软中断掩码并执行行与每个设置位相关的可延迟函数。由于正在执行一个软中断函数时可能出现新挂起的软中断,所以为了保证可延迟函数的低延迟性,__do_softirq()一直运行到执行完所有挂起的软中断。但是,这种机制可能迫使__do_softirq()运行很长一段时间,因而大大延迟用户态进程的执行。因此,__do_softirq()只做固定次数的循环,然后就返回。如果还有其余挂起的软中断,那么下一节要描述的内核线程ksoftirqd将会在预期的时间内处理它们。下面简单描述__do_softirq()函数执行的操作:
把循环计数器的值初始为10。
把本地CPU软中断的位掩码复制到局部变量pending中。
调用local_bh_disable()增加软中断计数器的值。在可延迟函数开始执行之前应该禁用它们,这似乎有点违反直觉,但确实极有意义。因为在绝大多数情况下可能会产生新的中断。当do_IRQ()执行irq_exit()宏时,可能有另外一个__do_softirq()函数的实例开始执行。这种情况是应该避免的,因为可延迟函数必须以串行的方式在CPU上运行。因此,__do_softirq()函数的第一实例禁用可延迟函数,以使每个新的函数实例将会在__do_softirq()函数的第1步就退出。
清除本地CPU的软中断位图,以便可以激活新的软中断。
执行local_irq_enable()来激活本地中断。
根据局部变量pending每一位的设置,执行对应的软中断处理函数。回忆一下,下标n的软中断函数的地址存放在softirq_vec[n]->action变量中。
执行local_irq_disable()以禁用本地中断。
把本地CPU的软中断位掩码复制到局部变量pending中,并且再次递减循环计数器。
如果pending不为0,那么从最后一次循环开始,至少有一个软中断被激活,而且循环计数器仍然是正数,跳转回到第4步。
如果还有更多的挂起软中断,则调用wakeup_softirqd()唤醒内核线程来处理本地CPU的软中断。
软中断计数器减1,因而重新激活可延迟函数。

Ksoftirqd内核线程

在最近的内核版本中,每个CPU都有自己的ksoftirqd内核线程。每个ksoftirqd/n内核线程都运行ksoftirqd()函数,该函数实际上执行下列的循环:

当内核线程被唤醒时,就检查local_softirq_pending()中的软中断位掩码并在必要时调用do_softirq()。如果没有挂起的软中断,函数把当前进程状态置为TASK_INTERRUPTIBLE,最后,如果当前进程需要就调用cond_resched()函数来实现进程切换。
ksoftirqd/n内核线程为重要而难以平衡的问题提供了解决方案。
软中断函数可以重新激活自己,实际上,网络软中断和tasklet软中断都可以这么做。此外,像网卡上数据包泛滥这样的外部事件可能以高频激活软中断。
软中断的连续高流量可能会产生问题,该问题就是由引入的内核线程来解决的。没有内核线程,开发者实际上就面临两种选择策略。
第一种策略就是忽略do_softirq()运行时新出现的软中断。换句话说,do_softirq()函数开始执行时,确定哪些软中断是挂起的,然后执行这些软中断的函数。接下来,do_softirq()不再重新检查挂起的软中断就终止。这种解决方法不是很好。假设一个软中断函数在do_softirq()执行期间被重新激活。在最坏情况下,即使机器空闲,也只有在下一次时钟中断到来时,该中断才被再执行。结果,对网络开发都来说,软中断的等待时间是不可接受的。
第二种策略在于不断地重新检查挂起的软中断。do_softirq()函数一直检索挂起的软中断,只有在没有挂起的软中断是才终止。尽管这种解决方法可能满足了网络开发者的愿望,但是,它肯定会使系统中的普通用户感到恼怒:如果网卡接收高频率的数据包流,或者如果一个软中断函数总是激活自己,那么,do_softirq()函数就会永远不返回,用户态程序实际上就停止执行。
ksoftirqd/n内核线程试图解决这种很难平衡的问题。do_softirq()函数确定哪些软中断是挂起的,并执行它们的函数。如果已经执行的软中断又被激活,do_softirq()函数则唤醒内核线程并终止。内核线程有较低的优先级,因此用户程序就有机会运行;但是,如果机器空闲,挂起的软中断就很快被执行。

tasklet

tasklet得I/O驱动程序中实现可延迟函数的首选方法。如前所述,tasklet建立在两个叫做HI_SOFTIRQ和TASKLET_SOFTIRQ的软中断之上。几个tasklet可以与同一个软中断相关联,每个tasklet执行自己的函数。两个软中断之间没有真正的区别,只不过do_softirq()先执行HI_SOFTIRQ的tasklet,后执行TASKLET_SOFTIRQ的tasklet。
tasklet和高优先级的tasklet分别存放在tasklet_vec和tasklet_hi_vec数组中。二都都包含类型为tasklet_head的NR_CUPS个元素,每个元素都由一个指向tasklet描述符链表的指针组成。tasklet描述符是一个tasklet_struct类型的数据结构,其字段如表4-11所示。

Tasklet描述符的state字段含有两个标志:
TASKLET_STATE_SCHED
该标志被设置时,表示tasklet是挂起的;也意味着tasklet描述符被插入到tasklet_vec和tasklet_hi_vec数组的其中一个链表中。
TASKLET_STATE_RUN
该标志被设置是,表示tasklet正在被执行;在单处理器系统上不使用这个标志,因为没有必要检查特定的tasklet是否在运行。
让我们假定,你正在写一个设备驱动程序,且想使用tasklet,应该做些什么呢?首先,你应该分配一个新的tasklet_struct数据结构,并调用tasklet_init()初始化它;该函数接收的参数为tasklet描述符的地址,tasklet函数的地址和它的可选整形参数。

调用tasklet_disable_nosync()或tasklet_disable()可以选择性地禁止tasklet。这两个函数都增加tasklet描述符的count字段,但是最后一个函数只有在tasklet函数已经运行的实例结束后才返回。为了重新激活你的tasklet。调用tasklet_enable()。
为了激活tasklet,你应该根据自己tasklet需要的优先级,调用tasklet_schedule()函数或tasklet_hi_schedule()函数。这两个函数非常类似,其中每个都执行下列操作:
检查TASKLET_STATE_SCHED标志;如果设置则返回
调用local_irq_save保存IF标志的状态并禁用本地中断。
在tasklet_vec[n]或tasklet_hi_vec[n]指向的链表的起始处增加tasklet描述符。
调用raise_softirq_irqoff()激活TASKLET_SOFTIRQ或HI_SOFTIRQ类型的软中断。
调用local_irq_restore恢复IF标志的状态。
最后,让我们看一下tasklet如何被执行。我们从前一节知道,软中断函数一旦被激活,就由do_softirq()函数执行。与HI_SOFTIRQ软中断相关的软中断函数叫做tasklet_hi_action()。而与TASKLET_SOFTIRQ相关的函数叫做tasklet_action()。这两个函数非常相似。它们都执行下列操作:
禁用本地中断。
获得本地CPU的逻辑号。
把tasklet_vec[n]或tasklet_hi_vec[n]指向的链表的地址存入局部变量list.
把tasklet_vec[n]或tasklet_hi_vec[n]的值赋为NULL,因此,已调度的tasklet描述符的链表被清空。
打开本地中断。
对于list指向的链表中的每个tasklet描述符
注意,除非tasklet函数重新激活自己,否则,tasklet的每次激活至多触发tasklet函数的一次执行。

参考:http://blog.csdn.net/droidphone/article/details/7518428

你可能感兴趣的:(linux)