Linux内核第8章
内核为处理中断而提供的中断处理程序(上半部)有一些限制:
-中断处理程序以异步方式执行,并且它有可能会打断其它重要代码(甚至包括其它中断处理程序)的执行。为了避免被打断的时间过长,中断处理程序应该执行得越快越好。
-如果当前有一个中断处理程序正在执行,在最好的情况下(如果IRQF_DISABLED没有被设置),与该中断同级的其它中断会被屏蔽,在最坏的情况下(如果设置了IRQF_DISABLED),当前处理器上所有其它中断都会被屏蔽。因为禁止中断后硬件与操作系统无法通信,因此中断处理程序执行得越快越好。
-由于中断处理程序往往需要对硬件进行操作,所以它们通常有很高的时限要求。
-中断处理程序不在进程上下文中执行,所以它们不能阻塞。
操作系统必须有一个快速、异步、简单的机制负责对硬件做出迅速响应并完成那些时间要求严格的操作。对于那些其它的、对时间要求相对宽松的任务,就应该推后到中断被激活以后再去运行。
所以整个中断处理流程被分为两部分:中断处理程序(上半部),内核通过对它的异步执行完成对硬件中断的即时响应;下半部本章讨论。
8.1 下半部:
下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。
如何划分上半部与下半部由驱动程序开发者自己判断。
-如果一个任务对事件非常敏感,将其放在中断处理程序中执行。
-如果一个任务和硬件相关,将其放在中断处理程序中执行。
-如果一个任务要保证不被其它中断(特别是相同的中断)打断,将其放在中断处理程序中执行。
-其它任务,考虑放置在下半部执行。
下半部并不需要指明一个确切时间,只要把这些任务推迟一点,让它们在系统不太繁忙并且中断恢复后执行就可以了。通常下半部在中断处理程序一返回就会马上运行。下半部执行的关键在于它们运行的时候,允许响应所有的中断。
下半部可以通过多种机制来实现:
1.BH
最早的BH机制(bottom half)。它提供了一个静态创建、由32个bottom halves组成的链表。上半部通过一个32位整数中的一位来标识出哪个bottom half可以执行。每个BH都在全局范围内进行同步。即使分属于不同的处理器,也不允许任何两个bottom half同时执行。这种机制不够灵活,简单却有性能瓶颈。
2.任务队列
task queue,内核为此定义了一组队列,其中每个队列都包含一个由等待调用的函数组成链表。根据其所处队列的位置,这些函数会在某个时刻执行。驱动程序可以把他们自己的下半部注册到合适的队列上去。这种机制表现还不错,但仍不够灵活,对于一些性能要求较高的子系统,如网络部份,它不能胜任。
3.软中断和tasklet
软中断softirqs是一组静态定义的下半部接口,有32个,可以在所有处理器上同时执行----即使两个类型相同也可以。tasklet是一种基于软中断实现的灵活性强、动态创建的下半部实现机制。两个不同类型的tasklet可以在不同的处理器上同时执行但类型相同的tasklet不能同时执行。tasklet其实是一种在性能和易用性之间寻求平衡的产物,对于大部分下半部处理来说,用tasklet就足够了,像网络这样对性能要求非常高的情况才需要使用软中断。在使用软中断时需要注意:两个相同的软中断可能同时被执行,软中断还必须在编译期间进行静态注册。而tasklet可以通过代码进行动态注册。
4.工作队列
另外一个可以用于将工作推后执行的机制是内核定时器。内核定时器把操作推迟到某个确定的时间段之后执行。尽管本章讨论的其它机制可以把操作推后到除了现在以外的任何时间进行,但当必须保证在一个确定的时间段过去以后再运行时,应该使用内核定时器。
BH和任务队列在2.5中去除;软中断、tasklet和工作队列从2.3开始引入。
8.2 软中断:
8.2.1 软中断的实现:
软中断是在编译期间静态分配的。它不像tasklet那样能被动地注册或注销。软中断由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];
每个被注册的软中断都占据该数组的一项,因此最多可能有32个软中断。注意这是一个定值----注册的软中断数目的最大值没法动态改变。在当前版本中,这32个项中只能用到9个。
软中断处理程序:
软中断处理程序action的函数原型如下:
void softirq_handler(struct softirq_action *);
当内核运行一个软中断处理程序时,它就会执行这个action函数,其唯一的参数为指向相应softirq_action结构体的指针。例如,如果my_softirq指向softirq_vec数组的某项,那么内核就会用如下方式调用软中断处理程序中的函数:
my_softirq->action( my_softirq);
将整个结构体都传递给了软中断处理程序,保证将来在结构体中加入新的域时,无须对所有的软中断处理程序都进行变动。
一个软中断不会抢占另外一个软中断,唯一可以抢占软中断的是中断处理程序。不过,其它的软中断(甚至是同类型的软中断)可以在其它处理器上同时执行。
执行软中断:
一个注册的软中断必须在被标记后才会执行。这被称作触发软中断(raising the softirq)。通常,中断处理程序会在返回前标记它的软中断,使其在稍后被执行。于是,在合适的时刻,该软中断就会运行。
在下列地方,待处理的软中断会被检查和执行:
-从一个硬件中断代码处返回时;
-在ksoftirq内核线程中;
-在那些显式检查和执行待处理的软中断的代码中,如网络子系统中。
不管是用什么方法唤起,软中断都要在do_softirq()中执行。如果有待处理的软中断,do_softirq()会循环遍历每一个,调用它们的处理程序。
do_softirq()原理:
1)用局部变量pending保存local_softirq_pending()宏返回的值。它是待处理的软中断的32位位图---如果第n位被设置为1,那么第n位对应类型的软中断等待处理;
2)现在待处理的软中断位图已经被保存,可以将实际的软中断位图清零了;
3)将指针h指向softirq_vec的第一项;
4)如果pending的第一位被置为1,则h->action(h)被调用;
5)指针加1,所以现在它指向softirq_vec数组第二项;
6)位掩码pending右移一位,原来第二位现在位于第一位;
7)现在指针h指向数组第二项,pending位掩码第二位也到了第一位上。重复上面的步骤;
8)一直重复下去,知道pending变为0,最多可循环32次;
8.2.2 使用软中断:
软中断保留给系统中对时间要求最严格以及最重要的下半部使用。目前,只有2个子系统(网络和SCSI)直接使用软中断。此外,内核定时器和tasklet都是建立在软中断上的。tasklet对加锁的要求不高。软中断适合对于时间要求严格且自己能够高效完成加锁工作的应用。
分配索引:
在编译期间,通过在<linux/interrupt.h>定义的一个枚举类型来静态地声明软中断。内核用这些从0开始的索引来表示一种相对优先级。索引号小的软中断在索引号大的软中断之前执行(do_softirq()中按位图的从低位高高位顺序执行)。
建立一个新的软中断必须在此枚举类型中加入新的项。必须根据希望赋予他的优先级来决定加入的位置。习惯上,HI_SOFTIRQ通常作为第一项,而RCU_SOFTIRQ作为最后一项。新项可能插在BLOCK_SOFTIRQ和TASKLET_SOFTIRQ之间。
以下列出已有的tasklet类型:tasklet--优先级---软中断描述
HI_SOFTIRQ 0 优先级高的tasklet
TIMER_SOFTIRQ 1 定时器的下半部
NET_TX_SOFTIRQ 2 发送网络数据包
NET_RX_SOFTIRQ 3 接受网络数据包
BLOCK_SOFTIRQ 4 BLOCK装置
TASKLET_SOFTIRQ 5 正常优先权的tasklet
SCHED_SOFTIRQ 6 调度程序
HRTIMER_SOFTIRQ 7 高分辨率定时器
RCU_SOFTIRQ 8 RCU锁定
注册处理程序:
接着在运行时通过调用open_softirq()注册软中断处理程序,该函数有两个参数:软中断的索引号和处理函数。如网络子系统,在net/coreldev.c通过以下方式注册自己的软中断:
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。在一个中断处理程序运行的时候,当前处理器上的软中断被禁止。但其它的处理器仍可执行别的软中断。实际上,如果同一个软中断在它被执行的同时再次被触发了,那么另一个处理器可以同时运行其处理程序。这意味着任何共享数据(甚至是仅在软中断处理程序内部使用的全局变量)都需要严格的锁保护。这也是tasklet更受青睐的原因。单纯地禁止软中断处理程序同时执行不是很理想,如果仅仅通过互斥的加锁方式来防止它自身的并发执行,那么使用软中断就没有任何意义了。因此,大部分软中断处理程序,都通过采取单处理器数据(仅属于某一个处理器的数据,因此根本不需要加锁)或其它一些技巧来避免显式地加锁,从而提供出色的性能。
触发软中断:
raise_softirq()函数可以将一个软中断设置为挂起状态,让它在下次调用do_softirq()函数时投入运行。举个例子,网络子系统可能会调用:
raise_softirq(NET_TX_SOFTIRQ);
这会触发NET_TX_SOFTIRQ软中断。它的处理程序就会在内核下一次执行软中断时投入运行。该函数在触发一个软中断之前先要禁止中断,触发后再恢复为原来的状态。如果中断本来就已经被禁止了,那么就可以调用另一函数raise_softirq_irqoff(),这会带来一些优化效果。如:
raise_softirq_irqoff(NET_TX_SOFTIRQ);
在中断处理程序中触发软中断是最常见的形式。在这种情况下,中断处理程序执行硬件设备的相关操作,然后触发相应的软中断,最后退出。
8.3 tasklet:
tasklet是利用软中断实现的一种下半部机制,它和进程没有任何关系。
8.3.1 tasklet的实现:
tasklet本身也是一种软中断,tasklet有两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ。这两者唯一的实际区别在于,HI_SOFTIRQ类型的软中断先于TASKLET_SOFTIRQ类型的软中断执行。
tasklet结构体:
tasklet由tasklet_struct结构表示。每个结构体单独代表一个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处理函数的参数
};
结构体中的func成员是tasklet的处理程序(像软中断中的action一样),data是它唯一的参数。
state成员只能在0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值。TASKLET_STATE_SCHED表明tasklet已被调度,正准备投入运行,TASKLET_STATE_RUN表明该tasklet正在运行。TASKLET_STATE_RUN只有在多处理器的系统上才会作为一种优化来使用,单处理器系统任何时候都清楚单个tasklet是不是正在运行(它要么就是当前正在执行的代码,要么不是)。
count成员是tasklet的引用计数器。如果它不为0,则tasklet被禁止,不允许执行;只有当它为0时,tasklet才被激活,并且在被设置为挂起状态时,该tasklet才能够执行。
调度tasklet:
已调度的tasklet(等同于被触发的软中断)存放在两个单处理器数据结构:tasklet_vec(普通tasklet)和tasklet_hi_vec(高优先级的tasklet)。这两个数据结构都是由tasklet_struct结构体构成的链表。链表中每个tasklet_struct代表一个不同的tasklet。
tasklet由tasklet_schedule()和tasklet_hi_schedule()函数进行调度,它们接受一个指向tasklet_struct结构的指针作为参数。这两个函数非常类似,区别在于一个使用TASKLET_SOFTIRQ而另一个用HI_SOFTIRQ。
tasklet_schedule()的执行步骤:
1)检查tasklet的状态是否为TASKLET_STATE_SCHED。如果是,说明tasklet已经被调度过了,函数立即返回。
2)调用_tasklet_schedule()。
3)保存中断状态,然后禁止本地中断。在我们执行tasklet代码时,这么做能够保证当tasklet_schedule()处理这些tasklet时,处理器上的数据不会弄乱,
4)把需要调度的tasklet加到每个处理器一个的tasklet_vec链表或tasklet_hi_vec链表的表头上去。
5)唤起TASKLET_SOFTIRQ或HI_SOFTIRQ软中断(将32位中断位图的第0和第5位置为1),这样在下一次调用do_softirq()时就会执行该tasklet。
6)恢复中断到原状态并返回。
在前面的内容中我们曾经提到过挂起,do_softirq()会尽可能早地在下一个合适的时机执行。由于大部分tasklet和软中断都是在中断处理程序中被设置成待处理状态,所以最近一个中断返回的时候看起来就是执行do_softirq()地最佳时机。因为TASKLET_SOFTIRQ和HI_SOFTIRQ已经被触发了,所以do_softirq()会执行相应的软中断处理程序。而这两个处理程序,tasklet_action()和tasklet_hi_action(),就是tasklet处理的核心。它们的处理如下:
1)禁止中断,并为当前处理器检索tasklet_vec或tasklet_hi_vec链表。
2)将当前处理器上的该链表设置为NULL,达到清空效果。
3)允许响应中断。没有必要再恢复它们回原状态,因为这段程序本身就是作为软中断处理程序被调用的,所以中断是应该被允许的。
4)循环遍历获得链表上的每一个待处理的tasklet。
5)如果是多处理器系统,通过检查TASKLET_STATE_RUN来判断这个tasklet是否正在其它处理器上运行。如果它正在运行,那么现在就不要执行,跳到下一个待处理的tasklet去。
6)如果当前这个tasklet没有执行,将其状态设置为TASKLET_STATE_RUN,这样别的处理器就不会再去执行它了。
7)检查count值是否为0,确保tasklet没有被禁止。如果tasklet被禁止了,则跳到下一个挂起的tasklet去。
8)我们已经知道这个tasklet没有在其它地方执行,并且被我们设置成执行状态,这样它在其它部分就不会被执行,并且引用计数为0,现在可以执行tasklet的处理程序了。
9)tasklet运行完毕,清除tasklet的state域的TASKLET_STATE_RUN状态标志。
10)重复执行下一个tasklet,直至没有剩余的等待处理的tasklet。
tasklet的实现是:所有的tasklet通过重复运用HI_SOFTIRQ和TASKLET_SOFTIRQ这两个软中断实现,当一个tasklet被调度时,内核就会唤起这两个软中断中的一个。随后,该软中断会被特定的函数处理,执行所有已调度的tasklet。这个函数保证同一时间里只有一个给定类型的tasklet会被执行(但其它不同类型的tasklet可以同时执行)。所有这些复杂性被一个简洁的接口隐藏起来了。
8.3.2 使用tasklet:
声明你自己的tasklet:
既可以静态地创建tasklet,也可以动态地创建它。选择哪种方式取决于你到底是有一个对tasklet的直接引用还是间接引用。如果准备静态地创建一个tasklet(也就是有一个它的直接引用),使用下面<linux/interrupt.h>中定义的两个宏之一:
DECLARE_TASKLET(name, func, data);
DECLARE_TASKLET_DISABLED(name, func, data);
这两个宏都能根据给定的名称静态地创建一个tasklet_struct结构。当该tasklet被调度后,给定的函数func会被执行,它的参数由data给出。这两个宏之间的区别在于引用计数器的初始值设置不同。前一个宏把创建的tasklet的引用计数器设置为0,该tasklet处于激活状态。后一个宏把引用计数器设置为1,所以该tasklet处于禁止状态。
DECLARE_TASKLET(name, func, data)等价于struct tasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0), func, data};
还可以通过将一个简介引用(一个指针)赋给一个动态创建的tasklet_struct结构的方式来初始化一个tasklet_init():
tasklet_init( t, tasklet_handler, dev); //这是动态创建
编写自己的tasklet处理程序:
tasklet处理程序必须符合规定的函数类型:
void tasklet_handler(unsigned long data);
因为是靠软中断实现,所以tasklet不能睡眠。这意味着你不能再tasklet中使用信号量或者其它什么阻塞式的函数。由于tasklet运行时允许响应中断,所以必须做好预防工作(如屏蔽中断然后获取一个锁),如果你的tasklet和中断处理程序之间共享了某些数据的1话。如果你的tasklet和其它的tasklet或者是软中断共享了数据,你必须进行适当地锁保护。
调度自己的tasklet:
通过调用tasklet_schedule()函数并传递给它相应的tasklet_struct的指针,该tasklet就会被调度以便执行:
tasklet_schedule(&my_tasklet); //把my_tasklet标记为挂起
在tasklet被调度以后,只要有机会它就会尽可能早地运行。在它还没有得到运行机会之前,如果有一个相同的tasklet又被调度了,那么它仍然只会运行一次。而如果这时它已经开始运行了,比如说在另一个处理器上,那么这个新的tasklet会被重新调度并再次运行。作为一种优化措施,一个tasklet总在调度它的处理器上执行---这是希望能更好地利用处理器的高速缓存。
可以调用tasklet_disable()函数来禁止某个指定的tasklet,如果该tasklet当前正在执行,该函数会等到它执行完毕后再返回。也可以调用tasklet_disable_nosync()函数,它也能用来禁止指定的tasklet且无须在返回前等待tasklet执行完毕。调用tasklet_enable()函数可以激活一个tasklet,如果希望激活DECLARE_TASKLET_DISABLED()创建的tasklet,也得调用tasklet_enable()。也可以调用tasklet_kill()函数从挂起的队列中去掉一个tasklet,该函数的参数是一个指向某个tasklet的tasklet_struct的长指针,该函数可以休眠,禁止在中断上下文中使用它。
ksoftirq:
每个处理器都有一组辅助处理软中断(和tasklet)的内核线程。当内核中出现大量软中断(tasklet)时,这些内核进程就会辅助处理它们。
软中断被触发的频率可能很高(像在进行大流量的网络通信期间)。更不利的是,处理函数有时还会自行重复触发。也就是说,当一个软中断本身出现的频率就高。再加上它们又有将自己重新设置为可执行状态的能力,那么就会导致用户空间进程无法获得足够的处理器时间而饥饿。
解决方案有两种:
1.只要还有被触发并等待处理的中断,本次执行就要负责处理,重新触发的软中断也在本次执行返回前被处理。这种方案在系统负载较高时就效果很差。
2.选择不处理重新触发的软中断。对于重新触发的软中断,会等一段时间后再去处理。这种方案不会让用户进程饥饿但是没有好好利用闲置的系统资源。
最终内核中实现的方案是不会立即处理重新触发的软中断,而作为改进,当大量软中断出现时,内核会唤醒一组内核线程来处理这些负载。。这些线程在最低优先级上运行,避免跟其它重要任务抢占资源。但它们最终会得到指向。
每个处理器都有一个这样的线程。所有线程的名字都叫做ksoftirqd/n,区别在于n,他对应的是处理器的编号。为了保证只要有空闲的处理器,他们就会处理中断,所以给每个处理器都分配一个这样的线程,一旦该线程初始化,它就会执行死循环操作:只要有待处理的软中断(softirq_pending()函数发现),ksoftirq就会调用do_softirq()去处理它们。通过重复这样的操作,重新触发的软中断也会被执行。如果有必要的话,每次迭代后都会调用schedule()以便让更重要的进程得到处理机会。当所有需要执行的操作都完成以后,该内核线程将自己设置为TASK_INTERRUPTIBLE状态,唤起调度程序选择其它可执行进程投入运行。
PS:软中断有一个固定长度的数组,每个数组对应一个softirq结构(只包含一个处理函数),中断处理程序唤起软中断(为数组的某个项设置处理函数),软中断检查数组的位图来进行相应处理程序的执行;tasklet对应于软中断的其中两种(HI_SOFTIRQ和TASKLET_SOFTIRQ),tasklet可以静态或动态创建tasklet_struct结构体(有状态、处理函数、计数器、参数等),所有tasklet在调度过程中根据所属的两种软中断类型放入两个链表中,不同中断处理程序会对应某种tasklet_struct,tasklet的唯一性通过其tasklet_struct结构体实现(两个相同的tasklet不能同时执行,但其它tasklet可以在其它处理器上执行)。
8.4 工作队列:
工作队列worker queue是另外一种可将工作推后执行的形式,它将工作推后交由一个内核线程去执行----这个下半部总是在进程上下文中执行。所以工作队列允许重新调度甚至是睡眠。
如果推后执行的任务需要睡眠,那么就选择工作队列,如果推后执行的任务不需要睡眠,那么就选择软中断或tasklet。
8.4.1 工作队列的实现:
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其它部分排到队列里的任务。它创建的这些内核线程称为工作者线程。工作队列可以让你的驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列子系统提供了一个缺省的工作者线程来处理这些工作。因此,工作队列最基本的表现形式,就转变成了一个把需要推后执行的任务交给特定的通用线程这样的一种接口。
缺省的工作者线程叫做events/n,这里n是处理器的编号;每个处理器对应一个线程。缺省的工作者线程会从多个地方得到被推后的工作。许多内核驱动程序都把它们的下半部交给缺省的工作者线程去做。除非一个驱动程序或者子系统必须建立一个属于它自己的内核线程,否则最好使用缺省线程。
处理器密集型和性能要求严格的任务会因为拥有自己的工作者线程而获得好处。同时创建自己的工作者线程也有助于减轻缺省线程的负担,避免工作队列中其它需要完成的工作处于饥饿状态。
1. 表示线程的数据结构:
工作线程用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_workqueue_struct是kernel/workqueue.c中的核心数据结构:
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; //关联线程
};
每个工作者线程类型关联一个自己的workqueue_struct。在该结构体里面,给每个线程分配一个cpu_workqueue_struct,因而也就是给每个处理器分配一个,因为每个处理器都有一个该类型的工作者线程。
2. 表示工作的数据结构:
所有的工作者线程都是用普通的内核线程实现的,它们都要执行worker_thread()函数。在它初始化完以后,这个函数执行一个死循环并开始休眠。当有操作被插入到队列里的时候,线程就会被唤醒,以便执行这些操作。当没有剩余的操作时,它又会继续休眠。
工作用<linux/workqueue.h>中定义的work_struct结构体表示:
struct work_struct{
atomic_long_t data;
struct list_head entry;
work_func_t func;
};
这些结构体被连接成链表,在每个处理器上的每种类型的队列都对应这样一个链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。
work_thread()函数的核心流程简化如下:在一个死循环中完成以下功能:
1)线程将自己设置为休眠状态(state被设为TASK_INTERRUPTIBLE),并把自己加入到等待队列中。
2)如果工作链表是空的,线程调用schedule()函数进入睡眠状态。
3)如果链表中有对象,线程不会睡眠。相反,它将自己设置成TASK_RUNNING,脱离等待队列。
4)如果链表非空,调用run_workqueue()函数执行被推后的工作。
下一步,由run_workqueue()函数来实际完成推后到此的工作:该函数遍历链表上每个待处理的工作,执行链表每个节点上的work_struct中的func成员函数:
1)当链表不为空时,选取下一个节点对象。
2)获取我们希望执行的函数func及其参数data。
3)把该节点从链表上解下来,将待处理标志位pending清零。
4)调用函数。
5)重复执行。
3. 工作队列实现机制的总结:
工作者线程 <----------- cpu_workqueue_struct //每个cpu只有1个
workqueue_struct结构体 //每种工作者线程有一个
work_struct结构体 //每个延迟函数有一个
位于最高层的是工作者线程。系统允许有多种类型的工作者线程存在。对于指定的一个类型,系统的每个CPU上都有一个该类的工作者线程。每个工作者线程都由一个cpu_workqueue_struct结构体表示。而workqueue_struct结构体则表示给定类型的所有工作者线程。
工作处于最底层。驱动程序创建这些需要推后执行的工作。它们用work_struct结构表示。这个结构体中最重要的是一个指针,它指向一个函数,而正是这个函数负责处理需要推后执行的具体任务。工作会被提交给某个具体的工作者线程,然后这个工作者线程会被唤醒并执行这些排好的工作。
8.4.2 使用工作队列
1.创建推后的工作:
首先要做的是实际创建一些需要推后完成的工作。可以通过DECLARE_WORK在编译时静态地创建该结构体:
DECLARE_WORK(name, void (*func) (void*), void *data);
这样静态创建了一个名为name,处理函数为func,参数为data的work_struct结构体。
同时也可以在运行时通过指针创建一个工作:
INIT_WORK(struct work_struct *work, void (*func) (void*), void *data);
动态初始化一个由work指向的工作,处理函数为func,参数为data。
2. 工作队列处理函数
工作队列处理函数的原型是:
void work_handler(void *data)
这一个函数由一个工作者线程执行,因此,函数会运行在进程上下文中。默认情况下,允许响应中断,并且不持有任何锁。如果需要,函数可以睡眠。尽管操作处理函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户控件没有相关的内存映射。
通常在发生系统调用时,内核会代表用户空间的进程运行,此时它才能访问用户空间,也只有在此时它才会映射用户空间的内存。
3. 对工作进行调度
现在工作已经被创建,可以调度它了。想要把给定工作的处理函数提交给缺省的events工作进程,只需调用:
schedule_work(&work);
work马上就会被调度,一旦其所在的处理器上的工作线程被唤醒,它就会被执行。
有时希望工作延迟一段时间后再执行:
schedule_delayed_work(&work, delay);
&work指向的work_struct直到delay指定的时钟节拍用完以后才会执行。
4. 刷新操作
排入队列的工作会在工作者线程下一次被唤醒时执行。有时需要确保不再有待处理的工作,所以内核准备了一个用于刷新指定工作队列的函数:
void flush_scheduled_work(void);
函数会一直等待,直到队列中所有对象都被执行以后才返回。在等待所有待处理的工作执行的时候,该函数会进入休眠状态,所以只能在进程上下文中使用它。
注意上一个函数不会取消任何延迟执行的工作。取消因为schedule_delayed_work调用而延迟的工作应使用:
int cancel_delay_work(struct work_struct *work);
这个函数可以取消任何与work_struct相关的挂起工作。
5. 创建新的工作队列
创建一个新的任务队列和与之相关的工作者线程,只需要调用一个简单的函数:
struct workqueue_struct *create_workqueue(const char *name);
name参数用于该内核线程的命名。比如,缺省的events队列的创建就调用的是:
struct workqueue_struct*keventd_wq; keventd_wq=create_workqueue("events");
创建一个工作队列的时候无须考虑工作队列的类型。在创建之后,可以调用下面列举的函数。这些函数与schedule_work()以及schedule_delayed_work()相近,唯一的区别就在于它们针对给定的工作队列而不是缺省的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);
该函数和前面讨论的flush_scheduled_work()作用相同,只是它在返回前等待清空的是给定的队列。
8.6 在下半部之间加锁
在使用下半部机制时,即使是在一个单处理器系统上,避免共享数据被同时访问也是至关重要的。一个下半部实际上可能在任何时候执行。
使用tasklet的一个好处是,它自己负责执行的序列化保障:两个相同类型的tasklet不允许同时执行,即使在不同的处理器上也不行。tasklet之间的同步----多个不同类型的tasklet共享同意数据时---需要正确使用锁机制。
如果进程上下文和一个下半部共享数据,在访问这些数据之前,需要禁止下半部的处理并得到锁的使用权。做这些是为了本地和SMP的保护并且防止死锁的出现。
如果中断上下文和一个下半部共享数据,在访问数据之前,需要禁止中断并得到锁的使用权。所做的这些也是为了本地和SMP的保护并且防止死锁的出现。
任何在工作队列中被共享的数据也需要使用锁机制。其中有关锁的要点和在一般内核代码中没有什么区别,因为工作队列本来就是在进程上下文中执行的。
8.7 禁止下半部
一般单纯禁止下半部的处理是不够的,为了保证共享数据的安全,更常见的做法是,先得到一个锁然后再禁止下半部的处理。
如果需要禁止所有下半部处理(软中断和tasklet),可以使用local_bh_disable()函数。允许下半部进行处理,可以调用local_bh_enable()函数。这两个函数可以嵌套使用,一次禁止对应一次激活。
函数通过preempt_count(内核抢占时也用这个)为每个进程维护一个计数器。当计数器变为0时,下半部才能够被处理。
这些函数并不能禁止工作队列的执行。工作队列在进程上下文中运行,不会涉及异步执行的问题。而软中断和tasklet是异步执行(前后前后按顺序)。