By:Ailson Jack
Date:2016.04.10
个人博客:www.only2fire.com
本文在我博客的地址是:http://www.only2fire.com/archives/871.html,排版更好,便于学习。
上一章简单的讲了一下中断的上半部(中断处理程序),这一章就讲讲中断的下半部以及下半部的几种实现机制,最后简单的写了几个测试的例子来测试软中断、tasklet和工作队列。
测试程序下载地址:。
1、下半部简述
中断下半部的任务是执行与中断处理密切相关但中断处理程序本身并不执行的工作。
对于中断中的任务应该在上半部还是下半部执行,并没有严格的规则来规定,但是还是有一些提示可供借鉴:
1)、如果一个任务对时间非常敏感,将其放在中断处理程序中执行;
2)、如果一个任务和硬件相关,将其放在中断处理程序中执行;
3)、如果一个任务要保证不被其他中断(特别是共享同一中断线的中断)打断,将其放在中断处理程序中执行;
4)、其他所有任务可以考虑放置在下半部执行。
和上半部只能通过中断处理程序实现不同,下半部可以通过多种机制实现。随着Linux内核的发展,产生了一些新的机制,也淘汰了一些旧的机制。目前,下半部主要有三种机制:软中断,tasklet,工作队列。
2、中断下半部机制 — 软中断
软中断是一组静态定义的下半部接口,在Linux-2.6.34内核中有10个,可以在所有处理器上同时执行(即使两个类型相同也可以)。软中断必须在编译期间就进行静态注册。
软中断保留给系统中对时间要求最严格以及最重要的下半部使用。目前,只有两个子系统(网络和SCSI)直接使用软中断。此外,内核定时器和tasklet都是建立在软中断上的。
软中断的代码在:kernel/softirq.c
使用软中断的流程图如下:
1)、分配索引
软中断目前有10种类型,软中断类型定义在include/linux/interrupts.h中,可以通过修改该文件中的枚举,来实现增加或者减少软中断,下面我增加一个自己定义的软中断AilsonJack_SOFTIRQ:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
AilsonJack_SOFTIRQ,/*自己定义的一个新的软中断类型*/
NR_SOFTIRQS
};
接着在kernel/softirq.c文件中的softirq_to_name数组中给新增的软中断取个名字:
char *softirq_to_name[NR_SOFTIRQS] = {
“HI”, “TIMER”, “NET_TX”, “NET_RX”, “BLOCK”, “BLOCK_IOPOLL”,
“TASKLET”, “SCHED”, “HRTIMER”, “RCU”, “AilsonJack”
};/*增加一种新的softirq —> AilsonJack*/
索引号小的软中断在索引号大的软中断之前执行。
2)、注册处理程序
注册处理程序使用的是open_softirq()函数,它的定义在kernel/softirq.c文件中:
/*
* 将软中断类型和软中断处理函数加入到软中断序列中
* @nr – 软中断类型
* @ void (*action)(struct softirq_action *) – 软中断处理的函数指针
*/
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
/* softirq_vec是个struct softirq_action类型的数组 */
softirq_vec[nr].action = action;
}
struct softirq_action 的定义在 include/linux/interrupt.h 文件中:
/*
* 这个结构体的成员是个函数指针,函数的名称是action
* 函数指针的返回值是void型
* 函数指针的参数是 struct softirq_action 的地址,其实就是指向 softirq_vec 中的某一项
* 如果 open_softirq 是这样调用的: open_softirq(NET_TX_SOFTIRQ, my_tx_action);
* 那么 my_tx_action 的参数就是 softirq_vec[NET_TX_SOFTIRQ]的地址
*/
struct softirq_action
{
void (*action)(struct softirq_action *);
};
一个软中断不会抢占另一个软中断。实际上,唯一可以抢占软中断的是中断处理程序。不过,其他的软中断(甚至是相同类型的软中断)可以在其他处理器上同时执行。
软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。这意味着你不能在软中断中使用信号量或者其他什么阻塞式函数。
3)、触发软中断
通过在枚举类型列表中添加新项以及调用open_sofirq()进行注册以后,新的软中断处理程序就能够运行。raise_softirq()函数可以将一个软中断设置为挂起状态,让它在下一次调用do_softirq()函数时投入运行。
触发软中断的函数raise_softirq()定义在kernel/softirq.c文件中:
/*
* 触发某个类型的软中断
* @nr – 被触发的软中断类型
* 从函数中可以看出,在处理软中断前后有保存和恢复本地中断的操作
*/
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
raise_softirq(NET_TX_SOFTIRQ),这样会触发NET_TX_SOFTIRQ软中断。
在中断处理程序中触发软中断是最常见的形式。
4)、执行软中断
在触发了软中断之后,系统会在合适的时刻让软中断运行,该函数不需要自己调用。
执行软中断do_softirq()定义在kernel/softirq.c文件中:
asmlinkage void do_softirq(void)
{
__u32 pending;
unsigned long flags;
/* 判断是否在中断处理中,如果正在中断处理,就直接返回 */
if (in_interrupt())
return;
/* 保存当前寄存器的值 */
local_irq_save(flags);
/* 取得当前已注册软中断的位图 */
pending = local_softirq_pending();
/* 循环处理所有已注册的软中断 */
if (pending)
__do_softirq();
/* 恢复寄存器的值到中断处理前 */
local_irq_restore(flags);
}
3、中断下半部机制 — tasklet
tasklet是用软中断实现的一种下半部机制。但是它的接口更简单,对锁保护的要求也较低。对于tasklet和软中断如何选择,通常应该选用tasklet,而对于那些执行频率很高和连续性要求很高的情况下才选用软中断。
tasklet有两类软中断类型:HI_SOFTIRQ,TASKLET_SOFTIRQ。这两者之间的唯一区别就是,HI_SOFTIRQ类型的软中断会先于TASKLET_SOFTIRQ类型的软中断执行。
tasklet由tasklet结构体表示。每个结构体单独代表一个tasklet,它定义在linux/interrupt.h文件中:
struct tasklet_struct
{
struct tasklet_struct *next;/*链表中的下一个tasklet*/
unsigned long state;/*tasklet的状态*/
atomic_t count;/*引用计数器*/
void (*func)(unsigned long);/*tasklet处理函数*/
unsigned long data;/*tasklet处理函数的参数*/
};
tasklet状态值:
0:表示tasklet没有被调度;
TASKLET_STATE_SCHED:表示tasklet已经被调度,正准备运行;
TASKLET_STATE_RUN:表示tasklet正在运行。
引用计数器count:
非0:tasklet被禁止,不允许执行;
0:tasklet被激活,可以执行。
使用tasklet的流程图如下:
1)、创建tasklet结构体
/* 静态声明一个tasklet */
#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(1), func, data }
/* 动态声明一个tasklet 传递一个tasklet_struct指针给初始化函数 */
extern void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
2)、编写处理程序
参照tasklet处理的原型,编写自己的处理函数:
void tasklet_handler(unsigned long date)
因为tasklet是靠软中断实现的,所以taskelt也不能睡眠,即不能在tasklet中使用信号量或者其他什么阻塞式的函数。两个相同的tasklet决不会同时执行。
3)、调度tasklet
通过调用tasklet_sched()函数并传递给它相应的task_struct的指针,该tasklet就会被调度以便执行:
tasklet_sched(&my_tasklet);
和软中断一样,通常也是在中断处理程序中来调度tasklet,之后,系统会在适合的时候调度你的tasklet处理程序来对中断的下半部进行处理。
4、中断下半部机制 — 工作队列
工作队列(work queue)是另一种将工作推后执行的形式,它和我们前面讨论的所有其它形式都不同。工作队列的执行会在进程上下文中进行,这样工作队列就允许重新调度甚至是睡眠。
通常在工作队列和软中断/tasklet中做出选择非常容易。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择软中断或者tasklet。
缺省的工作者线程名称是 events/n (n对应处理器号)。每个处理器对应一个线程。可以使用top | grep events来查看机器上的events线程。
工作队列主要用到下面3个结构体,弄懂了这3个结构体的关系,也就知道工作队列的处理流程了。
/* 在 include/linux/workqueue.h 文件中定义 */
struct work_struct {
atomic_long_t data; /* 这个并不是处理函数的参数,而是表示此work是否pending等状态的flag */
#define WORK_STRUCT_PENDING 0 /* T if work item pending execution */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
struct list_head entry; /* 中断下半部处理函数的链表 */
work_func_t func; /* 处理中断下半部工作的函数 */
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
/* 在 kernel/workqueue.c文件中定义
* 每个工作者线程对应一个 cpu_workqueue_struct ,其中包含要处理的工作的链表
* (即 work_struct 的链表,当此链表不空时,唤醒工作者线程来进行处理)
*/
/*
* The per-CPU workqueue (if single thread, we always use the first
* possible cpu).
*/
struct cpu_workqueue_struct {
spinlock_t lock; /* 锁保护这种结构 */
struct list_head worklist; /* 工作队列头节点 */
wait_queue_head_t more_work;
struct work_struct *current_work;
struct workqueue_struct *wq; /* 关联工作队列结构 */
struct task_struct *thread; /* 关联线程 */
} ____cacheline_aligned;
/* 也是在 kernel/workqueue.c 文件中定义的
* 每个 workqueue_struct 表示一种工作者类型,系统默认的就是 events 工作者类型
* 每个工作者类型一般对应n个工作者线程,n就是处理器的个数
*/
/*
* The externally visible workqueue abstraction is an array of
* per-CPU workqueues:
*/
struct workqueue_struct {
struct cpu_workqueue_struct *cpu_wq; /* 工作者线程 */
struct list_head list;
const char *name;
int singlethread;
int freezeable; /* Freeze threads during suspend */
int rt;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
注意,这里有三个概念,工作(work_struct),工作者线程(cpu_workqueue_struct),工作者类型(workqueue_struct)。
这里举个例子,假设计算机有4个处理器,除了默认的events工作者类型外,我自己还新加入falcon工作者类型,对于这种情况,来说说结构体的数目。因为此时计算机上有两种工作者类型:events和falcon,那么相应的也就有2个工作者类型(workqueue_struct)结构体分别与之对应。因为计算机有4个处理器,每个处理器对应一个工作者线程,那么events工作者类型有4个工作者线程(cpu_workqueue_struct)结构体,同样的falcon工作者类型也有4个工作者线程(cpu_workqueue_struct)结构体,计算机总共有8个工作者线程(cpu_workqueue_struct)结构体。对于工作(work_struct)结构体,它就是工作者线程需要处理的任务。
使用工作队列的流程图如下:
1)、创建推后工作
创建推后执行的工作,这里有静态和动态两种方法:
/* 静态创建一个work_struct
* @n – work_struct结构体,不用事先定义
* @f – 下半部处理函数
*/
#define DECLARE_WORK(n, f) \
struct work_struct n = __WORK_INITIALIZER(n, f)
/* 动态创建一个 work_struct
* @_work – 已经定义好的一个 work_struct
* @_func – 下半部处理函数
*/
#ifdef CONFIG_LOCKDEP
#define INIT_WORK(_work, _func) \
do { \
static struct lock_class_key __key; \
\
(_work)->data = (atomic_long_t) WORK_DATA_INIT(); \
lockdep_init_map(&(_work)->lockdep_map, #_work, &__key, 0);\
INIT_LIST_HEAD(&(_work)->entry); \
PREPARE_WORK((_work), (_func)); \
} while (0)
#else
#define INIT_WORK(_work, _func) \
do { \
(_work)->data = (atomic_long_t) WORK_DATA_INIT(); \
INIT_LIST_HEAD(&(_work)->entry); \
PREPARE_WORK((_work), (_func)); \
} while (0)
#endif
工作队列处理函数的原型:
typedef void (*work_func_t)(struct work_struct *work);
2)、刷新现有的工作
这个步骤不是必须的,可以直接进行创建推后工作和对工作进行调度这两步。
刷新现有工作的意思就是在追加新的工作之前,保证队列中的已有工作已经执行完了。
/* 刷新系统默认的队列,即 events 队列 */
void flush_scheduled_work(void);
/* 刷新用户自定义的队列
* @wq – 用户自定义的队列
*/
void flush_workqueue(struct workqueue_struct *wq);
3)、对工作进行调度
/* 调度第一步中新定义的工作,在系统默认的工作者线程中执行此工作
* @work – 第一步中定义的工作
*/
schedule_work(struct work_struct *work);
/* 调度第一步中新定义的工作,在系统默认的工作者线程中执行此工作
* @work – 第一步中定义的工作
* @delay – 延迟的时钟节拍
*/
int schedule_delayed_work(struct delayed_work *work, unsigned long delay);
/* 调度第一步中新定义的工作,在用户自定义的工作者线程中执行此工作
* @wq – 用户自定义的工作队列类型
* @work – 第一步中定义的工作
*/
int queue_work(struct workqueue_struct *wq, struct work_struct *work);
/* 调度第一步中新定义的工作,在用户自定义的工作者线程中执行此工作
* @wq – 用户自定义的工作队列类型
* @work – 第一步中定义的工作
* @delay – 延迟的时钟节拍
*/
int queue_delayed_work(struct workqueue_struct *wq,
struct delayed_work *work, unsigned long delay);
5、中断下半部小结
下面对实现中断下半部工作的3种机制进行总结,便于在实际使用中决定使用哪种机制:
下半部机制 | 上下文 | 复杂度 | 执行性能 | 顺序执行保障 |
软中断 | 中断 | 高
(需要自己确保软中断的执行顺序及锁机制) |
好
(全部自己实现,便于调优) |
没有 |
tasklet | 中断 | 中
(提供了简单的接口来使用软中断) |
中 | 同类型不能同时执行 |
工作队列 | 进程 | 低
(在进程上下文中运行,与写用户程序差不多) |
差 | 没有
(和进程上下文一样被调度) |
6、软中断、tasklet、工作队列的测试
这些测试程序在文章开始的地方,提供了一个下载链接,大家下载即可(重要的是那个Makefile文件,下面不会列出Makefile里面的内容)。
1)、软中断
在上面介绍软中断的类型时,自己添加了一个新的软中断类型,下面说说具体修改的内容吧:
在include/interrupt.h文件中的枚举列表中添加新的软中断类型:
在kernel/softirq.c文件中,给新添加的软中断类型命名:
导出raise_softirq()和open_softirq()函数,不然在编译模块时,会发出类似下面的警告:
WARNING: “open_softirq” undefined
WARNING: “raise_softirq” undefined
修改好之后,就可以编译内核(make,make modules_install,make install),然后从编译好的内核启动(具体详细的编译内核的方法,可以在我的博客中搜索)。
内核之所以没有导出open_softirq和raise_softirq函数,可能还是因为提倡我们尽量用tasklet来实现中断的下半部工作
由于测试程序比较长,这里就不贴出来了,大家可以去下载的文件中看。
执行下面的步骤,对软中断进行测试:
编译模块:make
加载模块:insmod softirq.ko
卸载模块:rmmod softirq
查看信息:dmesg | tail
可以看见打印信息。
2)、tasklet
由于测试程序比较长,这里就不贴出来了,大家可以去下载的文件中看。
执行下面的步骤,对tasklet进行测试:
编译模块:make
加载模块:insmod tasklet.ko
卸载模块:rmmod tasklet
查看信息:dmesg | tail
可以看见打印信息。
3)、工作队列
由于测试程序比较长,这里就不贴出来了,大家可以去下载的文件中看。
执行下面的步骤,对工作队列进行测试:
编译模块:make
加载模块:insmod workqueue.ko
卸载模块:rmmod workqueue
查看信息:dmesg | tail
可以看见打印信息。
注:转载请注明出处,谢谢!^_^