在之前我所写的Linux驱动程序中,会经常使用到中断机制,像CC1100高频驱动、倒车雷达驱动等等。但所用到的中断机制都基本上是用到中断的顶半部,即:编写中断处理函数,通过request_irq函数申请中断,这样当中断来临的时候,就会自动执行中断处理程序里面的内容。之所以没有使用到中断的底半部,是因为我们这些驱动程序中,中断处理函数一般都能被很快执行完,同时也不会存在有任何休眠的动作,因此使用中断的顶半部对于我们这些驱动程序来说,反而相对简单一些。因此这也就得出,并不是任何中断程序都一定会使用到中断的底半部。
中断顶半部
对于中断的顶半部,我想大部分的关于Linux驱动的书上都会有详细的讲解,并且这一块理解和实践起来都比较容易,但这里我需要讲解的是关于共享中断的这一部分,因为这一块可能对于一些初学者会有一点难度。
共享中断是指多个设备共享一根中断线(中断线在这里可以理解为中断号,也就是说多个设备共享一个中断号),为什么会有这种情况发生,因为在Linux内核中,中断线的数目是有限的,如果每一个设备都使用一根中断线的话,中断线肯定是不够的,所以聪明的Linux内核设计师们就提出了共享中断这一理念。这里理念的主要目的就是可以在一根中断线上搭载多个中断设备。那么好了,现在问题也来了,既然都在同一个中断线上,如果中断来了的话,要如何判断该中断来自于哪一个设备呢?其实对于Linux内核来说,要判断其来自哪一个设备,其需要做两步工作。当一个中断来临时,Linux内核会遍历该中断线上所有注册了的中断处理程序,在该中断处理程序中,就会迅速判断到底是来自于哪一个硬件设备。而在中断程序中如何来判断呢?这就需要相应产生中断的硬件设备来支持了。例如可能中断处理程序会检查一下该处理程序对应的硬件设备的某一寄存器的状态来判断是否该设备发生了中断,如果是该设备发出的中断,就执行接下来的处理函数。如果不是,就立即返回(应该返回IRQ_RETVAL(IRQ_NONE))。
首先我们来看一下在申请共享中断的过程与一般申请中断有哪些不同。
我们知道申请注册中断的函数是:
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
中断上下文
在这里顺便提一下中断上下文,当我们执行一个中断处理函数时,内核就会处于中断上下文(Interrupt Context)中。与进程上下文不同,中断上下文与进程并没有什么关系。与current宏也没有任何关系,尽管此时若使用的current标志位的话,其任然是指向被中断的进程。由于中断上下文不依赖与进程,因此中断上下文不能休眠,不能在中断上下文中调用某些可能引起休眠的函数。
由于中断上下文可以打断其他正在执行的代码,因此,中断上下文在执行时间上由严格的时间限制。中断上下文中的代码需要尽可能简洁,尽量不要使用循环或者是耗时比较长的函数来处理中断任务。这是由于中断上下文已经打断了其他正在执行的代码,甚至可能是其他的中断处理程序,因此中断处理程序应该快速地执行完,否则可能会使其他被打断的程序长时间等待而造成系统性能下降甚至崩溃。当然,在中断上下文中处理复杂耗时的任务也在所难免,但最好将这部分任务放在中断的底半部(主要因为中断的底半部,可以被其他甚至是同类型的中断打断,并且中断底半部函数是异步执行。)。这样既可以很快地执行完中断处理程序(尽快回复被中断的代码),又可以在中断程序中完成很复杂的任务。后面的软中断或者是tasklet都属于中断的上下文中。
在Linux2.6内核中,中断处理程序拥有自己的栈,每一个处理器一个,大小为一页(4KB),尽管中断栈并不算大,但平均可用栈空间要比Linux内核的其他程序大得多。因为中断程序把这一页据为己有。在我们编写中断处理程序时,并不需要关心如何设置中断栈或内核栈的大小,总之,尽量节约中断栈的空间就行了。
中断的底半部
下面我们来开始讲解中断底半部,如果用一个词来形容底半部的功能,就是“延迟执行”,为什么要这样说呢,后面分析过后就会深刻理解这一点了。在中断的上半部,即中断处理程序结束前,当前的中断线在所有的处理器上都会被屏蔽,如果在申请中断线时使用了IRQF_DISABLED,那么情况会更加糟糕,在中断处理程序执行时会禁止所有的本地中断。因此尽可能地缩短中断被屏蔽的时间对系统的响应能力和性能都至关重要。因此,要将耗时较长的任务放到底半部延迟执行。因为底半部并不禁止其他中断上半部的执行(哪怕是自己的中断处理函数)。对于中断底半部的实现方式一共有三种;
void irq_exit(void)
{
account_system_vtime(current);
trace_hardirq_exit();
sub_preempt_count(IRQ_EXIT_OFFSET);
if (!in_interrupt() && local_softirq_pending())
//判断是否有软中断被请求,主要是看是否有执行raise_softirq函数,
invoke_softirq(); //用于唤醒软中断,即会激活do_softirq函数
rcu_irq_exit();
#ifdef CONFIG_NO_HZ
/* Make sure that timer wheel updates are propagated */
if (idle_cpu(smp_processor_id()) && !in_interrupt() && !need_resched())
tick_nohz_stop_sched_tick(0);
#endif
preempt_enable_no_resched();
}
struct softirq_action
{
void (*action)(struct softirq_action *); //函数指针名为action,其中参数类型为一个
};
其实这个函数指针就是指向该软中断的处理函数。
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action; //指定软中断处理函数指针。
}
我们可以发现,这里出现了一个softirq_vec数组,该数组类型当然是softirq_action类型的了,数组的下标表示了不同的软中断类型。下标越小,软中断的优先级越高。下面是不同的下标表示不同的类型的软中断,其用一个枚举来表示。
enum
{
HI_SOFTIRQ=0, //优先级最高的软中断,用于tasklet
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ, //发送网络数据的软中断
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ, //tasklet软中断
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS //该枚举值就是当前Linux内核允许注册的最大软中断数
};
这里需要注意的是softirq_vec对于整个Linux内核是全局的,所以任何一个软中断的处理程序都是全局唯一的,即softirq_vec[nr].action = action中一旦指定了其软中断处理程序的方法,则这个类型的软中断的处理程序就确定了。
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags); //保存中断状态,禁止中断
raise_softirq_irqoff(nr); //挂起相应的中断类型
local_irq_restore(flags); //恢复中断。
}
所以其实raise_softirq函数真正调用的是raise_softirq_irqoff(nr)函数,同时从这里我们也知道了,一个软中断类型,只能对应一个软中断处理程序,而若要使用tasklet类型的软中断的话,就必须要执行raise_softirq_irqoff(TASKLET_SOFTIRQ)函数。
asmlinkage void do_softirq(void)
{
__u32 pending;
unsigned long flags;
if (in_interrupt())
return;
local_irq_save(flags);
pending = local_softirq_pending(); //再获取pending标志位,是否有软中断处理程序被raise
if (pending)
__do_softirq(); //如果有,则执行_do_softirq函数
local_irq_restore(flags);
}
所以do_softirq函数真正起作用的是_do_softirq函数。
asmlinkage void __do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;
pending = local_softirq_pending();
account_system_vtime(current);
__local_bh_disable((unsigned long)__builtin_return_address(0),
SOFTIRQ_OFFSET);
lockdep_softirq_enter();
cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);
local_irq_enable();
h = softirq_vec;
do {
if (pending & 1) { //将pending不同的为与1相"&",来确定哪种类型的软中断被挂起了
unsigned int vec_nr = h - softirq_vec; //获取softirq_vec数组的下标值,该下标值也就确定了软中断属于什么类型了
int prev_count = preempt_count();
kstat_incr_softirqs_this_cpu(vec_nr);
trace_softirq_entry(vec_nr);
h->action(h); //在这里执行软中断的处理函数action
trace_softirq_exit(vec_nr); //执行完该软中断处理程序之后,就应该将挂起的标志位重新置0,将相应的_softirq_pending
if (unlikely(prev_count != preempt_count())) {
printk(KERN_ERR "huh, entered softirq %u %s %p"
"with preempt_count %08x,"
" exited with %08x?\n", vec_nr,
softirq_to_name[vec_nr], h->action,
prev_count, preempt_count());
preempt_count() = prev_count;
}
rcu_bh_qs(cpu);
}
h++;
pending >>= 1;
} while (pending);
local_irq_disable();
pending = local_softirq_pending();
if (pending && --max_restart)
goto restart;
if (pending) //重新获取的pending,如果其不为0,说明又有新的软中断处理程序被挂起,如果待处理的软中断程序过多,就应该开启Ksoftirq线程。从而达到延时目的
wakeup_softirqd(); //开启Ksoftirq线程(软中断处理线程),即将Ksoftirq线程加入至可运行队列
lockdep_softirq_exit();
account_system_vtime(current);
__local_bh_enable(SOFTIRQ_OFFSET);
}
关于Ksoftirq线程后面会详细讲解。到此软中断的工作原理就全部讲解完了,理解了软中断的机制,再去理解tasklet就容易很多了。
struct tasklet_struct
{
struct tasklet_struct *next; //链接下一个tasklet_struct对象,以构成一个tasklet队列
unsigned long state; //该tasklet的运行状态标志位
atomic_t count; //该tasklet被引用的次数标志位,当count为0时,表示已激活可用
void (*func)(unsigned long); //该tasklet的处理函数指针,也是tasklet的核心所在
unsigned long data; //给上面的处理函数传的参数。
};
下面就给出一个tasklet的队列示意图
理解这个tasklet队列非常有用,这样我们就可以充分理解tasklet的工作机制了。从上面这个队列,我们可以看到这个队列的头是一个名叫tasklet_vec的tasklet_head结构体,我们来看看tasklet_head结构体体
struct tasklet_head
{
struct tasklet_struct *head;
struct tasklet_struct **tail;
};
可以看到,里面有两个元素,第一个是一个tasklet_struct结构体指针,在tasklet_vec中,这个用来指向与其最近的一个tasklet_struct结构体。另外一个是一个tasklet_struct类型的指针的指针,干嘛用的呢,从上图我们可以看到,它指向的是tasklet队列最后一个tasklet_struct的next指针的地址。这样一个tasklet_head就可以维护一个tasklet队列了。这里需要注意的是,当这个tasklet列表没有一个tasklet_struct元素时,它的指向是这样的:
tasklet_struct * t;
* _get_cpu_var(tasklet_vec).tail = t;
_get_cpu_var(tasklet_vec).tail = &(t->next);
初学者在第一次看到段代码时,一般都看不太懂,即便是已经看懂了我上面话的tasklet队列示意图。其实这段代码的目的非常简单,就是将新的tasklet_struct指针t加入tasklet队列,需要注意的是_get_cpu_var(变量名)的作用是获取独属于该CPU的tasklet_vec变量。要看懂这两行代码需要有较强的C语言功底,尤其是要对指针的指针理解深刻。
static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
local_irq_disable(); //禁止本地中断
list = __this_cpu_read(tasklet_vec.head); //获取本地中断的tasklet_vec.head指针的指向
__this_cpu_write(tasklet_vec.head, NULL); //将tasklet_vec.head赋值为null
__this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head); //将tasklet_vec.tail赋值为head的地址
local_irq_enable();
while (list) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) { //主要是判断该tasklet是否处于run状态,如果处于run状态的话,就从新将其放入tasklet_vec队列中
if (!atomic_read(&t->count)) {
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
BUG();
t->func(t->data); //执行tasklet的处理函数
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t; //如果tasklet正在被其他CPU运行,那么就将该tasklet重新装入队列现在再来看这两行代码就应该熟悉了吧
__this_cpu_write(tasklet_vec.tail, &(t->next));
__raise_softirq_irqoff(TASKLET_SOFTIRQ); //将tasklet挂起,等待下一次调用do_softirq函数的时候,这些加入tasklet队列的tasklet_struct对象就会被执行。
local_irq_enable();
}
}
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t); //调用_tasklet_schedule函数
}
这里要说明一下test_and_set_bit(TASKLET_STATE_SCHED, &t->state)函数,这个函数的目的是首先判断t->state是否是TASKLET_STATE_SCHED,如果是就返回TASKLET_STATE_SCHED,如果不是则将t->state设置为TASKLET_STATE_SCHED,同时返回t->state原来的值。所以这里要执行if后面的代码,就必须要t->state原来的值为0,即该tasklet是一个全新的tasklet,没有被用过。我们再来看看_tasklet_schedule函数:
void __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
local_irq_save(flags); //禁止本地中断,因为tasklet_vec是本地CPU的公共资源,在一个程序正在使用时,肯定不能被其他程序同时使用,这样被导致安全问题。
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next)); //这两行代码很熟悉吧
raise_softirq_irqoff(TASKLET_SOFTIRQ); //后面当然也很熟悉
local_irq_restore(flags); //恢复本地中断
}
DECLARE_TASKLET(name, func, data) //count = 0;处于激活状态
DECLARE_TASKLET_DISABLED(name, func, data) //count = 1;处于未激活状态
第二种是动态创建方式:
static struct tasklet_struct my_tasklet;
tasklet_init(&my_tasklet, tasklet_handler, 0); //count = 0,处于激活状态。
其中tasklet_init函数也是系统函数,可以直接使用的
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->next = NULL;
t->state = 0;
atomic_set(&t->count, 0);
t->func = func;
t->data = data;
}