中断处理程序的局限
中断处理程序异步进行,有可能打断其他重要代码,所以中断处理程序执行时间要越短越好。
如果当前有一个中断处理程序正在执行,最好的情况下,同级其他中断会被屏蔽;最坏的情况下,处理器上其他所有中断都会被屏蔽。所以中断处理程序执行时间要越短越好
中断处理程序往往需要对硬件操作,所以通常有很高的时限要求。
不在进程上下文中运行,所以不能阻塞,这限制了他们能做的事情。
所以中断适合完成快速,异步,简单并需要作出迅速响应的机制。下面研究中断处理程序中的另一部分-下半部
下半部
最理想的情况下中断处理程序将所有的工作交给下半部执行。
下半部的实现环境
完成下半部程序有多种不同的方法
软中断是一组静态定义的下半部接口,有32个,可以在所有处理器上同步执行
类型相同的软中断可以同时直行,所以需要小心
软中断必须在编译期间就进行动态注册
tasklet是一种基于软中断实现的灵活性强、动态创建的下半部实现机制,两个不同类型的tasklet可以在不同的处理器上同时执行,但类型相同的不能同时执行。
tasklet是一种在性能和易用性上寻求平衡的产物,对于大部分下半部处理来说,用tasklet就足够了。只有像网络这种性能要求非常高的情况才需要使用软中断。
可以通过代码进行动态注册
软中断kernel/softirq.c
软中断的结构体定义<linux/interrupt.h>
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
软中断的声明 <kernel/softirq.c>
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
定义了包含32个上述结构体的数组
static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;
软中断处理程序
程序原型
该函数为修改源码时自行定义,在原始的代码中貌似找不到
void softirq_handler(struct softirq_action *)
执行软中断
下列情况下会触发软中断
无论通过什么方法,都在do_softirq()中执行
其中的核心过程即为遍历每一个软中断调用处理程序
软中断在执行的过程中允许响应中断但他自己不能休眠,在一个处理程序运行的时候,当前处理器上的软中断被禁止,但其他处理器仍可以执行别的软中断。也就是同一个软中断可以被不同的处理器同时执行,那么任何共享数据(甚至是仅在软中断内部使用的全局变量)都需要严格的加锁保护。
因为上述原因,大部分的软中断处理程序都通过单处理器数据或其他技巧来避免显式加锁,从而实现更好的性能。
使用软中断
分配索引
注册处理程序
open_softirq(NET_TX_SOFTIRQ,net_tx_action);
open_softirq(NET_RX_SOFTIRQ,net_rx_action);
触发软中断
raise_softirq(NET_RX_SOFTIRQ);
可以将软中断设置为挂起装填,下次调用do_softirq()将投入运行。
void fastcall raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
local_irq_save() 功能为保存本地中断传递的当前状态,然后禁止本地中断
所以如果本地中断本身就已经是禁止状态,那么可以调用raise_softirq_irqoff(nr);实现更好的效果。
tasklet
tasklet和软中断很相近,但是接口更简单,锁保护要求也较低
通常应该选用tasklet,软中断只应用于执行频率和连续性都很高的场合才使用,而tasklet有更广泛的用途
tasklet的结构体
struct tasklet_struct
{
struct tasklet_struct *next;//链表中下一个tasklet
unsigned long state;//tasklet状态
atomic_t count;//引用计数器
void (*func)(unsigned long);//处理函数
unsigned long data;//给处理函数的参数
};
tasklet调度
已调度的tasklet(等同于被触发的软中断)存放在两个单处理器数据结构,都是由tasklet_struct结构体构成的链表。链表中的每一个tasklet_struct代表一个不同的tasklet
tasklet由tasklet_schedule()和tasklet_hi_schedule()进行调度,其接受一个执行tasklet_struct结构的指针作为参数,两个函数非常类似
tasklet_schedule的执行过程
上述过程中软中断被唤醒,下次调用do_softirq()时就会通过调用tasklet_action()执行该tasklet
tasklet的执行步骤
使用tasklet
可以静态创建也可以动态创建,取决于对一个tasklet直接引用还是间接引用,静态创建具体操作使用下列函数
DECLARE_TASKLET(name,func,data)
DECLARE_TASKLET_DISABLE(name,func,data)
前一个函数把创建的tasklet的计数器设置为0表示处于激活态,后一个设置为1处于禁止态
动态创建使用tasklet_init(t,tasklet_handler,dev);
编写tasklet处理程序
void tasklet_handler(unsigned long data)
tasklet使用软中断实现所以不能休眠,所以不能使用信号量或阻塞式函数,
调度tasklet
tasklet_schedule(&mytasklet);
一个tasklet总在调度他的处理器上执行,从而可以更好的利用处理器缓存
ksoftirqd
每个处理器都有一组辅助处理软中断和tasklet的内核线程,当内核中出现大量的软中断时,这些内核进程就会辅助处理他们。tasklet也是通过软中断实现,所以下面主要讨论软中断
对于软中断,内核会选择在几个特殊时期进行处理,在中断处理程序返回时处理最常见。而且比如网络子系统,会在软中断执行的时候,重新触发自己从而可以再次执行。如果软中断本身出现的频率就高而且又有执行自己的能力,那么就会导致用户空间进程无法获得足够的处理器时间而处于饥饿状态。
软中断的直观处理策略
每个处理器都会一个这样的线程,叫做ksoftirqd/n,区别在于n,表示处理器的编号,如双核CPU有两个线程ksoftirqd/0和ksofirqd/1,从而只要有空闲的处理器就会处理软中断。
一旦该线程被初始化,就会执行死循环:(在具体的源码文件中没有找到)
softirq_pending负责发现是否有待处理的终端,如果有的话通过do_softirq()进行处理,重复执行该操作,每次迭代调用schedule()以便让更重要的进行得到处理的机会,当所有的进程都操作完成以后,该内核线程将自己设置为TASK_INTERRUPTIBLE状态,唤醒调度程序选择其他可执行进程。
老的BH机制
在2.6版的内核中已经找不到该机制了
所有的BH都是静态定义的,最多可以有32个,实现模块时不能直接使用BH接口。每个BH处理程序严格的按顺序执行,不能同时执行,不利于多处理器的可扩展性和大型SMP的性能。
工作队列
工作队列是一种将工作推后执行的形式,交由一个内核线程去执行。这样通过工作队列执行的代码能占尽上下文的所有优势,允许重新调度甚至睡眠。
如果推后执行的任务需要睡眠,通常采用工作队列;反之则选择软中断或者tasklet。工作队列通常可以用内核线程替换,但是由于内核开发者反对创建新的内核线程,所以也推荐使用工作队列。
如果需要一个可以重新调度的实体来执行下半部,应该使用工作队列,这是唯一能在进程上下文中运行的下半部实现机制。也只有它可以睡眠。这意味着在需要获得大量内存,需要获取信号量,需要执行阻塞式IO操作时,都会非常有用。
工作队列的实现
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列中的任务。它创建的这些内核线程成为工作者线程。工作队列可以让驱动程序创建一个专门的工作者线程来处理推后的工作。工作队列子系统提供了一个缺省的工作者线程来处理这些工作,所以工作队列的最基本表现形式为把需要推后执行的任务交给特定的通用线程的一种接口。
缺省的工作者线程为events/n,n为处理器编号,每个处理器对应一个线程。缺省的工作者线程会从多个地方得到被推后的工作。许多内核驱动程序都把他们的下半部交给缺省的工作者线程来做。除非一个驱动程序或者子系统必须建立一个属于自己的内核线程,否则最好使用缺省线程。
工作队列的具体工作没有详细的看,见书121-127页
下半部机制的选择
三种可能的选择
如果需要一个可调度(需要休眠)的实体来执行推后完成的工作,工作队列是唯一的选择。
专注于性能的提高,则应该选择软中断。
上下文切换之间加锁
使用tasklet的一个好处是它自己负责执行序列化的保障,即使在不在同一个处理器上,两个相同类型的tasklet都不允许同时执行,所以无需考虑同步问题,tasklet之间的同步需要正确的使用锁机制。
进程上下文和下半部需要共享数据在访问数据之前需要禁止下半部的处理并得到锁的使用权,可以本地和SMP的保护并防止死锁的出现。
工作队列中共享的数据也需要使用锁机制。
禁止下半部
一般单纯的禁止下半部的处理是不够的,为了保证共享数据的安全一般是先得到一个锁然后再禁止下半部的处理。
如果需要禁止所有的下半部处理(所有软中断和所有tasklet)可以调用local_bh_disable()函数。
允许下半部处理可以调用local_bh_enable()函数。
函数通过preempt_count为每个进程维护一个计数器,当计数器变为0时,下半部才能够被处理。