1、什么是下半部
中断是一个很霸道的东西,处理器一旦接收到中断,就会打断正在执行的代码,调用中断处理函数。如果在中断处理函数中没有禁止中断,该中断处理函数执行过程中仍有可能被其他中断打断。出于这样的原因,大家都希望中断处理函数执行得越快越好。
另外,中断上下文中不能阻塞,这也限制了中断上下文中能干的事。
基于上面的原因,内核将整个的中断处理流程分为了上半部和下半部。上半部就是之前所说的中断处理函数,它能最快的响应中断,并且做一些必须在中断响应之后马上要做的事情。而一些需要在中断处理函数后继续执行的操作,内核建议把它放在下半部执行。
拿网卡来举例,在linux内核中,当网卡一旦接受到数据,网卡会通过中断告诉内核处理数据,内核会在网卡中断处理函数(上半部)执行一些网卡硬件的必要设置,因为这是在中断响应后急切要干的事情。接着,内核调用对应的下半部函数来处理网卡接收到的数据,因为数据处理没必要在中断处理函数里面马上执行,可以将中断让出来做更紧迫的事情。
可以有三种方法来实现下半部:软中断、tasklet和等待队列。
2、软中断
软中断一般很少用于实现下半部,但tasklet是通过软中断实现的,所以先介绍软中断。字面理解,软中断就是软件实现的异步中断,它的优先级比硬中断低,但比普通进程优先级高,同时,它和硬中断一样不能休眠。软中断是在编译时候静态分配的,要用软中断必须修改内核代码。
在kernel/softirq.c中有这样的一个数组:
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
内核通过一个softirq_action数组来维护的软中断,NR_SOFTIRQS是当前软中断的个数,待会再看他在哪里定义。
先看一下softirq_action结构体:
/*include/linux/interrupt.h*/
struct softirq_action
{
void (*action)(struct softirq_action *); //软中断处理函数
};
一看发现,结构体里面就一个软中断函数,他的参数就是本身结构体的指针。之所以这样设计,是为了以后的拓展,如果在结构体中添加了新成员,也不需要修改函数接口。在以前的内核,该结构体里面还有一个data的成员,用于传参,不过现在没有了。
接下来看一下如何使用软中断实现下半部
- 一、要使用软中断,首先就要静态声明软中断:
/*include/linux/interrupt.h*/
enum
{
HI_SOFTIRQ=0, //用于tasklet的软中断,优先级最高,为0
TIMER_SOFTIRQ, //定时器的下半部
NET_TX_SOFTIRQ, //发送网络数据的软中断
NET_RX_SOFTIRQ, //接受网络数据的软中断
BLOCK_SOFTIRQ,
TASKLET_SOFTIRQ, //也是用于实现tasklet
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
//add by xiaobai 2011.1.18
XIAOBAI_SOFTIRQ, //这是我添加的,优先级最低
NR_SOFTIRQS, //这个就是上面所说的软中断结构体数组成员个数
};
上面通过枚举定义了NR_SOFTIRQS(10)个软中断的索引号,优先级最高是0(HI_SOFTIRQ),最低是我刚添加上去的XIAOBAI_SOFTIRQ,优先级为9。
- 二、定义了索引号后,还要注册处理程序。
通过函数open_softirq来注册软中断处理函数,使软中断索引号与中断处理函数对应。该函数在kernel/softirq.c中定义:
/*kernel/softirq.c */
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
其实该函数就是把软中断处理函数的函数指针存放到对应的结构体中,一般的,我们自己写的模块是不能调用这个函数的,为了使用这个函数,我修改了内核:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
EXPORT_SYMBOL(open_softirq); //这是我添加的,导出符号,这样我编写的程序就能调用
在我的程序中如下调用:
/*6th_irq_3/1st/test.c*/
void xiaobai_action(struct softirq_action *t) //软中断处理函数
{
printk("hello xiaobai!\n");
}
open_softirq(XIAOBAI_SOFTIRQ, xiaobai_action);
- 三、在中断处理函数返回前,触发对应的软中断。
在中断处理函数完成了必要的操作后,就应该调用函数raise_sotfirq触发软中断,让软中断执行中断下半部的操作。
/*kernel/softirq.c*/
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
所谓的触发软中断,并不是指马上执行该软中断,不然和在中断上执行没什么区别。它的作用只是告诉内核:下次执行软中断的时候,记得执行我这个软中断处理函数。
当然,这个函数也得导出符号后才能调用:
/*kernel/softirq.c*/
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
EXPORT_SYMBOL(raise_softirq);
在我的程序中如下调用:
/*6th_irq_3/1st/test.c*/
irqreturn_t irq_handler(int irqno, void *dev_id) //中断处理函数
{
printk("key down\n");
raise_softirq(XIAOBAI_SOFTIRQ);
return IRQ_HANDLED;
}
经过三步,使用软中断实现下半部就成功了,看一下完整的函数:
/*6th_irq_3/1st/test.c*/
#include
#include
#include
#define DEBUG_SWITCH 1
#if DEBUG_SWITCH
#define P_DEBUG(fmt, args...) printk("<1>" "[%s]"fmt, __FUNCTI ON__, ##args)
#else
#define P_DEBUG(fmt, args...) printk("<7>" "[%s]"fmt, __FUNCTI ON__, ##args)
#endif
void xiaobai_action(struct softirq_action *t) //软中断处理函数
{
printk("hello xiaobai!\n");
}
irqreturn_t irq_handler(int irqno, void *dev_id) //中断处理函数
{
printk("key down\n");
raise_softirq(XIAOBAI_SOFTIRQ); //触发软中断
return IRQ_HANDLED;
}
static int __init test_init(void) //模块初始化函数
{
int ret;
/*注册中断处理函数:
* IRQ_EINT1:中断号,定义在"include/mach/irqs.h"中
* irq_handler:中断处理函数
* IRQ_TIRGGER_FALLING:中断类型标记,下降沿触发中断
* ker_INT_EINT1:中断的名字,显示在/proc/interrupts等文件中
* NULL;现在我不使用dev_id,所以这里不传参数
*/
ret = request_irq(IRQ_EINT1, irq_handler, IRQF_TRIGGER_FALLING, "key INT_EINT1", NULL);
if(ret){
P_DEBUG("request irq failed!\n");
return ret;
}
/*fostirq*/
open_softirq(XIAOBAI_SOFTIRQ, xiaobai_action); //注册软中断处理程序
printk("hello irq\n");
return 0;
}
static void __exit test_exit(void) //模块卸载函数
{
free_irq(IRQ_EINT1, NULL);
printk("good bye irq\n");
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xoao bai");
MODULE_VERSION("v0.1");
注意。在上面的程序,只是为了说明如何实现上下半步,而我的中断上下半步里面的操作是毫无意义的(只是打印)。上下半步的作用我在一开始就有介绍。
接下来验证一下:
[root: 1st]# insmod test.ko
hello irq
[root: 1st]# key down //上半部操作
hello xiaobai! //下半部操作
key down
hello xiaobai!
key down
hello xiaobai!
[root: 1st]# rmmod test
good bye irq
上面介绍,触发软中断函数raise_softirq并不会让软中断处理函数马上执行,它只是打了个标记,等到适合的时候再被实行。如在中断处理函数返回后,内核就会检查软中断是否被触发并执行触发的软中断。
软中断会在do_softirq中被执行,其中核心部分在do_softirq中调用的__do_softirq中:
/*kernel/softirq.c*/
asmlinkage void __do_softirq(void)
{
。。。。。。
do {
if (pending & 1) { //如果被触发,调用软中断处理函数
int prev_count = preempt_count();
h->action(h); //调用软中断处理函数
if (unlikely(prev_count != preempt_count())) {
printk(KERN_ERR "huh, entered softirq %td %p"
"with preempt_count %08x,"
" exited with %08x?\n", h - softirq_vec,
h->action, prev_count, preempt_count());
preempt_count() = prev_count;
}
rcu_bh_qsctr_inc(cpu);
}
h++; //下移,获取另一个软中断
pending >>= 1;
} while (pending); //大循环内执行,知道所有被触发的软中断都执行完
。。。。。。
3、tasklet
上面的介绍看到,软中断实现下半部的方法很麻烦,一般是不会使用的。一般,我们使用tasklet——利用软中断实现的下半部机制。
在介绍软中断索引号的时候,有两个用于实现tasklet的软中断索引号:HI_SOFTIRQ和TASKLET_SOFTIRQ。两个tasklet唯一的区别就是优先级的大小,一般使用TAKSLET_SOFTIRQ。
先看一下如何使用tasklet,用完之后再看内核中是如何实现的:
-
步骤一、编写tasklet处理函数,定义并初始化结构体tasklet_struct:
内核中是通过tasklet_struct来维护一个tasklet,介绍一下tasklet_struct里面的两个成员:
/*linux/interrupt.h*/
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long); //tasklet处理函数
unsigned long data; //给处理函数的传参
};
所以,在初始化tasklet_struct之前,需要先写好tasklet处理函数,如果需要传参,也需要指定传参,你可以直接传数据,也可以传地址。我定义的处理函数如下:
/*6th_irq_3/2nd/test.c*/
void xiaobai_func(unsigned long data)
{
printk("hello xiaobai!, data[%d]\n", (int)data); //也没干什么事情,仅仅打印。
}
同样,可以通过两种办法定义和初始化tasklet_struct。
1、静态定义并初始化
/*linux/interrupt.h*/
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name ={ NULL, 0, ATOMIC_INIT(0), func, data }
上面两个函数都是定义一个叫name的tasklet_struct,并指定他的处理函数和传参分别是func和data。唯一的区别是,DCLARE_TASKLET_DISABLED初始化后的处于禁止状态,暂时不能被使用。
2、动态定义并初始化
跟以往的一样,需要先定义结构体,然后把结构体指针传给tasklet_init来动态初始化:
/*kernel/softirq.c*/
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data)
在我的程序中,使用动态定义并初始化:
/*6th_irq_3/2nd/test.c*/
struct tasklet_struct xiaobai_tasklet; //定义tasklet结构体
tasklet_init(&xiaobai_tasklet, xiaobai_func, (unsigned long)123);
我这里的传参直接传一个数值123。这操作也相当于:
DECLEAR_TASKLET(xiaobai_tasklet, xiaobai_func, (unsigned long)123);
-
步骤二、在中断返回前调度tasklet:
跟软中断一样(其实tasklet就是基于软中断实现),这里说的调度并不是马上执行,只是打个标记,至于什么时候执行就要看内核的调度。
调度使用函数tasklet_schedule或者tasklet_hi_schedule,两个的区别是一个使用TASKLET_SOFTIRQ,另一个使用HI_SOFTIRQ。这两个函数都是一tasklet_struct指针为参数:
/*linux/interrupt.h*/
static inline void tasklet_schedule(struct tasklet_struct *t)
static inline void tasklet_hi_schedule(struct tasklet_struct *t)
在我的函数中,使用tasklet_schedule:
/*6th_irq_3/2nd/test.c*/
tasklet_schedule(&xiaobai_tasklet);
-
步骤三、当模块卸载时,将tasklet_struct结构体移除:
/*kernel/softirq.c*/
void tasklet_kill(struct tasklet_struct *t)
确保了 tasklet 不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。如果 tasklet 正在运行, 程序会休眠,等待直到它执行完毕
另外,还有禁止与激活tasklet的函数。被禁止的tasklet不能被调用,直到被激活:
/*linux/interrupt.h*/
static inline void tasklet_disable(struct tasklet_struct *t) //禁止
static inline void tasklet_enable(struct tasklet_struct *t) //激活
最后附上程序:
/*6th_irq_3/2nd/test.c*/
#include
#include
#include
。。。。省略。。。。
struct tasklet_struct xiaobai_tasklet; //定义tasklet结构体
void xiaobai_func(unsigned long data)
{
printk("hello xiaobai!, data[%d]\n", (int)data);
}
irqreturn_t irq_handler(int irqno, void *dev_id) //中断处理函数
{
printk("key down\n");
tasklet_schedule(&xiaobai_tasklet);
return IRQ_HANDLED;
}
static int __init test_init(void) //模块初始化函数
{
int ret;
/*tasklet*/
tasklet_init(&xiaobai_tasklet, xiaobai_func, (unsigned long)123);
ret = request_irq(IRQ_EINT1, irq_handler, IRQF_TRIGGER_FALLING, "key INT_EINT1", NULL);
if(ret){
P_DEBUG("request irq failed!\n");
return ret;
}
printk("hello irq\n");
return 0;
}
static void __exit test_exit(void) //模块卸载函数
{
tasklet_kill(&xiaobai_tasklet);
free_irq(IRQ_EINT1, NULL);
printk("good bye irq\n");
}
module_init(test_init);
module_exit(test_exit);
最后验证一下,还是老样子,上下半步只是打印一句话,没有实质操作:
[root: 2nd]# insmod test.ko
hello irq
[root: 2nd]# key down //上半部操作
hello xiaobai!, data[123] //下半部操作
key down
hello xiaobai!, data[123]
[root: 2nd]# rmmod test
good bye irq
既然知道怎么使用tasklet,接下来就要看看它是怎么基于软中断实现的
上面说明的是单处理器的情况下,如果是多处理器,每个处理器都会有一个tasklet_vec和tasklet_hi_vec链表,这个情况我就不介绍了。
-
四、总结
这节介绍了如何通过软中断(tasklet也是软中断的一种实现形式)机制来实现中断下半部。使用软中断实现的优缺点很明显:
优点:运行在软中断上下文,优先级比普通进程高,调度速度快。
缺点:由于处于中断上下文,所以不能睡眠。
也许有人会问,那软中断和tasklet有什么区别?
个人理解,tasklet是基于软中断实现的,基本上和软中断相同。但有一点不一样,如果在多处理器的情况下,内核不能保证软中断在哪个处理器上运行(听起来像废话),所以,软中断之间需要考虑共享资源的保护。而在tasklet,内核可以保证,两个同类型(TASKLET_SOFTIRQ和HI_SOFTIRQ)的tasklet不能同时执行,那就说明,同类型tasklet之间,可以不考虑同类型tasklet之间的并发情况。
一般的,优先考虑使用tasklet。
本文非原创:Linux中断的上半部与下半部