【读书笔记】Linux内核设计与实现--下半部和推后执行的工作

1.下半部

下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作

ps:在理想情况下,最好的是中断处理程序将所有工作都交给下半部分执行,因为中断处理程序中完成的工作越少越好(越快越好)。

对于上半部和下半部之间划分工作,尽管不存在某种严格的规则,但还是有一些提示可供借鉴:

  1. 如果一个任务对事件非常敏感,将其放在中断处理程序中执行;(上半部)
  2. 如果一个任务和硬件相关,将其放在中断处理程序中执行;(上半部)
  3. 如果一个任务要保证不被其他中断(特别是相同的中断)打断,将其放在中断处理程序中执行;(上半部)
  4. 其他所有任务,考虑放在下半部执行。

1.1 为什么要用下半部

Q:为什么要用下半部?
A:在中断上下文运行的时候,当前的中断线在所有处理器上都会被屏蔽。更糟糕的是,如果一个处理程序是IRQF_DISABLED类型,它执行的时候会禁止所有本地中断(而且把本地中断线全局地屏蔽掉)。而缩短中断被屏蔽的时间对系统的响应能力和性能都至关重要。再加上中断处理程序要与其他程序(甚至是其他的中断处理程序)异步执行。必须尽量减少中断处理的执行,解决方法就是把一些工作放到”以后“去做。(这个以后仅仅说明不是马上

通常下半部在中断处理程序一返回就会马上运行。下半部执行的关键在于当它们运行的时候,允许响应所有的中断。

1.2 下半部的环境

和上半部只能通过中断处理程序实现不同,下半部可以通过多种机制实现
这些用来实现下半部的机制分别由不同的接口和子系统组成。

被弃用的一些机制:BH,任务队列(Tsk queues)。

ps:不要错误的把所有的下半部机制都叫做”软中断“。

当前,由三种机制可以用来实现将工作推后执行:软中断、tasklet和工作队列
下表揭示了下半部机制的演化例程:
在这里插入图片描述

扫描二维码关注公众号,回复: 11077267 查看本文章

2.软中断

实际的下半部实现–软中断方法开始。软中断使用得比较少;而tasklet是下半部更常用的一种形式。(tasklet是通过软中断实现的),软中断代码位于kernel/softirq.c文件中。

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个软中断。
ps:NR_SOFTIRQS是一个定值–注册的软中断数目的最大值无法动态改变

软中断处理程序:
软中断处理程序action的函数原型如下:

void softirq_handler(struct softirq_action *);

当内核运行一个软中断处理程序的时候,它就会执行这个action函数,其唯一参数为指向相应softirq_action结构体的指针。如:如果my_softirq指向softirq_vec数组的某项,那么内核会用如下的方式调用软中断处理程序中的函数:

my_softirq->action(my_softirq);

notice:这里内核把整个结构体都传递给软中断处理程序而不是仅仅传递数据值。这是因为可以保证将来在结构体加入新的域时,无须对所有软中断处理程序都进行变动。或者如有需要,软中断处理程序可以方便地解析它的参数,从数据成员中提取整数。

ps:一个软中断不会抢占另外一个软中断。实际上,唯一可以抢占软中断的是中断处理程序。不过,其他的软中断(甚至是相同类型的软中断)可以在其它处理器上同时执行。

执行软中断:
触发软中断(raising the softirq):一个注册的软中断必须在标记后才会执行
通常,中断处理程序在返回前标记它的软中断,使其在稍后被执行。
在下列地方,待处理的软中断会被检查和执行:

  1. 从一个硬件中断代码处返回时;
  2. 在ksoftirqd内核线程中;
  3. 在那些显式检查和执行待处理的软中断的代码中,如网络子系统中。

在软中断被唤醒后,软中断都要在do_softirq()中执行。
关于do_softirq函数的解析可阅读源码或者参考该书8.2.1章节。

2.2 使用软中断

软中断保留给系统中对时间要求最严格以及最重要的下半部使用。目前,只有两个子系统(网络和SCSI)直接使用软中断。

ps:在想实现一个软中断之前,先想想用tasklet能不能满足要求。另外,对于时间要求严格并能自己高效的完成加锁工作的应用,软中断会是正确的选择。

Q:如何使用软中断?
A:
1.分配索引
编译期间,通过在<linux/interrupt.h>中定义的一个枚举类型来静态声明软中断
建立一个新的软中断必须在此枚举类型中加入新的项
在这里插入图片描述
索引号小的软中断在索引号大的软中断之前执行。
加入新项的时候必须根据希望赋予它的优先级来决定加入的位置。

ps:习惯上HI_SOFTIRQ通常作为第一项,而RCU_SOFTIRQ作为最后一项。
新项可能插在BLOCK_SOFTIRQ和TASKLET_SOFTIRQ之间。

2.注册处理程序
在运行时通过open_softirq()注册软中断处理程序,该函数接受两个参数:软中断的索引号和处理函数。
eg: 网络子系统 net/coreldev.c通过以下方式注册自己的软中断:

open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);

notice:
软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。
在一个处理程序运行的时候,当前处理器上的软中断被禁止。
如果同一个软中断在它被执行的同时被再次触发了,那么另一个处理器可以同时运行其处理程序。(可重入),这里就要考虑到并发和资源竞争的问题,而同一个处理程序的多个实例能在多个处理器上同时运行又是软中断的特色,如果单纯靠锁避免并发竞争,那么这二者就冲突了。

ps:
大部分的软中断处理程序,都是通过采取单处理器数据(仅属于一个处理器的数据,因此根本不需要加锁)或其他一些技巧来避免显示加锁,从而提供更出色的性能。
引入软中断的主要原因是其可扩展性。如果不需要扩展到多个处理器,那么,可使用tasklet.
tasklet本质上也是软中断,只不过同一个处理程序的多个实例不能再多个处理器上同时运行。

3.触发软中断–raise_sofrirq()
raise_sofrirq()函数可以将一个软中断设置为挂起状态,让它在下次调用do_softirq()函数时投入运行

ps:
raise_sofrirq()函数在触发一个软中断之前先要禁止中断,触发后再恢复原来的状态。如果中断本来就已经禁止了,可以调用另一函数raise_softirq_irqoff()会带来一些优化效果。

在中断处理程序中触发软中断是最常见的形式
通常,中断处理程序执行硬件设备的相关操作,然后触发相应的软中断,最后退出。内核在执行完中断处理程序后,马上就会调用do_softirq()函数。这样软中断开始执行中断处理程序留给它去完成的剩余任务。(上半部和下半部的含义就此显现)

3.tasklet

tasklet是利用软中断实现的一种下半部机制。它的接口更简单,锁保护也要求较低。
Q:选择tasklet还是软中断?
A:通常应该选用tasklet,软中断的使用者屈指可数,它只在那些执行频率很高和连续性要求很高的情况才需要使用

3.1 tasklet的实现–本身也是软中断

tasklet有两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ。
这两者之间唯一的实际区别在于,HI_SOFTIRQ类型的软中断先于TASKLET_SOFTIRQ类型的软中断执行。

1.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处理函数的参数 */
};

state成员只能在0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值。
TASKLET_STATE_SCHED表明tasklet已经被调度,正准备投入运行;
TASKLET_STATE_RUN表明该tasklet正在运行。

count成员是tasklet的引用计数器。如果为0,则tasklet被禁止,不允许执行;只有为0时,tasklet才被激活,并且在被设置为挂起状态时,该tasklet才能够执行。

ps:TASKLET_STATE_RUN只有在多处理器的系统上才会作为一种优化来使用,单处理器系统任何时候都清楚单个tasklet是不是正在运行(要么正在运行,要么不是)。

2.调度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软中断,下次调用do_softirq时就会执行该tasklet;
  6. 恢复中断到原状态返回。

do_softirq会尽可能地在下一个何时的时机执行,由于大部分tasklet和软中断都是在中断处理程序中被设置成待处理状态,所以最近一个中断返回的时候看起来就是执行do_softirq的最佳时机。

因为TASKLET_SOFTIRQ和HI_SOFTIRQ已经被触发了,所以do_softirq会执行相应的软中断处理程序。也就是tasklet处理的核心–tasklet_action()和tasklet_hi_action()

tasklet处理的核心执行流程如下

  1. 禁止中断(没有必要首先保存其状态,这里的代码总是作为软中断被调用,而且中断总是被激活的),并未当前处理器检索tasklet_vec或tasklet_hi_vec链表;
  2. 将当前处理器上的该链表设置为NULL,达到清空的效果;
  3. 允许响应中断。没有必要再恢复它们回原状态,因为这段程序本身就是作为软中断处理程序被调用的,所以中断是应该被允许的;
  4. 循环遍历获得链表上的每一个待处理的tasklet;
  5. 如果是多处理器系统,通过检查TASKLET_STATE_RUN来判断这个tasklet是否正在其他处理器上运行。如果它正在允许,那么现在就不要执行,跳到下一个待处理的tasklet去–同一时间里,相同类型的tasklet只能由一个执行;
  6. 如果当前这个tasklet没有执行,将其状态设置为TASKLET_STATE_RUN,这样别的处理器就不会再去执行它了;
  7. 检查count值是否为0,确保tasklet没有被禁止。如果tasklet被禁止了,则跳到下一个挂起的tasklet去;
  8. 目前已经清楚的知道这个tasklet没有再其他地方执行,并且被设置成执行状态,这样它再其他部分就不会被执行,而且引用计数为0,现在可以执行tasklet的处理程序了;
  9. 重复执行下一个tasklet,直至没有剩余的等待处理的tasklet。

ps:tasklet接口的使用保证了同一时间里只有一个给定类型的tasklet会被执行(但其他不同类型的tasklet可以同时执行) 。

3.2 使用tasklet

Q:如何使用tasklet?
A:
1.声明tasklet
tasklet的声明支持静态或动态,创建方式取决于需要一个对tasklet的直接引用(静态)还是简介引用(动态)。

方式 说明
静态 DECLARE_TASKLET(name, func, data);
DECLARE_TASK_DISABLED(name, func, data);
这两个宏都能根据给定的名称
静态的创建一个tasklet_struct结构.
当该tasklet被调度后,
给定的函数func回被执行,
它的参数由data给出
它们的区别在于
引用计数的初始值不同。
一个是0,一个是1;
则前者是激活,后者是禁止。
动态 tasklet_int(t, tasklet_handler, dev); 将一个间接引用(一个指针)
赋给一个动态创建的tasklet_struct结构
的方式来初始化一个tasklet_init()

2.编写tasklet处理程序
tasklet处理程序必须符号规定的函数类型(回调):

void tasklet_handler(unsigned long data);

因为靠软中断实现,所以tasklet不能睡眠。也就是说不能在tasklet中使用信号量或者其他什么阻塞式的函数

ps:由于tasklet运行时允许响应中断,必须做好资源的保护工作。

3.调度tasklet
调用tasklet_schedule()函数并传递给它相应的tasklet_struct的指针,该tasklet就会被调度以便执行:

tasklet_schedule(&my_tasklet); /* 把 my_tasklet 标记为挂起 */

在tasklet被调度以后,只要有机会它就会尽可能早地运行。
在它还没有得到运行机会之前,如果有一个相同的tasklet又被调度了,那么它仍然只会运行依次。
如果这时它已经开始运行了,比如说在另一个处理器上,那么这个新的tasklet会被重新调度并再次运行。(疑问:这样的话,岂不是也要做锁保护?) 作为一种优化措施,一个tasklet总在调度它的处理器上执行–希望更好的利用处理器的高速缓存。

tasklet相关操作函数如下表所示:

函数 说明
tasklet_disable() 禁止某个指定的tasklet(阻塞式) ,会等tasklet执行完毕在返回
tasklet_disable_nosync() 禁止某个指定的tasklet(非阻塞式)
tasklet_enable() 激活一个tasklet(包括静态创建的tasklet)
tasklet_kill() 从挂起的队列中去掉一个tasklet,该函数的参数是一个指向某个tasklet_struct的长指针

ps:tasklet_kill函数在处理一个经常重新调度它自身的tasklet的时候,从挂起的队列中移去已调度的tasklet会很有用。这个函数首先等待该tasklet执行完毕,然后再将它移去。
注意:没有什么可以阻止其他地方的代码重新调度该tasklet。由于该函数可能会引起休眠,所以禁止再中断上下文中使用它。

4.ksoftirqd辅助处理软中断(包括tasklet)的内核线程
每个处理器都有一组辅助处理软中断(和tasklet)的内核线程。
内核线程中出现大量软中断的时候,这些内核线程就会辅助处理它们。

Q:为什么需要ksoftirqd这个线程?
A:对于软中断,内核会选择在几个特殊时机进行处理。而在中断处理程序返回时处理是最常见的。
软中断被触发的频率有时可能很高(像在进行大流量的网络通信期间–这样就会中断很频繁,根据前面的分析,会导致软中断触发的频率很高)。

更不利的是,处理函数有时还会自行重复触发。也就是说,当一个软中断执行的时候,它可以重新触发自己以便再次得到执行。如果软中断本身出现的频率就高,再加上它们又有将自己重新设置为可执行状态的能力,那么就会导致用户空间进程无法获得足够的处理器实际,因而处于饥饿状态

因此提出以下两种最容易直观的方案(具体方案可参考本书8.3.2章节),因为这两种方案要不是会让用户空间的进程处于饥饿状态就是会让软中断处理程序处于饥饿状态。得不到一个好的利用处理器的处理方案。引出了一个这种并且同时兼顾不让用户空间进程饥饿和软中断处理程序饥饿的处理方式–引入ksoftirqd线程

大量软中断出现的时候,内核会唤醒一组内核线程(ksoftirqd)来处理这些负载
这些线程在最低的优先级上运行(nice值是19),这能避免它们跟其他重要的任务抢夺资源。但它们最终肯定会被执行,所以,这个折中的方案能够保证在软中断负担很重的时候,用户程序不会因为得不到处理时间而处于饥饿状态,也能保证”过量“的软中断终究会得到处理。

ps:在空闲系统上(负载很低的情况),这个方案也同样表现良好,软中断处理得非常迅速,因为i仅存的内核线程肯定会马上调度。

notice:
每个处理器都有一个这样的线程
所有线程的名字都叫做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()函数负责发现),ksoftirq就会调用do_softirq()去处理它们。通过重复执行这样的操作,重新触发的软中断也会被执行。如果有必要,每次迭代后都会调用schedule()以便让更重要的进程得到处理机会。当所有需要执行的操作都完成以后,该内核线程将自己设置为TASK_INTERRUPTIBLE状态,唤起调度程序选择其他可执行进程投入运行

只要do_softirq()函数发现已经执行过的内核线程重新触发了它自己,软中断内核线程就会被唤醒。

3.3 老的BH机制

因为是丢弃了的机制,不多学习,详细可参考本书8.3.3章节。这里简单提下丢弃的原因,随着多处理器的发展,老的BH机制不利于多处理器的可扩展性,也不利于大型SMP的性能。使用BH的驱动程序很难从多个处理器受益,特别是网络层,可以说是饱受困扰。

4.工作队列

工作队列(work queue)是另外一种将工作推后执行的形式。
工作队列可以把工作推后,交由一个内核线程去执行--这个下半部总是会在进程上下文中执行
通过工作队列执行的代码能占尽进程上下文的所有优势,特别是工作队列允许重新调度甚至是睡眠
工作队列通常可以用内核线程替换,但内核开发者们非常反对创建新的内核线程(因为在有些场合,可能会吃到苦头)。

当需要用一个可以重新调度的实体来执行你的下半部处理,应该使用工作队列。

工作队列是唯一能在进程上下文中运行的下半部实现机制,下半部实现机制也只有它才可以睡眠。

在需要获得大量内存时,在需要获取信号量的时候,在需要执行阻塞式的I/O操作时,它都会非常有用。

ps:如果不需要用一个内核线程来推后执行工作,考虑tasklet。

4.1 工作队列的实现

工作队列子系统是一个用于创建内核线程(工作者线程(woker thread))的接口,通过它创建的进程负责执行由内核其他部分排到队列里的任务。

ps:工作队列子系统可以提供了两种工作者线程:驱动创建专门的工作者线程由工作队列子系统提供的一个缺省的工作者线程

由上述可知工作队列最基本的表现形式,就转变成了一个把需要推后执行的任务交给特定的通用线程的这样一种接口。

缺省的工作者线程叫做events/n,同前面见过的ksoftirqd的n一个意思,标识处理器的编号。
缺省的工作者线程会从多个地方得到被推后的工作

ps:
许多内核驱动程序都把它们的下半部交给缺省的工作者线程去做。除非一个驱动程序或者子系统必须建立属于它自己的内核线程,否则最好使用缺省线程。
处理器密集型和性能要求严格的任务会因为拥有自己的工作者线程而获得好处。此时这么做也有助于减轻缺省线程的负担,避免工作队列中其他需要完成的工作处于饥饿状态。

工作者线程用workqueue_struct结构标识:

/*
*	外部可见的工作队列抽象是
*	由每个CPU的工作队列组成的数组
*/
struct workqueue_struct {
	struct cpu_workqueue_struct cpu_wq[NR_CPUS];	/* 数组中的每一项对应系统中的一个处理器 定义在kernel/workqueue.c中 */
	struct list_head list;	
	const char *name;
	int singlethread;
	int freezeable;
	int re;
};

由于系统中每个处理器对应一个工作者线程,所以对于给定的计算机,每个处理器,每个工作者线程都对应这样一个cpu_workqueue_struct结构体。
cpu_workqueue_struct是kernle/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;		/* 关联线程 */
};

表示工作的数据结构:
工作用<linux/workqueue.h>中定义的work_struct结构体表示:

struct work_struct {
	atomic_long_t data;
	struct list_head entry;
	work_func_t func;
};

所有的工作者线程都是用普通的内核线程实现的,它们都要执行worker_thread()函数。
在它初始化完成后,这个函数执行一个死循环并开始休眠
当有操作被插入到队列里的时候,线程就会被唤醒,以便执行这些操作;
当没有剩余的操作时,又继续休眠。

ps:work_thread函数的工作分析可参考本书8.4.1章节。

3.工作队列实现机制的总结
在这里插入图片描述
每个工作者线程都由一个cpu_workqueue_struct结构体表示。
workqueue_struct结构体则表示给定类型的所有工作者线程。
工作用work_struct结构表示,该结构体中最重要的是一个指针,指向一个函数,该函数负责处理需要推后指向的具体任务

工作被提交给某个具体的工作者线程后,这个工作者线程就会被唤醒并执行该函数。

4.2 使用工作队列

1.创建推后的工作

方式 声明 说明
静态 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);

该函数会由一个工作中线程执行,因此,函数会运行在进程上下文中。
默认情况下,允许响应中断,并不持有任何锁。如有需要,函数可以睡眠。

ps:
尽管操作处理函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射。因此在工作队列和内核其他部分之间使用锁机制就像在其他进程上下文中使用锁机制一样方便。

通常在发生系统调用时,内核会代表用户空间的进程运行,此时它才能访问用户空间,也只有在此时它才会映射用户空间的内存。

3.对工作进行调度
Q:如何把给定工作的处理函数提交给缺省的events工作线程?
A:如下表所示:

方法函数 说明
schedule_work(&work) work马上就会被调度,
一旦其所在的处理器上的工作者线程被唤醒,它就会被执行
schedule_delay_work(&work, delay) &work指向的work_struct直到delay指定的时钟节拍用完以后才会执行

4.刷新操作
排入队列的工作会在工作者线程下一次被唤醒的时候执行。
考虑到如下情况:有时在继续下一步工作之前,必须保证一些操作已经执行完毕。如在模块卸载之前,就需要保证有些操作已经执行完毕。而在内核的其他部分,为了防止竞争条件的出现,也可能需要确保不再有待处理的工作。
因此,内核准备了一个用于刷新指定工作队列的函数:

void flush_scheduled_work(void);

该函数会一直等待,直到队列中所有对象都被执行以后才返回。
ps:在等待所有待处理的工作执行的时候,该函数会进入休眠状态–只能在进程上下文中使用

刷新函数并不取消任何延迟执行的工作–任何通过schedule_delayed_work()调度的工作,如果其延时时间未结束,它并不会因为调用flush_scheduled_work()而被刷新掉。

取消延时执行的工作应调用如下:

int cancel_delayed_work(struct work_struct *work); /* 取消任何与work相关的挂起工作 */

Q:前面提到工作者线程可用系统提供的缺省的events,也可以是驱动对应自己的,如何做?
A:创建新的工作队列,如果缺省的队列不能满足需要,那么应该创建一个新的工作队列和与之相应的工作者线程。由于这么做会在每个处理器上都创建一个工作者线程,所以只有你在明确了必须要靠自己的一套线程来提高性能的情况下,再创建自己的工作队列。

创建一个新的任务队列和与之相关的工作者线程,需要使用如下函数:

struct workqueue_struct *create_workqueue(const char *name); /* name参数用于该内核线程的命名 */

该函数调用后会创建所有的工作者线程(系统中的每个处理器都有一个),并且做好所有开始处理工作之前的准备工作。
新的工作队列操作函数如下所示(前面的都是针对缺省的events队列):

函数 说明
int queue_work(struct workqueue_struct *wq, struct work_struct *work); 同schedule_work()
int queue_delayed_work(struct workqueue_struct *wq,
struct work_struct *work, unsigned long delay);
同schedule_delayed_work()
flush_workqueue(struct workqueue_struce *wq); 同flush_scheduled_work()

4.3 老的任务队列(现在是工作队列)机制

因为是丢弃了的机制,不多学习,详细可参考本书8.4.3章节。简单介绍下:任务队列机制通过定义一组队列来实现其功能,每个队列都有自己的名字,如调度程序队列、立即队列和定时器队列。

5.下半部机制的选择

目前的可选择中有三种:软中断、tasklet和工作队列
tasket基于软中断实现。
工作队列机制靠内核线程实现。
三种下半部机制的比较如下:
在这里插入图片描述
Q:如何选择下半部机制?
A:
1.软中断:
从设计的角度考虑,软中断提供的执行序列化的保障最少。这就要求软中断处理函数必须格外小心地采取一些步骤确保共享数据的安全,两个甚至更多相同类别的软中断有可能在不同的处理器上同时执行
如果被考察的代码本身多线索化的工作就做得非常好,如网络子系统,它完全使用单处理器变量,那么软中断就是非常好的选择。对于时间要求严格和执行频率很高(内核会启动ksofrirqd线程辅助)的应用来说,它执行得也快。
2.tasklet:
如果代码多线索化考虑得并不充分,那么选择tasklet意义更大。它的接口非常简单,而且,由于两个同种类型的tasklet不能同时执行,所以实现起来也会简单一些。tasklet是有效的软中断,但不能并发运行。驱动程序开发者应当尽可能选择tasklet而不是软中断,当然,如果准备利用每一处理器上的变量或者类似的情形,以确保软中断能安全地在多个处理器上并发的运行,那么还是选择软中断。
3.工作队列:
如果需要把任务推后到进程上下文中(可能睡眠),那么在这三者中就只能选择工作队列了。如果进程上下文并不是必须的条件(不需要睡眠),那么软中断和tasklet可能更合适。工作队列造成的开销最大,因为它要牵扯到内核线程甚至是上下文切换。这并不是说工作队列的效率低,如果每秒钟有几千次中断,像网络子系统时常经历的那样,那么采用其他的机制可能更合适些。尽管如此,针对大部分情况,工作队列都能够提供足够的支持。

上述简单来说,是否需要一个可调度的实体(是否有休眠需要)来执行需要推后完成的工作–有,工作队列就是唯一选择
否则最好使用tasklet,若是必须专注于性能的提高,那么就考虑软中断

6.在下半部之间加锁

在使用下半部机制时,即使是在单处理器的系统上,避免共享数据被同时访问也是至关重要的。
记住,一个下半部实际上可能在任何时候执行

使用tasklet的一个好处在于,它自己负责执行的序列化保障:两个相同类型的tasklet不允许同时执行,即使在不同的处理器上也不行。

Q:通常在哪些地方枷锁?
A:

  1. 如果进程上下文和一个下半部共享数据,在访问这些数据之前,需要禁止下半部的处理并得到锁的使用权。是为了本地和SMP的保护并且防止死锁的出现;
  2. 如果中断上下文和一个下半部共享数据,在访问数据之前,需要禁止中断并得到锁的使用权。为了本地和SMP的保护并防止死锁的出现;
  3. 任何在工作队列中被共享的数据也需要使用锁机制,其中有关锁的要点和一般内核代码中没什么区别,工作队列本来就是在进程上下文中执行的。

7.禁止下半部

一般如果需要禁止下半部,那么单纯禁止下半部的处理是不够的的。
为了保证共享数据的安全,更常见的做法是,先得到一个锁然后在禁止下半部的处理

下半部机制控制函数如下所示:
在这里插入图片描述
上述函数有可能被嵌套使用–因此最后被调用的local_bh_enable()最后激活下半部。
并且它们通过preempt_count(内核抢占的时候也是它)为每个进程维护一个计数器。
当计数器变为0时,下半部才能够被处理。因为下半部的处理已经被禁止,所以local_bh_enable()还需要检查所有现存的待处理的下半部并执行它们。
这些函数与硬件体系结构相关,位于<asm/softirq.h>中,通常由一些复杂的宏实现。

ps:这些函数并不能禁止工作队列的执行(工作队列是在进程上下文中运行的,不会涉及异步执行的问题,所以也就不需要禁止工作队列执行。而软中断和tasklet是异步发生的–在中断处理返回的时候,所以内核要禁止它们)

发布了91 篇原创文章 · 获赞 17 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_23327993/article/details/105514044