在中断的处理中,有一些不紧急不关键的任务在需要的时候可以被延迟一段时间来执行。对于中断服务程序来说,一般情况下如果它不结束执行,就不应该产生新的中断;而这些延迟的任务可以在打开中断的情形下执行,因而把它们从中断服务程序中提取出来可以降低内核的响应时间。linux支持两种非紧迫的、可中断的内核函数: 可延迟函数(包括软中断和tasklets )和通过工作队列执行的函数。
软中断和takslet是紧密相关的,tasklet在软中断之上实现。但是二者也有区别:linux定义了如下的软中断:
这些值都是枚举变量,从0开始,最大值为NR_SOFTIRQS,NR_SOFTIRQS表示系统中当前支持的软中断的数目。这些枚举变量也定义了对应的软中断的优先级,值越小优先级越高。软中断都存放在softirq_vec数组中,每个元素包括一个handler指针和一个当作参数的通用数据指针。
但是需要注意的是软中断的优先级只是定义了它们的执行顺序,而不会影响它们相对于其它“任务”的优先级,也不会影响它们被执行的频度。
preempt_count字段用于跟踪内核抢占和内核控制路径的嵌套,它存放在每个进程描述父的thread_info字段中。它分为不同的比特子字段,每个子字段有不同的含义:
每个CPU包含一个描述处于pending状态的软中断的32比特掩码。它位于irq_cpustat_t数据结构的_ _softirq_pending域。内核会周期性的检查是否有pengding的中断需要处理,如果有就会进行处理,这种检查是在特点的位置进行的(典型的位置是退出中断处理时irq_exit以及ksoftirqd中)。在软中断处理中,由于在执行一个软中断函数时可能出现新的软中断,因而为了保证可延迟函数的低延迟性,软中断处理函数会一直运行到执行完所有pending的软中断或者已经在软中断处理中运行了一定的时间(3.9.4中是2ms)。如果在软中断处理完成后仍有未处理的软中断,ksoftirqd将会处理它(每个CPU都有一个ksoftirqd内核线程)。
软中断运行时硬件中断是打开的,但是会在本地禁止软件中断,结果就是do_softirq在每个CPU上只能进入一次。在处理软中断时,会在关闭中断的情况下将本地CPU的pending的软中断保存到局部变量并且将本地CPU的软中断掩码清0,然后遍历每个pending的软中断并且先打开本地中断再执行软中断函数,再关闭本地中断,也就是说只有在调用中断处理函数时本地中断是打开的(但是本地的软中断十关闭的)。当把pending的处理完后,会重新读取本地CPU的软中断pending状态,如果仍有pending的就继续处理,直到处理完所有的pending的软中断或已经在软中断处理中运行了一定的时间。Tasklets以及高优先级的tasklets分别存放于tasklet_vec和tasklet_hi_vec数组中。这两个数组都包含NR_CPUS个类型为tasklet_head的元素, 每一个元素都包含一个指向由tasklet描述符组成的链表的指针。tasklet描述符的字段:
tasklet 是一个特殊的函数, 它在软中断上下文被调度。它可能被调度运行多次,但是tasklet调度不累积,也就是即使在tasklet被执行之前请求了多次来执行该tasklet,它也只运行一次。不会有同一个tasklet的多个实例同时运行。但是tasklet可以与SMP系统上的其他tasklet并行运行。因此, 如果多个tasklet会使用相同的资源, 它们必须采取某类加锁来避免彼此冲突。除非tasklet重新激活自己,否则每次tasklet激活只会运行一次。
在HI_SOFTIRQ 和TASKLET_SOFTIRQ的处理函数中,与当前CPU相关联的tasklet_vec或tasklet_hi_vec元素会被保存到局部变量,并且元素本身会被设置为NULL(在关闭本地中断的情况下)。然后遍历链表中的每一个元素,检查是否已经是运行状态(在其它CPU上),是否是禁止状态,如果都不是,并且tasklet被激活,tasklet就会被运行。API对应的头文件为linux/interrupt.h
DECLARE_TASKLET(name, func, data);声明并定义一个Tasklet
DECLARE_TASKLET_DISABLED(name, func, data);声明并定义一个Tasklet,且其初始状态为禁止的
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
为了使用tasklet必须首选分配一个tasklet_struct的数据结构并使用tasklet_init来初始化它。
static inline void tasklet_disable(struct tasklet_struct *t)
它用于禁止tasklet
static inline void tasklet_enable(struct tasklet_struct *t)
它用于启用tasklet
static inline void tasklet_schedule(struct tasklet_struct *t)
static inline void tasklet_hi_schedule(struct tasklet_struct *t)
它们用于激活tasklet,一个用于高优先级软中断相关联的tasklet,一个用于正常的软中断相关联的tasklet。
void tasklet_kill(struct tasklet_struct *t); 用于确保tasklet不再被调度执行,通常用在设备要关闭或模块要退出是。如果tasklet正在被调度执行,则该函数会先等待其执行完成,然后再开始自己的动作。如果tasklet会重新调度自己,则应该在重新调度时做某些判断,以防止永远无法kill掉。tasklet_kill的代码如下:
void tasklet_kill(struct tasklet_struct *t) { if (in_interrupt()) printk("Attempt to kill tasklet from interrupt\n"); while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) { do { yield(); } while (test_bit(TASKLET_STATE_SCHED, &t->state)); } tasklet_unlock_wait(t); clear_bit(TASKLET_STATE_SCHED, &t->state); }
tasklet执行时的核心代码如下:
if (tasklet_trylock(t)) { if (!atomic_read(&t->count)) { if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state)) BUG(); trace_irq_tasklet_low_entry(t); t->func(t->data); trace_irq_tasklet_low_exit(t); tasklet_unlock(t); continue; } tasklet_unlock(t); }
从执行的角度上说,workqueue中的函数是由内核线程调用的。
数据结构workqueue_struct用于表示工作队列,work_struct用于定义需要由工作队列(的线程)执行的任务。
API对应的头文件为 linux/workqueue.h
create_workqueue(name)
这实际上是一个宏,它返回新创建的工作队列的描述父地址。该函数还创建n个工作者线程(n是系统中CPU数目),并根据name为工作者线程命名。
create_singlethread_workqueue(name)
这也是一个宏,它完成和create_workqueue类似的工作,但是只创建一个工作者线程。
void destroy_workqueue(struct workqueue_struct *wq);
该函数用于销毁工作者队列,其参数为指向工作者队列数据结构的指针。