- 在2.6版本中,内核提供了三种下半部实现机制:软中断、tasklets和工作队列。
- 内核定时器:把操作推迟到某个确定的时间段之后执行
- 软中断
<a>软中断由softirq_action表示,softirq.c中定义了包含有32个该结构体的数组。
struct softirq_action{
void (*action)(struct softirq_action *)
}
static struct softirq_action softirq_vec[NR_SOFTIRQS];
<b>软中断处理程序action的函数原型如下:
void softirq_handler(struct softirq_action *)
如果my_softirq指向softirq_vec数组的某项,那么内核会用如下方式调用软中断处理程序中的函数:
my_softirq->action(my_softirq);
注意:这里把整个结构体都传递给软中断处理程序而不是仅仅传递数据值,这个小技巧可以保证将来在结构体中加入新的域时,无需对所有软中断处理程序都进行变动。
注意:一个软中断不会抢占另一个软中断,唯一可以抢占软中断的是中断处理程序。不过,其他软中断可以在其他处理器上同时执行。
<c>执行软中断
在以下地方,待处理的软中断会被检查和执行:
- 从一个硬件中断代码处返回
- 在ksoftirqd内核线程中
- 显式检查和执行待处理软中断的代码
注意:不管用什么方法唤起,软中断都要在do_softirq()中执行。
<d>使用软中断
分配索引:在interrupt.h中定义的一个枚举类型来静态声明软中断,按照希望的优先级添加自己的软中断;
注册处理程序:open_softirq(NET_TX_SOFTIRQ, net_tx_action);
注意:软中断处理程序执行时,允许响应中断,但它自己不能休眠;在一个处理程序运行时,当前处理器上的软中断被禁止。但其他处理器仍可以执行别的软中断,这就意味着共享数据加锁的问题。引入软中断是因为其可扩展性,可以扩展到多个处理器。
触发软中断:raise_softirq(NET_TX_SOFTIRQ)将一个软中断设置为挂起状态,内核在下次调用do_softirq()时投入运行。该函数在触发一个软中断前先要禁止中断。
- tasklet
<a>tasklet的实现
tasklet由两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ,前者先于后者执行。
tasklet有tasklet_struct结构表示:
struct tasklet_struct{
struct tasklet_struct *next;
unsigned long state; //只能在0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值;TASKLET_STATE_SCHED表明tasklet已被调度,正准备投入运行;TASKLET_STATE_RUN表明该tasklet正在运行,只有在多处理器系统上才会作为一种优化来使用,单处理器系统任何时候都清楚单个tasklet是不是正在运行。
atomic_t count; //引用计数器,如果它不为0,则tasklet被禁止
void (*func)(unsigned long);
unsigned long data;
}
<b>调度tasklet
已调度的tasklet存放在两个链表中:tasklet_vec和tasklet_hi_vec。tasklet由tasklet_schedule()函数进行调度,主要就是把需要调度的tasklet加到每个处理器一个的tasklet_vec链表,然后唤起软中断。
总结:raise_softirq()和tasklet_schedule()在触发软中断和调度tasklet前都要先保存中断状态,然后禁止本地中断,这么做能够保证当tasklet_schedule()处理这些tasklet时,处理器上的数据不会弄乱。
注意:同一时间,相同类型的tasklet只能有一个执行(通过TASKLET_STATE_RUN状态标志)。
<c>使用tasklet
静态创建:DECLARE_TASKLET(name, func, data)
DECLARE_TASKLET_DISABLED(name, fun, data); 等价于:
struct tasklet_struct my_tasklet = {NULL, 0, ATOMIC_INIT(0), fun, data};
动态创建:tasklet_init(t, tasklet_handler, dev);
编写自己的tasklet处理程序:
void tasklet_handler(unsigned long data)
注意:
- 因为是用软中断实现的,所以tasklet不能睡眠,这意味着不能再tasklet中使用信号量或者其他阻塞式的函数。
- 由于tasklet运行时允许响应中断,如果你的tasklet和中断处理程序之间共享了某些数据的话,必须做好预防工作(如屏蔽中断然后获取一个锁)
- 两个相同的tasklet绝不会同时执行,这点和软中断不同;如果你的tasklet和其他tasklet或者软中断共享了数据,必须恰当地锁保护。
<d >调度你自己的tasklet
tasklet_schedule(&my_tasklet);
禁止指定的tasklet:tasklet_disable(&my_tasklet);
激活指定的tasklet:tasklet_enable(&my_tasklet);
从挂起的队列中去掉一个tasklet:tasklet_kill();
<e>ksoftirqd
作用:辅助处理软中断(和tasklet)的内核线程。
- 当大量软中断出现的时候内核会唤醒一组内核线程来处理这些负载(只要do_softirq()函数发现已经执行过的内核线程重新出发了它自己,软中断内核线程就会被唤醒)
- 这些线程在最低的优先级上运行(nice值是19),这能避免它们跟其他重要的任务抢夺资源。
- 每个处理器都有一个这样的线程。名字叫做ksoftirqd/n,n对应的是处理器编号。
- 工作队列
<a>工作队列由内核线程执行,在进程上下文中执行。
<b>工作队列允许重新调度甚至是睡眠
<c>在工作队列和软中断/tasklet中做出选择非常容易。如果推后执行的任务需要睡眠,就选择工作队列。不需要睡眠,就选择软中断或tasklet
<d>唯一能在进程上下文中运行、睡眠的下半部机制。在需要获得大量内存、需要获取信号量、需要执行阻塞式的I/O操作时,工作队列会非常有用。
<e>工作队列的实现
- 工作者线程:创建的内核线程负责执行由内核其他部分排到队列里的任务
- 缺省的工作者线程叫做events/n,这里的n是处理器编号
- 表示线程的数据结构
工作者线程用workqueue_struct结构标识
struct workqueue_struct{
struct cpu_workqueue_struct cpu_wq[NR_CPUS];
…
}
每个处理器,每个工作者线程对应一个cpu_workqueue_struct结构体。
struct cpu_workqueue_struct {
spinlock_t lock;
struct workqueue_struct *wq; //关联工作队列结构
}
工作用work_struct结构体表示
struct work_struct{
atomic_long_t data;
struct list_head entry;
work_fun_t func;
- 使用工作队列
缺省的events工作队列
<a>创建推后的工作:
编译时静态创建 DECLARE_WORK(name, void (*func) (void *), void *data);
动态创建 INIT_WORK(struct work_struct *work, void (*func) (void *), void *data);
<b>工作队列处理函数:
void work_handler(void *data)
<c>对工作进行调度:
schedule_work(&work);
schedule_delayed_work(&work, delay);
<d>刷新操作:在继续下一步工作之前,必须保证一些操作已经执行完毕。
void flush_scheduled_work(void);
函数会一直等待,知道队列中的所有对象都被执行才返回。该函数不取消任何延迟执行的工作。取消延迟执行工作应调用:
Int cancel_delayed_work(struct work_struct *work);
<f>创建新的工作队列
-
- 创建自己的工作队列
struct workqueue_struct *create_workqueue(const char *name);
-
- 对工作进行调度
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 | 中断 | 同类型不能同时执行 |
工作队列 | 进程 | 没有(和进程上下文) |
如果你需要休眠,那么只能选择工作队列,否则最好用tasklet。
- 在下半部直接加锁
<a>如果进程上下文和一个下半部共享数据,在访问这些数据之前,你需要禁止下半部的处理并得到锁的使用权。为了本地和SMP的保护并且防止死锁的出现
<b>如果中断上下文和一个下半部共享数据,在访问这些数据之前,你需要禁止下半部的处理并得到锁的使用权。
- 禁止下半部
<a>为了保证共享数据的安全,驱动程序通常的做法:先得到一个锁然后再禁止下半部的处理。
<b>禁止本地处理器的软中断和tasklet:void local_bh_disable()
<c>激活本地处理器的软中断和tasklet:void local_bh_enable()
<d>这些函数不能禁止工作队列的执行。因为工作队列是在进程上下文运行的,不涉及异步执行的问题。由于软中断和tasklet是异步发生的(就是说在中断处理返回的时候),所以内核必须禁止它们。