Linux Kernel Development 笔记(七)下半部处理以及延迟工作

来源:互联网 发布:青岛雨人软件 编辑:程序博客网 时间:2024/06/06 17:24

中断处理是一种很重要的操作系统中的一部分,是一种用以处理硬件中断的内核机制。但因为中断处理的各种限制,故此其只能形成中断处理中的前半部分。以下是中断处理的限制:

1. 中断函数是异步的,因此会中断其他可能很重要的代码,包括别的中断。因此为了防止停顿被中断的代码过久,中断处理需要越迅速越好。

2. 中断函数运行时,最好的情况下是禁止当前的中断,最坏的情况是禁止处理器的所有中断。因为会禁止导致硬件与操作系统的交流,故此这种中断处理要求越迅速越好。

3. 中断函数都是时间紧迫的,因为它们与硬件直接打交道。

4. 中断函数是运行在中断上下文中的,因此其不能被阻塞,这限制了很多可做的事情。

目前很明确的是,中断函数只是解决硬件中断的一部分。操作系统需要一种快速的,异步的,简单的,能立即响应硬件的处理机制,而中断函数就很好的适应此工作。但其他相对来说没那么紧迫的工作,可以推迟到中断恢复后的某个时间点。因此处理硬件中断被分成两部分。第一部分(上半部分)就是中断函数,以快速响应硬件为目的的异步执行。第二部分(下半部分)就是bottom halves。

Bottom Halves是处理与中断相关,但不在中断函数的做的事情。在现实中,这几乎是所有你需要响应中断而进行的工作,因为只有很小一部分是放在中断函数那里完成。通过把大部分工作放到Bottom Halves那里,你可以让中断函数尽快的返回被中断的代码那里。然后,还是有些工作需要中断函数来处理的。如,中断函数需要保证对硬件中断的一个响应,或需要与硬件进行数据交互等。这些工作是时间敏感的,所以这些工作在中断函数里是合理的。

当然,所有这些工作也可以在bottom halves中进行。如与硬件的数据交互。目前没有一个硬性的规则来告诉你那些工作适合在那部分执行。这个决定权是归驱动人员的。虽然没有规定那种安排是合法的,但其实可以保证安排是次优的。主要记住,中断函数是异步的,至少保证当前的处理的中断线是禁止的。执行时间越短越好。下面一些建议是针对如何在上下半部安排工作的提出的:

1. 如果工作时间是敏感的,在中断函数执行

2. 如果工作是与硬件打交道的,放在中断函数执行

3. 如果需要确保另外一个中断不会中断它的工作,则放在中断函数执行

4. 其他的,可以考虑是否放在bottom halves上处理。

当尝试去写自己的驱动代码的时候,可以参考其他的中断函数以及对应的bottom halves。当要做决定哪些工作放在那个部分的时候,就必须自己想清楚那个部分是必须放在中断函数的,那些是可以放在botto halves的。一般说来,中断函数处理越快越好。

清楚知道为什么要推迟工作以及什么时刻去推迟它是一个很关键的。你必须限制中断函数处理的任务,因为每一个中断函数都会禁止当前的中断线,更坏的情况下是(IRQF_DISABLED)禁止所有的中断。对于系统响应来说,最小化中断函数的执行时间是很重要的。而且,中断函数是异步与别的代码处理的,这很明显说明你需要尽量短的处理中断函数,因为中断函数是中断了当前的代码。 因此某些工作需要推迟到bottom halves来处理。推迟的意思仅仅是指不是当前。关键点是不在未来某个指定的时间里,而是简单的推迟到只要系统不繁忙,以及中断恢复的某个时刻。经常的,bottom halves都是在中断返回后立即执行的。这里的关键是,此时中断是允许的。Linux不是唯一一个这样把中断操作分成两部分的系统,大部分系统都如此。这样区分,有利于保持系统的低延时。

跟中断函数只靠自己处理上半部分工作不同的是,bottom halves有多种机制来实现。这些机制是不同的接口,以及不同的子系统来使你实现bottom halves。在Linux的历史过程中,存在着多个bottom-half的实现机制,某些机制还有相似的怪名字,需要特别的程序员才能说出起名字。


原本的bottom half

刚开始,linux只依靠bottom half来实现bottom halves。这个名字是逻辑的。因为在当时,这是仅有的一种推迟工作的手段。为了避免与我们一般称呼的bottom half有混淆,一般称为BH。BH的接口非常的简单,就如旧时候别的事物一样。它给整个系统提供了静态创建的32个bottom havels。上半部分通过设置32位的整数来标记bottom half是否运行。每一个BH是全局同步的,不存在并行的BH,甚至是在不同处理器运行都不允许。这种方式是很易使用,但缺乏灵活,有瓶颈。


任务队列

后来,开发者引入了一种任务队列的方式即提供了一种推迟工作的方法,也作为BH机制的一种替代。内核定义了一系列的队列,每一个队列都包含一个被叫函数的列表。被排队的函数会依赖于其所在队列来决定其在某个时刻运行。这运行的不错,但依然缺乏灵活以致与不能完全替代BH机制。而且,对于需要高效的网络运作来说,不是一种轻量级解决之道。


软中断以及子任务

Linux 2.3中引入了softirq 和 tasklet (软中断和子任务)。除了与现有驱动的兼容性问题外,其可以完全的替代BH机制。Softirqs是一套静态创建的bottom halves,可以同时在任一个处理器上运行。即使是两个同类型的,也可以并行运行。Tasklets名字比较古怪以及迷惑,但其是一种在灵活的,动态创建于softirq之上的一种bottom halves。两个不同类型的tasklet可以在不同的处理器上同时运行,但相同类型的不能。因此,tasklet在使用简易上以及执行效率是取得很好的平衡。对于多数的bottom-half处理来说,tasklet是足够的。对于效率很紧迫的情况下,softirq是很有帮助的,如网络。但使用softirq需要很谨慎,因为同类型的可以并行。同时,softirq必须在编译前静态注册。相反,tasklets可以动态创建。更让这个话题疑惑的是,一些人会把bottom halves直接当成软中断或softirq。换句话说,他们会把softirq以及bottom halves机制都统称为softirq。同样也有人把BH与tasklet混为一谈。当Linux 2.5以后,BH接口被抛弃了,因为人们转向另外一套接口。同时,任务队列(task queue)被工作队列(work queue)所替代了。工作队列是一种简单但行之有效的可以把任务推迟到进程上下文处理的一种机制。结果,目前在内核中有三种机制:softirq, tasklets和work queue。


Softirqs

Softirqs比较少直接使用。tasklets是一种相对较多的公共bottom half方式。因为tasklets是基于softirq建立的,所以先讨论softirq。代码是处于 kernel/softirq.c

Softirq是由softirq_action结构体代表的,定义在linux/interrupt.h中

struct softirq_action

{

void (*action)(struct softirq_action *);

}

一个有32个的结构数组定义在kernel/softirq.c中。static struct softirq_action softirq_vec[NR_SOFTIRQS];

每一个注册的softirq都会消耗一个数组项。因此共有NR_SOFTIRQS个注册的softirqs。因此其数量是编译期间就决定的,不能后期更改。内核限制其个数最多32个,目前只有9个。

softirq的处理函数原型为  void softirq_handler(struct softirq_action *);

当内核执行softirq函数的时候,会把对应的softirq_action结构体作为唯一的参数传给处理函数。举个例子如果my_softirq指向数组的一个项,内核会这样调用:

my_softirq->action(my_softirq);

这看上去有点怪,内核把整个结构发给了函数。这个技巧保证了未来对结构体的附加新东西的时候,可以不用更改其处理函数。softirq永远不会抢占另外一个softirq,唯一会抢占softirq的是中断函数(上半部)。softirq即使是同类型的,也能同时运行在另一个处理器那里。一个注册了的softirq必须在使用前被标注,这个称为raising softirq. 通常来说,一个中断函数会在其运行前标注其softirq来执行,接着在适合的时候,softirq运行。计划中的softirq会被检测以及在以下地方执行:

1. 从硬件中断代码路径中返回的时候

(在中断函数处理后,在未返回给被中断的代码之前一般是irq_exit中,会检测softirq是否有pending,此时还是处于中断上下文中。中断上下文有分硬中断上下文,软中断上下文以及不可屏蔽上下文三种)

2. 在ksoftirqd 内核线程中

3. 在任何明显指定检测以及执行softirq的代码中,例如网络子系统

softirq是在__do_softirq()中执行的,而这个函数是通过调用do_softirq()调用的。这个函数很简单,如果存在待执行的softirq, __do_softirq会循环的调用其处理函数。


这是这个函数重要一部分的简化版。这个片段是softirq的核心部分。它检测以及执行待运行的softirq,特殊的:

1. 把local_softirq_pending()的返回值存放于pending这个32位的变量,每一位都标志一个softirq是否pending状态

2. 因为pending的状态已经保存了,故此可以清掉实际的位标志了

3. 把h指向第一个softirq数组的值

4. 如果第一位设置了,则h->action(h)被调用

5. h增加一,此时h指向数组的第二个元素

6. pending标志位右移一位,此时pending的第一位代表的是第二个softirq

7. 重复以上操作,知道pending为0.


softirq一般保留给重要的以及时间紧迫的bottom-half处理来用。目前有两个子系统-网络以及块设备会直接使用。同时,定时器以及tasklets会基于softirq来创建。如果你增加一个softirq,你要清楚为什么使用tasklets不足以满足你的需求。tasklets是动态的创建的,而且使用简单,对锁要求低,而且依然比较有效率。然后,对于时间紧迫的,同时自己有高效的锁机制的应用,还是建议直接用softirq。 你要在linux/interrupt.h中通过数组声明softirq,内核使用这个索引(从0开始)作为相对的优先级。最低索引的softirq最早执行。创建新的softirq需要在枚举中创建一个新的入口索引。当增加一个softirq,你不能仅仅简单的在列表末尾增加你的入口。相反,你需要依据你希望赋予它的优先级来插入到枚举的某个位置。传统来说,HI_SOFTIRQ总是第一个, RCU_SOFTIRQ总是最后一个。新的入口一般都是介于BLOCK_SOFTIRQ与TASKLET_SOFTIRQ之间。接着就是通过open_softirq来注册softirq函数。这个注册函数带两个参数,一个是softirq入口索引,一个是对应的处理函数。sotfirq函数是在中断激活的情况下运行的,而且不能休眠。当一个softirq函数在运行的时候,当前的处理器上其他softirq被禁止。但别的处理器,却可以运行其他的softirq。同类型的softirq是可以同时分别在不同的处理器上运行的。这意味着,任何共享内存,甚至是全局变量在softirq函数内部要采用适当的锁机制。这是很重要的一点,这也是为什么tasklet比较受欢迎的原因。仅仅避免softirq并行执行不是一个理想的办法,如果softirq通过锁的方式来阻止其别的实例同时运行,这样的话,就没必要运行softirq了。结果,大部分的softirq函数都会采用处理器私有的数据和其他技巧来避免直接的上锁,提供了优秀的扩展性。如果你不需要扩展到无限处理器上,那就选用tasklet吧。tasklets本质上是那种不能在多个处理器上同时运行处理函数的softirq。

在增加了入口索引以及通过open_softirq注册后,就可以准备运行了。调用raise_softirq来标志其待运行,接着就会在下一个do_softirq的调用中执行了。这个函数调用前会禁止中断,在执行完这个函数后会恢复中断原有状态。如果中断本身是被禁止的,则可以调用raise_softirq_irqoff来获得小小的优化。softirq大部分都是在中断函数中被引发的。在中断函数调用的情况下,中断函数会执行跟硬件相关的操作,接着引起softirq,接着就退出。当处理中断的时候,内核会调用do_softirq,此时softirq函数就会处理中断函数剩余要做的工作了。


Tasklets 任务片

tasklets是一种建立在softirq之上的一种下半部分解决机制。这个跟task没有关系。tasklets在本质上以及行为上是类似softirqs的,然而有更简单的接口以及松散的锁规则。作为驱动开发者,决定用softirq还是用tasklets是简单的事情:你基本上总是使用tasklets。softirq仅仅需要用在高频率高线程化的使用场景,tasklets相反适用于大部分场景,而且很容易使用。

因为Tasklets是建立在softirqs之上的,故此其也算是一种softirq。如前所讨论的,tasklets是有两种softirq代表的:HI_SOFTIRQ和TASKLET_SOFTIRQ。这两者的区别仅仅在于HI_SOFTIRQ运行的优先级高于TASKLET_SOFTIRQ。Tasklets是由结构 tasklet_struct 代表的,存放与 linux/interrupt.h中。

struct tasklet_struct

{

struct tasklet_struct *next;

unsigned long state;

atomic_t count;

void (*func)(unsigned long);

unsigned long data;

};


func成员是tasklet的处理函数,data是作为funce的唯一个参数。state只有 0, TASKLET_STATE_SCHED 或 TASKLET_STATE_RUN三种值。TASKLET_STATE_SCHED表明tasklet被调度去运行,TASKLET_STATE_RUN 表明tasklet正在运行。一般来说,这个状态多用在多处理器的情况,因为单处理器的机器总是知道是否有tasklet在运行。count 作为tasklet的一个引用计数器。如果此值非零,则tasklet被关闭且不能运行,如果它为0,则tasklet是被打开且可以运行的(如果标志待运行的话)

被调度的tasklet(跟引发softirq是同一个概念)是由两个处理器私有的结构保存的, tasklet_vec (一般的tasklet) 和 tasklet_hi_vec(高优先级的tasklet)。两种结构都是tasklet_struct类型的链表。链表中的每一个项都是不同的tasklet。tasklet是通过tasklet_shcedule以及tasklet_hi_schedule来调度的,都接收一个tasklet_struct结构体作为参数。每一个函数都保证,提供的tasklet_struct还没被调度,然后再调用对应的_tasklet_schedule或__tasklet_hi_Schedule. 这两个函数都是差不多的,唯一的区别就是一个用TASKLET_SOFTIRQ,一个用HI_SOFTIRQ。task_schedule的步骤如下:

1. 检测tasklet的状态是否为TASKLET_STATE_SCHED,如果是表明此tasklet已经被调度就要运行,故此函数退出

2. 调用 __tasklet_schedule

3. 保存当前中断系统的状态,然后禁止本地处理器中断,确保tasklet_Schedule在处理Tasklets时候不会被打扰。

4. 把要被调度tasklet放到tasklet_vec的头部,这个tasklet_vec是每一个处理器唯一的。

5. 引发TASKLET_SOFTIRQ或HI_SOFTIRQ的softirq,调用do_softirq来尽快执行Tasklet

6. 恢复中断系统的状态,以及返回。

TASKLET_SOFTIRQ或HI_SOFTIRQ对应的tasklet_action或tasklet_hi_action的处理函数会被调用,这些函数是tasklet处理的核心部分,如下过程:

1. 禁止本地中断,接收本地进程的tasklet_vec或tasklet_hi_vec。(因为这是在softirq处理函数中,所有的中断总是激活的,所以没有必要保存中断状态)

2. 清除本地处理器的tasklet_vec或tasklet_hi_vec的列表

3. 激活本地中断, 这里再一次不需要保存中断系统状态,因为这里清楚知道它们都是原有就激活的。

4. 在列表中遍历所有pending状态的tasklet

5. 如果是多处理器系统,则要检查tasklet的状态是否为TASKLET_STATE_RUN。如果它是运行的,则略过此tasklet,取下一个tasklet(一种类型的tasklet只能运行一个)

6. 如果tasklet没有正在运行,则设置TASKLET_STATE_RUN标志,保证别的处理器不会运行它

7. 检查count的值似乎否为0 ,确保tasklet没有被关闭(disabled)。如果是被关闭了,则略过取下一个tasklet

8. 目前我们已清楚tasklet没有在别的处理器上运行,目前也被标志为运行状态,同时也被没有被关闭,故此我们可以去执行tasklet的处理函数了。

9. 在tasklet运行后,清楚TASKLET_STATE_RUN标志。

10. 重复取下一个tasklet,知道没有更多的可被调度的tasklet等待运行。

tasklet的实现是很简单,也很聪明的。可以看出,所有tasklet是复用softirq的,一个是HI_SOFTIRQ,一个是TASKLET_SOFTIRQ(因为tasklet是建立在softirq之上的,是运行在softirq的处理函数中的)。当tasklet被调度了,内核会引发一个softirq,softirq接着会被处理,接着运行对应的处理函数,而这个函数中就会去调度tasklets。这个函数保证了当前只有一个数量的一个类型的tasklet在跑。(但别的类型的tasklet可以在别的处理器运行)。所有这些复杂的动作,都隐藏在简单的接口之后。

大部分情况下,tasklet都是实现你普通设备的bottom half首选的机制。tasklet是动态创建的,容易使用,运行快速。你可以动态或静态的创建tasklet,这选择依赖于你是否拥有(或想有)直接的或间接的tasklet实例。如果要静态创建的话,则使用如下宏( linux/interrupt.h)

DECLARE_TASKLE(name, func, data)

DECLARE_TASKLET_DISABLED(name, func, data)

两个宏都是静态创建指定名字的tasklet_struct。当tasklet被调度,给定的func函数会被执行,并传递data作为参数。这两个宏的区别是初始tasklet_struct的count值。第一个宏设置count为0,确保tasklet是激活的。另外一个是设置为1,确保tasklet是关闭的。动态创建的tasklet_Struct可以调用tasklet_init函数来初始化,参数是结构体tasklet_struct以及处理函数和对应的数据。tasklet处理函数的原型为:

void tasklet_handler(unsigned ling data);

如Softirq一样,tasklet不能休眠。这意味着你不能在处理函数里使用信号量或别的可以被阻塞的函数。tasklet也是在所有中断都激活的状态下运行的,所以在与别的中断函数共享数据的时候,你要非常警惕。如果tasklet与别的类型的tasklet或softirq共享数据的时候,你需要适当的锁。当你要调度你的tasklet,执行tasklet_schedule(&my_tasklet)便可以了。当你执行这个之后,你的tasklet会在近期被执行一次。在你的tasklet未运行时候,如果再一次执行调度,你的tasklet同样也只会执行一次而不是两次。如果本身同类型的tasklet在别的处理器运行,则tasklet会被重新调度,再运行。你可以取消一个tasklet的执行,通过tasklet_disable。如果被取消的tasklet在运行中,则此函数不会返回,直到tasklet运行完毕。或者,可以调用tasklet_disable_nosync()来异步处理,这样就算tasklet在运行,你也不需要等待了。但这个是不安全的,因为你不知道tasklet运行结束与否。你也可以调用tasklet_enable来重新激活Tasklet。你可以tasklet_remove来移除正在pending中的tasklet。当处理一个总是自己调度自己的tasklet时候,从被调度中的tasklet移除tasklet是很有用的。这个函数必须不能在中断上下文中被调用,因为它会休眠。它会等到当前要被移除的tasklet运行完毕才会返回(如果要移除的tasklet正在运行)。

softirq处理过程都有一套进程私有的内核线程辅助。这些内核线程会帮助处理softirq,当系统不堪负重大量softirq的时候。因为tasklet是通过softirq来实现的,以下的讨论均适用于tasklet,故此会以softirq为主要讨论对象。如前讨论的,内核在多种地方来处理softirq,特别的是在中断函数返回后。softirq也许会被高频率的被引发,或更进一步,softirq函数可以重新激活自身(死循环?记得前面是判断pending值)。那就是说,当softirq运行时,它可以引发自己,因此它可以再运行一遍。高频发生的softirq加上他们能自我重标志为激活状态的能力,可以导致用户态的应用得不到处理器的眷恋。在某些时刻,如果不执行被重新激活的softirq也是不恰当的。因此softirq在设计之初就面临了两难境地。当初是在衡量考虑牺牲用户态运行的时间或牺牲softirq处理的高效性两方面选择。内核设计者,为了这个而进行一个妥协了。最终的解决办法是并不立即执行重新激活的softirq,相反,当softirq的数量增大过多的时候,内核会唤醒一套内核线程来辅助处理。内核线程是运行在最低优先级上的(nice值为19),确保他们不会抢着重要的事情处理。这个做法,可以避免繁重的softirq活动让用户态得不到处理器时间。相反,它还保证了过量的softirq最终得到处理。最后,这个方法还保证在空闲的处理器时候,softirq会等到相应快速的处理,因为此时内核线程会被立即调度。每一个处理器只有一个内核线程,线程名字为ksoftirqd/n n代表处理器数字。在线程初始化之后,会如下运行:

for(; ;)

{

if(!softirq_pending(cpu))

schedule();

set_current_state(TASK_RUNNING);

while(softirq_pending(cpu))

{

do_softirq();

if(need_resched())

{

schedule();

}

}

set_current_state(TASK_INTERRUPTIBLE);

}

如果有任何softirq处于pending,ksoftirqd会调用do_softirq来处理。注意到,这里通过循环来处理所有重激活的softirq。在每一次循环,都会调用schedule来确保其他更重要的事情可以进行。在所有处理都完成后,内核线程会自己设为TASK_INTERRUPTIBLE状态,并调用schedule来让出处理器时间。


工作队列

工作队列是另外一种推迟中断大部分工作的不同形式。工作队列把工作推迟到内核线程那里,因此这个bottom half总是运行在进程上下文。因此拥有进程上下文的代码所拥有的好处。更重要的是,工作队列都是可调度的,以及可休眠的。一般来说,很容易决定使用工作队列还是softirq/tasklet。如果需要休眠的,则用工作队列,不需要用休眠的则用softirq/tasklet。真正的,被工作队列所代替的是内核线程。因为内核开发者反对创建新的内核线程,他们更喜欢用工作队列。他们真的很容易使用。如果你需要一个可调度的实体来执行你的bottom half,你需要用工作队列。他们是运行在进程上下文的bottom half,因此可以休眠。这意味着他们适用于需要申请大量内存的,用到信号量或执行I/O锁的情况。如果你不需要用到内核线程来处理你的延迟工作,可以简单的选择tasklet。

工作队列子系统最基本的形式是一种创建内核线程来异处处理工作队列的接口。这些内核线程称为工作线程。工作队列让你的驱动创建特殊的工作线程来处理你的延迟工作。工作队列子系统,实现并提供了一个默认的工作线程来处理供走,因此,在它最基本通用形式内,工作队列就是一个简单的把工作推迟到一般内核线程的接口。

默认的工作线程叫做events/n,n代表所处的处理器序号。每一个处理器只有一个。这个默认工作线程从多个地方来处理延迟的工作。多数的内核驱动会把延迟工作交给默认工作线程。除非驱动或子系统强烈有建立新内核线程的需求,否则更青睐于默认工作线程。但是,也并不会阻止创建自己的工作线程。这会是一个优势,就是你要在工作线程执行大量的处理。紧迫的工作在自己的工作线程处理是有好处的。同时也会减负默认工作线程的负担,防止剩余的工作队列得不到处理器资源。

工作队列是有结构 workqueue_struct代表的

struct workqueue_struct

{

struct cpu_workqueue_struct cpu_wq[NR_CPUS];

struct list_head list;

const char *name;

int singlethread;

int freezeable;

int rt;

};

这个结构是定义在 kernel/workqueue.c那里,包含了struct 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关联的。所有的工作队列都通过运行worker_thread函数的普通内核线程来实现的。初始化配置后,这个函数就进入无线循环,并进入休眠。当工作被列进队列后,线程被唤醒并处理工作。当没有工作后,就再次进入休眠。工作是由 struct work_struct结构体表示的。

struct work_struct

{

atomic_lont_t data;

struct list_head entry;

work_func_t func;

};

这些结构都捆绑到一个链表上的,每一个都放每一个处理器上的一种类型的队列中去。当工作线程被唤醒后,会执行列表上的工作,在执行完毕后,会在队列中移除改工作。如果队列为空,则休眠。

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);

}

解释如下:

1. 线程先标志自己下一步休眠,并把自己加入到等待队列。

2. 如果链表空,则执行调度进入休眠。

3. 如果非空,则线程不休眠,相反又把自己标志为TASK_RUNNING同时把自己从等待队列中移除。

4. 如果队列非空,线程调用run_workqueue函数执行延迟工作。

在函数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);

}

这个函数循环遍历每一个pending工作的链表的入口,并执行其成员func:

1. 当列表为非空,获取下一个链表入口。

2. 取得要执行的函数func以及参数data

3. 从链表中移除该入口,并清除pending的标志位。

4. 调用该函数

5. 重复。

上述几个结构,形成稍微复杂,用图示表明其关系:


最上层,是工作线程。可以有多个工作线程。但每一个处理器只有一种类型的工作线程。部分的内核有需要也可以创建工作线程。默认有一个events工作线程。每一个工作线程由cpu_workqueue_struct结构代表。workqueue_struct则代表一种类型的所有工作线程。举个例子,除了一般的events类型工作类型,还有falcon工作线程类型。也假设你工作在4处理器机器上。那么就有4个events线程以及四个falcon线程。因此有一个workqueue_struct包含所有events类型的结构以及一个包含所有falcon类型的结构。

现在看看最底层,驱动创建工作(想要推迟的工作)并存放到work_struct里面去。这个结构有一指针指向一个处理这个推迟工作的函数。这个工作被传递到一个特定的工作线程。工作线程则被唤醒,然后执行排好队的工作。大部分的驱动都用默认的工作线程(events)一些相对特殊的情况的,会用自己的工作线程。

先来看看默认工作线程events的使用方式。

首先要创建推迟的工作。可以静态的创建

DECLARE_WORK(name, void(*func)(void *), void *data);

也可以动态创建,在用宏初始化

INIT_WORK(struct work_struct *work, void (*func)(void *), void *data);

工作队列的处理函数的原型: void work_handler(void *data);

一个工作线程执行这个函数,因此这个函数是运行在进程上下文的。默认下,中断是激活的,而且没有锁。如果需要,函数可以休眠。尽管是工作在进程上下文,但工作函数不能访问用户空间的内存,因为在内核线程里,没有与用户空间内存存在映射关系。内核仅仅只能在其代表用户进程运行的时候才能访问用户空间的内存,例如执行系统调用时候。在工作队列中或内核的部分中上锁,就如别的进程上下文胆码上锁一样。因此写工作函数会更简单。

工作建立后,我们就可以调度它了。要把工作排进属于events工作线程的工作队列里,可以简单的呼叫

schedule_work(&work);

这个工作会立即被调度,当events的工作线程一旦唤醒,会立即执行。有时候,你并不希望立即执行,或许要推迟一点。这情况下,可以呼叫

schedule_delayed_work(&work, delay)

delay是要延迟的时间,以ticker为单位。

被排队的工作只有在工作线程被唤醒才会接着工作。有时候,你需要确保一堆的工作必须在继续下一步的之前完成。这对于模块很重要,因为模块通常希望在被卸载的时候处理完其工作。内核的其他地方,也许不希望有正在等待的工作,来阻止竞争。这些需求,都可以通过flush(释放?)一个工作队列来完成。

void flush_scheduled_work(void);

这个函数会等待到所有队列的工作执行完毕后才返回。当等待任何工作执行时,其会休眠。因此你仅仅只能从进程上下文中来调用。注意的是,这个函数并不是要取消工作。但通过shcedule_delayed_work调用的work,是不会被flush的,因此要用 cancel_delayed_work函数来取消这个工作。

如果默认的工作队列不足以完成你的需要,你可以创建新的工作队列以及对应的工作线程。因为一个工作线程只跑在一个处理器,你应该仅仅在你必须一套线程的效率来完成你的工作时候,才去创建唯一的工作队列。你可以通过以下函数来创建一个工作队列以及对应的工作线程。(也就是说,如果仅仅需要单一个线程来完成工作,就没必要创建新的工作队列了,用默认的即可)

struct workqueue_struct *create_workqueue(const char *name);

name是用来命名这个内核线程的,举个例子

struct workqueue_struct *keventd_wq;

keventd_wq = create_workqueue("events");

这个函数创建所有工作线程(一个处理器一个),并准备工作。创建工作尽管队列类型不一样,但方式是一样的。创建完工作后,如默认队列的操作调用schedule_work或schedule_delayed_work一样,调用如下

queue_work(struct workqueue_struct *wq, struct work_struct *work) 或

queue_delayed_work(struct workqueue_struct *wq, struct work_struct *work, unsigned long delay)

比默认的多了一个工作队列的指定。 最后flush一个队列,可以调用 flush_workqueue(struct workqueue_struct *wp)。


决定用哪种bottom half机制是很重要的。目前有三种,softirq, tasklets和work queues。因为tasklets是建立在softirqs之上的,因此是相似的。而工作队列是完全不同的机制,因为它是建立在内核线程中的。softirq设计上是最低程度串行化的。因为同类型的softirqs可以他哦你是并发与不同的处理器,这要求softirq函数必须通过额外的步骤来保证共享内存的安全。如果代码已经是高度线程化了,softirq是好的选择。对于时间关键的,高频使用的,softirq是最快的选择。

如果代码是相对于线程化的,tasklet是更有意义的。它接口简单,同时因为同类型的tasklet不能并行,因此它很容易实现。tasklet是一种不可以并发(同类型)的softirq。驱动开发者应该多选择tasklet,除非要采用处理器私有的变量或有类似的办法来保证softirq可以安全的并发运行在多个处理器上。

如果你希望把推迟的工作放到进程上下文里,则你只能选用工作队列的机制。如果不需要用到进程上下文,也不需要休眠,则softirq或tasklet是首选。工作队列因为调用了内核线程,因此会有上下文切换,故此会引起高负荷。但并不是等于说它就是低效的,但鉴于中断在每秒触发上千次来说,其他方法会更适合。但在多数情况下,工作队列是满足要求的。

softirq和tasklet都是工作在中断上下文的(软中断上下文),而工作队列是工作在进程上下文的。简单的说,一般驱动工程师有两个选择:第一,你是否需要调度来执行你的延迟工作(基本考虑),你是否需要代码休眠?如果是,则工作队列是你的唯一选择。其他的,tasklet是首选,仅仅当扩展性成为你的关注重点,才去考虑softirq。

因为softirq没有很好的串行化,故此其共享数据的安全性要适当的保护。一般是上锁加关闭bottom half。关闭bottom half采用

local_bh_disable函数,可以采用local_bh_enable来重新激活。这两个函数可以嵌套使用。在嵌套使用时,只有最后一次对应的local_bh_enable调用,才可以真正的激活bottom half(带计数器的)。这个函数是通过维护一个task私有的计数器(preempt_count)来实现的。当计数器为0,bottom half处理可以进行。local_bh_enable同时也会检查pending的bottom half,并在激活后,执行它们。

void local_bh_disable(void)

{

struct thread_info *t = current_thread_info();

t->preempt_count += SOFTIRQ_OFFSET;

}

void local_bh_disable(void)

{

struct thread_info *t = current_thread_info();

t->preempt_count += SOFTIRQ_OFFSET;


if(unlikely(!t->preempt_count && softirq_pending(smp_processor_id()))

do_softirq();

}