第8章 下半部和推后执行的工作
下半部
一般来说:
任务对时间非常敏感,放在中断处理程序中执行。
任务和硬件相关,放在中断处理程序中执行。
任务要保证不被其它中断打断,放在中断处理程序中执行。
其它所有任务,考虑放置在下半部执行。
下半部的环境
下半部可以通过多种机制实现。
- “下半部”的起源
最早linux提供“bottom half”用于实现下半部,简称“BH”。接口简单,提供了一个静态创建、由32个bottom havles 组成的链表。上半部通过一个32位整数中的一位来表示哪个bottom half可以执行。每个BH在全局范围内(所有的处理器)进行同步,不允许两个下半部同时执行。使用方便不够灵活,简单但有性能瓶颈。 - 任务队列
任务队列,task queue。 - 软中断和tasklet
软中断是由一组静态定义的下半部接口,有32个,可以在所有的处理器上同时执行(相同类型也可以)。
tasklet是一种基于软中断实现的灵活性强、动态创建的下半部实现机制(相同类型不可同时执行)。
软中断使用与对性能要非常高的情况,如网络。其它情况使用tasklet已经足够。
软中断是要考虑相同的软中断可能同时被执行,在编译期间就进行静态注册。
tasklet可以通过代码动态注册。 - 发展
BH和任务队列在2.5版本中已被移除,下半部的实现有三种机制,软中断、tasklet、工作队列。
软中断
实际使用比较少,tasklet更常用,不过后者基于前者。
代码位于kernel/softirq.c文件中。
实现
在编译期间静态分配的。
由结构softirq_action结构表示,定义在<linux/interrupt.h>中。
struct softirq_action{
void (*action)(struct softirq_action *);
};
kernel/softirq.c中定义了一个包含有32个该结构体的数组。
static struct softirq_action softirq_vec[NR_SOFTIRQS];
- 软中断处理程序
原型如下:
void softirq_handler(struct softirq_action *);
一个软中断不会抢占另一个软中断,但是可以被中断处理程序抢占。
2. 执行软中断
注册的软中断在标记后才会执行,触发软中断(raising the softirq)。一般由中断处理程序在返回前标记它的软中断,使其在稍后执行,但执行时间不确定。
在以下情况中可以被唤起:
- 从一个硬件中断代码返回时
- 在ksoftirq内核线程中
- 在那些显式检查和执行待处理的软中断代码中,如网路子系统
中断的执行在do_softirq()中执行,该函数会尝试循环处理所有待处理的软中断。
/* 核心代码,检查并处理所有待处理的软中断 */
u32 pending;
pending=local_softirq_pending();
if(pending){
struct softirq_action *h;
/* 重设待处理的位图 */
set_softirq_pending(0);
h=softirq_vec;
do{
if(pending&1)
h->action(h);
h++;
pending>>=1;
}while(pending);
}
使用软中断
软中断保留给系统中对时间要求最严格以及最重要的下半部使用。
目前,只有两个子系统直接使用软中断(网络和SCSI)。内核定时器和tasklet都是建立在软中断上的。
tasklet可以动态生成,对加锁的要求不高,使用方便,性能也不错,是首选项。
对时间要求严格的话,才使用软中断。
- 分配索引
在编译期间,通过<linux/interrupt.h>中定义的一个枚举类型来静态地声明软中断。索引号代表着优先级,小的优先执行。
习惯上HI_SOFTIRQ通常作为第一项,RCU_SOFTIRQ作为最后一项。
已有的tasklet类型:
tasklet | 优先级 | 软中断描述 |
---|---|---|
HI_SOFTIRQ | 0 | 优先级高的tasklets |
TIMER_SOFTIRQ | 1 | 定时器的下半部 |
NET_TX_SOFTIRQ | 2 | 发送网络数据包 |
NET_RX_SOFTIRQ | 3 | 接收网络数据包 |
BLOCK_SOFTIRQ | 4 | BLOCK装置 |
TASKLET_SOFTIRQ | 5 | 正常优先权的tasklets |
SCHED_SOFTIRQ | 6 | 调度程序 |
HRTIMER_SOFTIRQ | 7 | 高分辨率定时器 |
RCU_SOFTIRQ | 8 | RCU锁定 |
- 注册处理程序
在运行时通过调用open_softirq()注册软中断处理程序,使用两个参数:软中断的索引号和处理函数。
如网络子系统
open_softirq(NET_TX_SOFTIRQ,net_tx_action);
open_softirq(NET_RX_SOFTIRQ,net_rx_action);
同一程序可以被多个处理器同时执行,需要严格的所保护。
3. 触发软中断
raise_softirq()将一个软中断设置为挂起状态,在下次调用do_softirq()函数时投入运行。网络子系统可能会调用:
raise_softirq(NET_TX_SOFTIRQ);
tasklet
基于软中断实现,简单,大多数情况使用
实现
tasklet结构体
struct tasklet_struct{
struct tasklet_struct *next; /* 链表中的下一个tasklet */
unsigned long state; /* tasklet的状态 */
atomic_t count; /* 引用计数器 */
void (*func)(unsigned long); /* tasklet处理函数 */
unsigned long data; /* 给tasklet处理函数的参数 */
成员 | 描述 |
---|---|
func | tasklet的处理函数,data是它唯一的参数 |
state | TASKLET_STATE_SECHED表明tasklet已被调度,正准备投入运行;TASKLET_STATE_RUN表明该tasklet正在运行,只有在多处理器上作为优化来使用。还可以取0 |
count | 引用计数器 |
- 调度tasklet
已调度的tasklet(等同于被触发的软中断)存放在两个数据结构:tasklet_vec(普通tasklet),tasklet_hi_vec(高优先级的tasklet)。
tasklet由tasklet_schedule()和task_hi_schedule()函数进行调度(区别在于一个使用TASKLET_SOFTIRQ,而另一个使用HI_SOFTIRQ),他们接受一个指向tasklet_struct结构的指针作为参数。
tasklet_schedule()的执行步骤:
检查tasklet的状态,是否为TASKLET_STATE_SCHED,如果是,说明tasklet已被调度,函数立即返回。
调用_tasklet_schedule()。
保存中断状态,然后禁止本地中断。确保处理器上面的数据不会被弄乱。
把tasklet加到每个处理器的一个tasklet_vec或者tasklet_hi_vec链表的表头上去。
唤起TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,这样在下一次调用do_softirq()时就会执行该tasklet。
恢复中断的状态并返回。
tasklet_action和tasklet_hi_action是tasklet处理的核心。
禁止中断,为当前处理器检索tasklet链表。
当前处理器上该链表置为NULL,达到清空的效果。
允许相应中断。
循环处理每一个链表上每一个待处理的tasklet。
如果是多处理器,需要检查TASKLET_STATE_RUN来判断这个tasklet是否正在其它处理器上运行。如果正在执行,跳过即可。
如果没有执行,设置为TASKLET_STATE_RUN。
检查count值是否为0,确保tasklet没有被禁止。否则执行下一个。
已经确保tasklet没有在其它处理器上执行,而且引用计数为0,执行tasklet的处理程序。
运行完毕,清楚TASKLET_STATE_RUN。
重复执行下一个tasklet,处理完所有待处理的tasklet。
使用TASKLET
- 声明
静态:
#include <linux/interrupt.h>
/* 都可以静态创建task_struct结构 */
/* 该宏引用计数为0,处于激活状态 */
DECLARE_TASKLET(name,func,data)
/* 该宏引用计数为1,处于禁止状态 */
DECLARE_TASKLET_DISABLED(name,func,data)
DECLARE_TASKLET(my_struct,my_tasklet_handler,dev);
// 等价与
struct tasklet_struct my_struct={NULL,0,ATOMIC_INIT(0),my_tasklet_handler,dev);
动态:
tasklet_init(t,tasklet_handler,dev);
- 编写tasklet的处理程序
函数类型:
void tasklet_handler(unsigned long data)
软中断实现,不能睡眠。不能使用信号量或者阻塞函数。而且tasklet允许相应中断,还要做好处理工作。
3. 调度
将task_struct指针传递给task_schedule()函数即可,只要有机会,就会尽快的执行。
作为优化,一个tasklet总在调度它的处理器上执行。
tasklet_disable()用来禁止某个指定的tasklet。
tasklet_disable_nosync()禁止指定tasklet,无需等待返回,不安全。
tasklet_enable()激活tasklet。
tasklet_kill()从挂起的队列中去除一个tasklet。
4. ksoftirqd
每个处理器都有一组辅助处理软中断(和tasklet)的内核线程。
软中断的被触发的频率会很高,处理函数有时还会自行触发。
方案一:本次执行处理完所有触发的软中断,包括重新触发的软中断。在负载高的情况下,用户空间的任务只能等待。只能在负载低时有效。
方案二:不处理重新触发的软中断。时好时坏,软中断等待,没有充分利用闲置的资源。
折中。内核实现的方案是不会处理重新触发软中断。作为优化,在大量软中断出现的时候,内核会唤起一组内核线程来处理这些负载。这些线程在最低的优先级上运行(nice值是19)。
每个处理器都有这个线程,叫ksoftirq/n,n是处理器编号。初始后执行类似下面的死循环:
for(;;){
if(!softirq_pending(cpu))
schedule();
set_current_state(TASK_RUNING);
while(softirq_pending(cpu)){
do_softirq();
if(need_resched())
schedule();
}
set_current_state(TASK_INTERRUPTIBLE);
}
只要do_softirq()函数发现已经执行过的内核线程重新触发了自己,软中断内核线程就会被唤起。
老的BH机制
淘汰,2.5中去除。
工作队列
work queue把工作退后,交给另一个内核线程去执行,总会在内核上下文中,允许重新调度或者睡眠。
可以使用内核线程替换,但还是建议使用工作队列。
在需要获得大量的内存,在需要获取信号量时,在需要执行阻塞式的I/O操作时,使用它!
实现
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其它部分排到队列中的任务。创建的线程称为工作者线程(worker thread)。
缺省的工作者线程叫做events/n,n是处理器的编号。缺省的工作者线程会从多个地方得到被推后的工作。
在需要工作者线程执行大量的处理操作时,如处理器密集型和性能要求严格的任务,可以进行工作者线程的创建,减少缺省线程的压力。
- 表示线程的数据结构
工作者线程用workqueue_struct结构表示:
/*
* 外部可见的工作队列抽象是由每个
* CPU的工作队列组成的数组
*/
struct workqueue_struct{
struct cpu_workqueue_struct cpu_wq[NR_CPUS];
struct list_head list;
const char *name;
int singlethread;
int freezeable;
int rt;
}
是一个有cpu_workqueue_struct结构组成的数组,定义在kernel/workqueue.c结构体中。每个处理器,每个工作者线程都对应一个cpu_workqueue_struct结构体。
cpu_work_queue_struct是核心数据结构:
struct cpu_workqueue_struct{
spinlock_t lock; /* 锁 */
struct list_head worklist; /* 工作列表 */
wait_queue_head_t more_work;
struct work_struct *current_struct;
struct workqueue_struct *wq; /* 关联工作队列结构 */
task_t *thread; /* 关联线程 */
}
- 表示工作的数据结构
所有的工作者线程都是由普通的内核线程实现的,执行work_thread()函数。这个函数执行一个死循环并开始休眠,当有操作队列被插入到队列里的时候,线程就会被唤醒。当没有操作时,又会休眠。
<linux/workqueue.h>里的work_struct结构体表示工作。
struct work_struct{
atomic_long_t data;
struct list_head entry;
work_func_t func;
每一个工作队列都对应一个工作链表,当被唤醒时,会执行工作链表上的所有工作。没有工作时,会休眠。
work_thread()的简化核心流程如下:
/* cwq 是cpu_workqueue_struct */
for(;;){
prepare_to_wait(&cwq->more_work,&wait,TASK_INTERRUPTIBLE);
if(list_empty(&cwq->worklist))
schedule();
finish_wait(&cwq->more_work,&wait);
run_workqueue(&cwq);
}
①将自己设置为休眠状态(设置state),加入到等待队列中。
②如果工作链表是空的,调用schedule()函数进入睡眠状态。
③如果工作队列非空,会将自己设置为TASK_RUNNING,脱离等待队列。
④调用run_workqueue()函数执行被推后的工作。
while(!list_empty(&cwq->worklist)){
struct work_struct *work;
work_func_t f;
void *data;
work=list_entry(cwq->worklist.next,struct work_struct,entry);
f=work->func;
list_del_init(cwq->worklist.next);
work_clear_pending(work);
f(work);
run_workqueue循环遍历链表上每一个待处理的工作,执行每个节点上的func成员函数:
①当链表不为空时,选取下一个对象。
②获取希望执行的函数func和参数data。
③把该节点从链表上解下来,将待处理标志位pending清零。
④调用函数。
⑤重复执行。
3. 总结
每个工作者线程都由一个cpu_workqueue_struct结构体表示,而workqueue_struct结构体则表示给定类型的所有工作者线程。
工作队列workqueue_strut表示一类工作者线程,是多个处理器上该类工作者线程的集合。对于一类工作者线程,每个处理器上都要有且仅有一个该类工作者线程。
使用工作队列
- 创建推后的工作
创建工作结构体
DECLARE_WORK(name,void (*func)(void*),void *data);
INIT_WORK(struct work_struct *work,void(*func)(void*),void *data);
- 处理函数编写
void work_handler(void *data)
运行在进程上下文中。默认响应中断,不持有锁。因为内核线程在用户空间没有相关的内存映射,不能访问用户空间。(通常在发生系统调用时,内核代表用户空间的进程执行,才能访问用户空间)。
3. 对工作进行调度
交给缺省的events工作线程:
/* work马上被调度,所在处理器上的工作者线程被唤醒后,就会被执行 */
schedule_work(&work);
/* 延时被调度,delay指时钟节拍 */
schedule_delayed_work(&work,delay);
- 刷新操作
确保一些操作被执行。刷新指定工作队列。
void flush_scheduled_work(void);
函数会一直等待,直到所有对象被执行后才返回。在等待所有待处理的工作执行的时候,该函数会进入睡眠,只能在进程上下文中使用。
/* 取消任何与work_struct相关的挂起工作 */
int cancel_delayed_work(struct *work);
- 创建新的工作队列
缺省的工作队列不能满足需求的话,可以创建一个新的工作队列和与其相应的工作者线程。每个处理器上都会创建一个工作者线程。
只有在明确需要自己的一套线程提高性能的情况下,再这样做。
/* 创建工作队列 */
struct workqueue_struct *create_workqueue(const char *name);
name是内核线程的命名,如events队列的创建:
struct workqueue_struct *keventd_wq;
keventd_wq=create_workqueue("events");
调度工作队列:
int queue_work(struct workqueue_struct *wq,struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *wq,struct work_struct *work,unsigned long delay);
刷新工作队列 :
flush_workqueue(struct workqueue_struct *wq);
下半部机制的选择
下半部 | 上下文 | 顺序执行保障 |
---|---|---|
软中断 | 中断 | 没有 |
tasklet | 中断 | 同类型不能同时执行 |
工作队列 | 进程 | 没有(和进程上下文一样被调度) |
下半部之间加锁
在访问共享数据之前,需要禁止中断并得到所得使用权。不管是中断上下文还是进程上下文。
禁止下半部
常见的做法是,先得到一个锁,然后再禁止下半部的处理。
函数 | 描述 |
---|---|
void local_bh_disable() | 禁止本地处理器的软中断和tasklet的处理 |
void local_bh_enable() | 激活本地处理器的软中断和tasklet的处理 |