在 Linux异常(中断)处理体系结构 这篇文章,我们详细描写了内核如何进行中断(异常)向量表的初始化、如何初始化硬件中断(IRQ)的操作。
在这篇文章中,我们将重心放在软件中断上。也就是 CPU 本身的中断。
这篇文章包括五个内容:
- 软中断
- tasklet
- 等待队列
- 完成量
- 工作队列
1. 软中断
软中断使得内核可以延期执行任务。因为它们的运作方式与上文描述的中断类似,但完全是用软件实现的,所以称为软中断(software interrupt)或 softIRQ 。
软中断机制的核心部分是一个表,包含32个 softirq_action 类型的数据项。该数据类型结构非常简单,只包含两个成员:
以下内容节选自 《深入Linux内核架构》
1.1 软中断的初始化
在内核中有一个软件中断的数组:
static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;
softirq_action 这个数据结构用于描述一个软件中断。
默认情况下,系统上只能使用 32 个软中断。
但这个限制不会有太大的局限性,因为软中断充当实现其他延期执行机制的基础,而且也很适合设备驱动程序的需要。
<interrupt.h>
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
- action:是一个指向处理程序例程的指针,在软中断发生时由内核执行该处理程序例程;
- data:是一个指向处理程序函数私有数据的指针。
软中断必须先注册,内核才能执行软中断。open_softirq 函数即用于该目的,它在 softirq_vec 表中指定的位置写入新的软中断:
kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
}
在每次调用软中断处理程序 action 时,data 用作参数。
各个软中断都有一个唯一的编号,这表明软中断是相对稀缺的资源,使用其必须谨慎,不能由各种设备驱动程序和内核组件随意使用。
只有中枢的内核代码才使用软中断。软中断只用于少数场合,这些都是相对重要的情况:
<interrupt.h>
enum { HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, TASKLET_SOFTIRQ SCHED_SOFTIRQ, #ifdef CONFIG_HIGH_RES_TIMERS HRTIMER_SOFTIRQ, #endif };
其中两个用来实现 tasklet(HI_SOFTIRQ、TASKLET_SOFTIRQ)
两个用于网络的发送和接收操作(NET_TX_SOFTIRQ 和 NET_RX_SOFTIRQ,这是软中断机制的来源和其最重要的应用)
一个用于块层,实现异步请求完成(BLOCK_SOFTIRQ)
一个用于调度器(SCHED_SOFTIRQ),以实现SMP系统上周期性的负载均衡。
在启用高分辨率定时器时,还需要一个软中断(HRTIMER_SOFTIRQ)
软中断的编号形成了一个优先顺序,这并不影响各个处理程序例程执行的频率或它们相对于其他系统活动的优先级,但定义了多个软中断之间同时活动或待决时处理例程执行的次序。
raise_softirq(int nr)
用于引发一个软中断(类似普通中断)。软中断的编号通过参数指定。该函数设置各 CPU 变量 irq_stat[smp_processor_id].__softirq_pending 中的对应比特位。该函数将相应的软中断标记为执行,但这个执行是延期执行。通过使用特定于处理器的位图,内核确保几个软中断(甚至是相同的)可以同时在不同的 CPU 上执行。
如果不在中断上下文调用 raise_softirq,则调用 wakeup_softirqd 来唤醒软中断守护进程,这是开启软中断处理的两个可选方法之一。
在 start_kernel 函数会调用 softirq_init();
函数来初始化软件中断:
void __init softirq_init(void)
{
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
}
1.2 开启软中断处理
有几种方法可开启软中断处理,但这些都归结为调用 do_softirq 函数。为此,我们详细介绍该函数。
asmlinkage void do_softirq(void)
{
__u32 pending;
unsigned long flags;
if (in_interrupt())
return;
local_irq_save(flags);
pending = local_softirq_pending();
if (pending)
__do_softirq();
local_irq_restore(flags);
}
in_interrupt()
该函数确认当前不处于中断上下文中(当然,即不涉及硬件中断)。如果处于中断上下文,则立即结束。因为软中断用于执行 ISR 中非时间关键部分,所以其代码本身一定不能在中断处理程序内调用。
通过 local_softirq_pending,确定当前 CPU 软中断位图中所有置位的比特位。如果有软中断等待处理,则调用 __do_softirq。
asmlinkage void __do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;
pending = local_softirq_pending();
account_system_vtime(current);
__local_bh_disable((unsigned long)__builtin_return_address(0));
trace_softirq_enter();
cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);
local_irq_enable();
h = softirq_vec;
do {
if (pending & 1) {
h->action(h);
rcu_bh_qsctr_inc(cpu);
}
h++;
pending >>= 1;
} while (pending);
local_irq_disable();
pending = local_softirq_pending();
if (pending && --max_restart)
goto restart;
if (pending)
wakeup_softirqd();
trace_softirq_exit();
account_system_vtime(current);
_local_bh_enable();
}
set_softirq_pending(0)
该函数将原来的位图重置为0。
换句话说,清除所有软中断。
这两个操作都是在(当前处理器上)禁用中断的情况下执行,以防其他进程对位图的修改造成干扰。而后续代码是在允许中断的情况下执行。这使得在软中断处理程序执行期间的任何时刻,都可以修改原来的位图。
asmlinkage void __do_softirq(void)
{
/* ...省略... */
do {
if (pending & 1) {
h->action(h);
rcu_bh_qsctr_inc(cpu);
}
h++;
pending >>= 1;
} while (pending);
/* ...省略... */
}
softirq_vec 中的 action 函数在一个 while 循环中针对各个待决的软中断被调用。
在处理了所有标记出的软中断之后,内核检查在此期间是否有新的软中断标记到位图中。要求在前一轮循环中至少有一个没有处理的软中断,而重启的次数没有超过 MAX_SOFTIRQ_RESTART(通常设置为10)。如果是这样,则再次按序处理标记的软中断。这操作会一直重复下去,直至在执行所有处理程序之后没有新的未处理软中断为止。
pending = local_softirq_pending();
if (pending && --max_restart)
goto restart;
如果在 MAX_SOFTIRQ_RESTART 次重启处理过程之后,仍然有未处理的软中断,那么应该如何?内核将调用 wakeup_softirqd 唤醒软中断守护进程。
if (pending)
wakeup_softirqd();
1.2 软中断守护进程
软中断守护进程的任务是,与其余内核代码异步执行软中断。为此,系统中的每个处理器都分配了自身的守护进程,名为 ksoftirqd。
内核中有两处调用 wakeup_softirqd 唤醒了该守护进程。
-
在 do_softirq 中,如前所述。
-
在 raise_softirq_irqoff 末尾。该函数由 raise_softirq 在内部调用,如果内核当前停用了中断,也可以直接使用。
唤醒函数本身只需要几行代码。
static inline void wakeup_softirqd(void)
{
/* Interrupts are disabled: no need to stop preemption */
struct task_struct *tsk = __get_cpu_var(ksoftirqd);
if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}
-
首先,借助于一些宏,从一个个 CPU 变量读取指向当前 CPU 软中断守护进程的 task_struct 的指针。
/* var is in discarded region: offset to particular copy we want */ #define per_cpu(var, cpu) (*({ \ extern int simple_identifier_##var(void); \ RELOC_HIDE(&per_cpu__##var, __per_cpu_offset[cpu]); })) #define __get_cpu_var(var) per_cpu(var, smp_processor_id())
-
如果该进程当前的状态不是 TASK_RUNNING,则通过 wake_up_ process 将其放置到就绪进程的列表末尾。
尽管这并不会立即开始处理所有待决软中断,但只要调度器没有更好的选择,就会选择该守护进程(优先级为19)来执行。
在系统启动时用 initcall 机制(见附录D)调用 init 不久,即创建了系统中的软中断守护进程。
在初始化之后,各个守护进程都执行以下无限循环:
**kernel/softirq.c **
static int ksoftirqd(void * __bind_cpu)
{
/* ...省略... */
while (!kthread_should_stop()) {
if (!local_softirq_pending()) {
schedule();
}
__set_current_state(TASK_RUNNING);
while (local_softirq_pending()) {
do_softirq();
cond_resched();
}
set_current_state(TASK_INTERRUPTIBLE);
}
/* ...省略... */
}
-
每次被唤醒时,守护进程首先检查是否有标记出的待决软中断,否则明确地调用调度器,将控制转交到其他进程。
if (!local_softirq_pending()) { schedule(); }
-
如果有标记出的软中断,那么守护进程接下来将处理软中断。进程在一个while循环中重复调用两个函数 do_softirq 和 cond_resched,直至没有标记出的软中断为止。
while (local_softirq_pending()) { do_softirq(); cond_resched(); }
cond_resched 确保在对当前进程设置了 TIF_NEED_RESCHED 标志的情况下调用调度器。这是可能的,因为所有这些函数执行时都启用了硬件中断。
2. tasklet
软中断是将操作推迟到未来时刻执行的最有效的方法。但该延期机制处理起来非常复杂。因为多个处理器可以同时且独立地处理软中断,同一个软中断的处理程序例程可以在几个 CPU 上同时运行。
对软中断的效率来说,这是一个关键,多处理器系统上的网络实现显然受惠于此。但处理程序例程的设计必须是完全可重入且线程安全的。另外,临界区必须用自旋锁保护(或其他IPC机制,参见第5章),而这需要大量审慎的考虑。
tasklet 和工作队列是延期执行工作的机制,其实现基于软中断,但它们更易于使用,因而更适合于设备驱动程序(以及其他一般性的内核代码)。
在深入技术细节之前,请注意所使用的术语:由于历史原因,术语下半部(bottom half)通常指代两个不同的东西;
- 首先,它是指 ISR 代码的下半部,负责执行非时间关键操作。
- 遗憾的是,早期内核版本中使用的操作延期执行机制,也称为下半部,因而使用的术语经常是含糊不清的。在此期间,下半部不再作为内核机制存在。它们在内核版本2.5开发期间被废弃,被 tasklet 代替,这是一个好得多的替代品。
tasklet 是“小进程”,执行一些迷你任务,对这些任务使用全功能进程可能比较浪费。
2.1 创建 tasklet
各个 tasklet 的中枢数据结构称作 tasklet_struct,定义如下:
<interrupt.h>
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
-
func:从设备驱动程序的角度来看,最重要的成员是 func。它指向一个函数的地址,该函数的执行将被延期。
-
data:用作该函数执行时的参数。
-
next:是一个指针,用于建立 tasklet_struct 实例的链表。这容许几个任务排队执行。
-
state:表示任务的当前状态,类似于真正的进程。但只有两个选项,分别由 state 中的一个比特位表示,这也是二者可以独立设置/清除的原因。
-
在tasklet注册到内核,等待调度执行时,将设置 TASKLET_STATE_SCHED。
-
TASKLET_STATE_RUN 表示 tasklet 当前正在执行。
第二个状态只在 SMP 系统上有用。用于保护 tasklet 在多个处理器上并行执行。
-
原子计数器 count 用于禁用已经调度的 tasklet。如果其值不等于0,在接下来执行所有待决的 tasklet 时,将忽略对应的 tasklet。
2.2 注册 tasklet
tasklet_schedule 将一个 tasklet 注册到系统中:
<interrupt.h>
static inline void tasklet_schedule(struct tasklet_struct *t);
如果设置了 TASKLET_STATE_SCHED 标志位,则结束注册过程,因为该 tasklet 此前已经注册了。否则,将该 tasklet 置于一个链表的起始,其表头是特定于 CPU 的变量 tasklet_vec。该链表包含了所有注册的 tasklet,使用 next 成员作为链表元素。在注册了一个 tasklet 之后,tasklet 链表即标记为即将进行处理。
2.3 执行 tasklet
tasklet 的生命周期中最重要的部分就是其执行。因为 tasklet 基于软中断实现,它们总是在处理软中断时执行。
tasklet 关联到 TASKLET_SOFTIRQ 软中断。因而,调用 raise_softirq(TASKLET_SOFTIRQ),即可在下一个适当的时机执行当前处理器的 tasklet。内核使用 tasklet_action 作为该软中断的 action 函数。
该函数首先确定特定于 CPU 的链表,其中保存了标记为将要执行的各个 tasklet。它接下来将表头重定向到函数局部的一个数据项,相当于从外部公开的链表删除了所有表项。接下来,函数在以下循环中逐一处理各个 tasklet:
static void tasklet_action(struct softirq_action *a)
{
/* ...省略... */
while (list) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
BUG();
t->func(t->data);
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
t->next = __get_cpu_var(tasklet_vec).list;
__get_cpu_var(tasklet_vec).list = t;
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable();
}
}
在 while 循环中执行 tasklet,类似于处理软中断使用的机制。
-
因为一个 tasklet 只能在一个处理器上执行一次,但其他的 tasklet 可以并行运行,所以需要特定于 tasklet 的锁。 state 状态用作锁变量。在执行一个 tasklet 的处理程序函数之前,内核使用 tasklet_trylock 检查 tasklet 的状态是否为 TASKLET_STATE_RUN。换句话说,它是否已经在系统的另一个处理器上运行:
static inline int tasklet_trylock(struct tasklet_struct *t) { return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state); }
如果对应比特位尚未设置,则设置该比特位。
-
如果 count 成员不等于0,则该 tasklet 已经停用。在这种情况下,不执行相关的代码。
-
在两项检查都成功通过之后,内核用对应的参数执行 tasklet 的处理程序函数,即调用 t->func(t->data)。
-
最后,使用 tasklet_unlock 清除 tasklet 的 TASKLET_SCHED_RUN 标志位。
-
如果在 tasklet 执行期间,有新的 tasklet 进入当前处理器的 tasklet 队列,则会尽快引发 TASKLET_SOFTIRQ 软中断(
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
)来执行新的 tasklet。
除了普通的 tasklet 之外,内核还使用了另一种 tasklet,它具有“较高”的优先级。除以下修改之外,其实现与普通的 tasklet 完全相同。
-
使用 HI_SOFTIRQ 作为软中断,而不是 TASKLET_SOFTIRQ,相关的 action 函数是 tasklet_hi_action。
-
注册的 tasklet 在 CPU 相关的变量 tasklet_hi_vec 中排队。这是使用 tasklet_hi_schedule 完成的。
在这里,“较高优先级”是指该软中断的处理程序 HI_SOFTIRQ 在所有其他处理程序之前执行,尤其是在构成了软中断活动主体的网络处理程序之前执行。
当前,大部分声卡驱动程序都利用了这一选项,因为操作延迟时间太长可能损害音频输出的音质。而用于高速传输的网卡也可以得益于该机制。
3. 等待队列
等待队列(wait queue)用于使进程等待某一特定事件发生,而无须频繁轮询。进程在等待期间睡眠,在事件发生时由内核自动唤醒。完成量(completion)机制基于等待队列,内核利用该机制等待某一操作结束。这两种机制使用得都比较频繁,主要用于设备驱动程序。
3.1 数据结构
每个等待队列都有一个队列头,由以下数据结构表示:
<wait.h>
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
因为等待队列也可以在中断时修改,在操作队列之前必须获得一个自旋锁 lock(参见第5章)。
task_list 是一个双链表,用于实现双链表最擅长表示的结构,即队列。
wait_queue_t 的定义如下:
struct __wait_queue {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;
1234567
- flags:flags 的值或者为 WQ_FLAG_EXCLUSIVE,或者为0,当前没有定义其他标志。WQ_FLAG_EXCLUSIVE 表示等待进程想要被独占地唤醒(稍后将详细讲述)。
- private:是一个指针,指向等待进程的 task_struct 实例。该变量本质上可以指向任意的私有数据,但内核中只有很少情况下才这么用,因此这里不会详细讲述这种情形。
- func:调用 func,唤醒等待进程。
- task_list:用作一个链表元素,用于将 wait_queue_t 实例放置到等待队列中。
3.2 等待队列
3.2.1 等待队列的使用
等待队列的使用分为如下两部分:
-
为使当前进程在一个等待队列中睡眠,需要调用 wait_event 函数(或某个等价函数,在下文讨论)。进程进入睡眠,将控制权释放给调度器。
内核通常会在向块设备发出传输数据的请求后,调用该函数。因为传输不会立即发生,而在此期间又没有其他事情可做,所以进程可以睡眠,将CPU时间让给系统中的其他进程。
除了 wait_event 函数外,还有 wait_event_timeout 和 wait_event_interruptible 两个变体函数。
-
在内核中另一处,就我们的例子而言,是来自块设备的数据到达后,必须调用 wake_up 函数(或某个等价函数,将在下文讨论)来唤醒等待队列中的睡眠进程。
wake_up 有与其相关的其它变体函数:wake_up_nr、wake_up_all、wake_up_interruptible、wake_up_interruptible_nr、wake_up_interruptible_all、wake_up_locked、wake_up_interruptible_sync。
在使用 wait_event 使进程睡眠之后,必须确保在内核中另一处有一个对应的 wake_up 调用。
3.2.2 使进程睡眠
add_wait_queue
add_wait_queue 函数用于将一个进程增加到等待队列,该函数在获得必要的自旋锁后,将工作委托给 __add_wait_queue:
<wait.h>
static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)
{
list_add(&new->task_list, &head->task_list);
}
在将新进程统计到等待队列时,除了使用标准的 list_add 链表函数,没有其他工作需要做。
add_wait_queue_exclusive
内核还提供了 add_wait_queue_exclusive 函数。它的工作方式与 add_wait_queue 相同,但将进程插入在队列尾部,并将其标志设置为 WQ_EXCLUSIVE(该标志的语义在下文讨论)。
void fastcall add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
wait->flags |= WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
__add_wait_queue_tail(q, wait);
spin_unlock_irqrestore(&q->lock, flags);
}
prepare_to_wait
使进程在等待队列上睡眠的另一种方法是 prepare_to_wait。除了 add_wait_queue 需要的参数之外,还需要进程的状态:
kernel/wait.c
void fastcall prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
unsigned long flags;
wait->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
if (list_empty(&wait->task_list))
__add_wait_queue(q, wait);
/* ...省略... */
set_current_state(state);
spin_unlock_irqrestore(&q->lock, flags);
}
像在上文讨论的那样,调用 __add_wait_queue 之后,内核将进程当前的状态设置为传递到 prepare_to_wait 的状态。
prepare_to_wait_exclusive
prepare_to_wait_exclusive 是一个变体,它会设置 WQ_FLAG_EXCLUSIVE 标志并将等待队列的成员添加到队列尾部。
prepare_to_wait_exclusive(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
unsigned long flags;
wait->flags |= WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
if (list_empty(&wait->task_list))
__add_wait_queue_tail(q, wait);
/*
* don't alter the task state if this is just going to
* queue an async wait queue callback
*/
if (is_sync_wait(wait))
set_current_state(state);
spin_unlock_irqrestore(&q->lock, flags);
}
3.2.3 初始化等待队列
下面两个标准方法可用于初始化一个等待队列项。
-
init_waitqueue_entry 初始化一个动态分配的 wait_queue_t 实例:
static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p) { q->flags = 0; q->private = p; q->func = default_wake_function; } 1234567
default_wake_function:只是一个进行参数转换的前端,试图用第2章描述的 try_to_wake_up函数来唤醒进程。
-
DEFINE_WAIT 创建 wait_queue_t 的静态实例,它可以自动初始化:
#define DEFINE_WAIT(name) \ wait_queue_t name = { \ .private = current, \ .func = autoremove_wake_function, \ .task_list = LIST_HEAD_INIT((name).task_list), \ }
这里用 autoremove_wake_function 来唤醒进程。
int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key) { int ret = default_wake_function(wait, mode, sync, key); if (ret) list_del_init(&wait->task_list); return ret; }
该函数不仅调用 default_wake_function,还将所属等待队列成员从等待队列删除。
wait_event
add_wait_queue 通常不直接使用。更常用的是 wait_event(还有 wait_event_timeout 和 wait_event_interruptible)。这是一个宏,需要如下两个参数。
-
在其上进行等待的等待队列。
-
一个条件,以所等待事件有关的一个 C 表达式形式给出。
这个宏只确认条件尚未满足。如果条件已经满足,可以立即停止处理,因为没什么可等待的了。主要的工作委托给 __wait_event:
#define __wait_event(wq, condition)
do { \
DEFINE_WAIT(__wait); \
\
for (;;) { \
prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE); \
if (condition) break; \
schedule(); \
} \
finish_wait(&wq, &__wait); \
} while (0)
- 在用 DEFINE_WAIT 建立等待队列成员之后,这个宏产生了一个无限循环。
- 使用 prepare_to_wait 使进程在等待队列上睡眠。
- 每次进程被唤醒时,内核都会检查指定的条件是否满足,
- 如果条件满足则退出无限循环。
- 否则,将控制转交给调度器,进程再次睡眠。
在条件满足时,finish_wait 将进程状态设置回 TASK_RUNNING,并从等待队列的链表移除对应的项。
除了 wait_event 之外,内核还定义了其他几个函数,可以将当前进程置于等待队列中。其实现实际上等同于 sleep_on:
#define wait_event_interruptible(wq, condition)
#define wait_event_timeout(wq, condition, timeout) { ... }
#define wait_event_interruptible_timeout(wq, condition, timeout)
- wait_event_interruptible 使用进程状态变为 TASK_INTERRUPTIBLE。因而睡眠进程可以通过接收信号而唤醒。
- wait_event_timeout 等待满足指定的条件,但如果等待时间超过了指定的超时限制(按jiffies指定)则停止。这防止了进程永远睡眠。
- wait_event_interruptible_timeout 使进程睡眠,但可以通过接收信号唤醒。它也注册了一个超时限制。从内核采用的命名方式来看,一般不会有出人意料之处!
此外,内核还定义了若干废弃的函数(sleep_on、sleep_on_timeout、interruptible_sleep_on 和 interruptible_sleep_on_timeout),这些不应该在新的代码中继续使用。保留这些函数,主要是出于兼容性的目的。
3.2.4 唤醒进程
内核定义了一系列宏,可用于唤醒等待队列中的进程。它们基于同一个函数:
#define wake_up(x) __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_nr(x, nr) __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_all(x) __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 0, NULL)
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_interruptible_all(x) __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)
在获得了用于保护等待队列首部的锁之后,__wake_up 将工作委托给 __wake_up_common。
kernel/sched.c
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int sync, void *key);
- q:用于选定等待队列;
- mode: 指定进程的状态,用于控制唤醒进程的条件。
- nr_exclusive:表示将要唤醒的设置了 WQ_FLAG_EXCLUSIVE 标志的进程的数目。
内核接下来遍历睡眠进程,并调用其唤醒函数 func:
kernel/sched.c
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int sync, void *key)
{
/* ...省略... */
list_for_each_safe(curr, next, &q->task_list, task_list) {
unsigned flags = curr->flags;
if (curr->func(curr, mode, sync, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
这里会反复扫描链表,直至没有更多进程需要唤醒,或已经唤醒的独占进程的数目达到了 nr_exclusive。
该限制用于避免所谓的惊群(thundering herd)问题。如果几个进程在等待独占访问某一资源,那么同时唤醒所有等待进程是没有意义的,因为除了其中一个之外,其他进程都会再次睡眠。nr_exclusive 推广了这一限制。
最常使用的 wake_up 函数将 nr_exclusive 设置为1,确保只唤醒一个独占访问的进程。
回想上文,WQ_FLAG_EXCLUSIVE 进程被添加在等待队列的尾部。这种实现确保在混合访问类型的队列中,首先唤醒所有的普通进程,然后才考虑到对独占进程的限制。
如果进程在等待数据传输的结束,那么唤醒等待队列中所有的进程是有用的。这是因为几个进程的数据可以同时读取,而互不干扰。
4. 完成量
完成量与信号量有些相似,但是基于等待队列实现的。
我们感兴趣的是完成量的接口。场景中有两个参与者:一个在等待某操作完成,而另一个在操作完成时发出声明。实际上,这已经被简化过了:可以有任意数目的进程等待操作完成。为表示进程等待的即将完成的“某操作”,内核使用了下述数据结构:
<completion.h>
struct completion {
unsigned int done;
wait_queue_head_t wait;
};
-
可能在某些进程开始等待之前,事件就已经完成,done用来处理这种情形。
struct completion 中 done 的语义是什么呢?
每次调用 complete 时,该计数器都加1,仅当 done 等于0时,wait_for 系列函数才会使调用进程进入睡眠。
实际上,这意味着进程无须等待已经完成的事件。
complete_all 的工作方式类似,但它会将计数器设置为最大可能值(UINT_MAX/2,这是无符号整数最大值的一半,因为计数器也可能取负值),这样,在事件完成后调用 wait_ 系列函数的进程将永远不会睡眠。
-
wait 是一个标准的等待队列,等待进程在队列上睡眠。
init_completion 初始化一个动态分配的 completion 实例,而 DECLARE_COMPLETION 宏用来建立该数据结构的静态(static)实例。
进程可以用 wait_for_completion 添加到等待队列,进程在其中等待(以独占睡眠状态),直至请求被内核的某些部分处理。这函数需要一个 completion 实例作为参数:
<completion.h>
void wait_for_completion(struct completion *);
int wait_for_completion_interruptible(struct completion *x);
unsigned long wait_for_completion_timeout(struct completion *x, unsigned long timeout);
unsigned long wait_for_completion_interruptible_timeout(struct completion *x, unsigned long timeout);
此外还提供了如下几个改进过的变体。
-
通常进程在等待事件的完成时处于不可中断状态,但如果使用 wait_for_completion_interruptible,可以改变这一设置。如果进程被中断,该函数返回 -ERESTARTSYS,否则返回0。
-
wait_for_completion_timeout 等待一个完成事件发生,但提供了超时设置(以 jiffies 为单位),如果等待时间超出了这一设置,则取消等待。这有助于防止无限等待某一事件。如果在超时之前事件已经完成,则函数返回剩余的时间,否则返回0。
-
wait_for_completion_interruptible_timeout 是前两种变体的组合。
在请求由内核的另一部分处理之后,必须调用 complete 或 complete_all 来唤醒等待的进程。因为每次调用只能从完成量的等待队列移除一个进程,对n个等待进程来说,必须调用该函数 n 次。
另一方面,complete_all 将唤醒所有等待该完成的进程。 complete_and_exit 是一个小的包装器,首先调用 complete,接下来调用 do_exit 结束内核线程。
<completion.h>
void complete(struct completion *);
void complete_all(struct completion *);
kernel/exit.c
NORET_TYPE void complete_and_exit(struct completion *comp, long code);
complete、complete_all 和 complete_and_exit 需要一个指向 struct completion实例的指针作为参数,标识所述的完成量。
5. 工作队列
工作队列是将操作延期执行的另一种手段。因为它们是通过守护进程在用户上下文执行,函数可以睡眠任意长的时间,这与内核是无关的。在内核版本2.5开发期间,设计了工作队列,用以替换此前使用的 keventd 机制。
每个工作队列都有一个数组,数组项的数目与系统中处理器的数目相同。每个数组项都列出了将延期执行的任务。
对每个工作队列来说,内核都会创建一个新的内核守护进程,延期任务使用上文描述的等待队列机制,在该守护进程的上下文中执行。
新的工作队列通过调用 create_workqueue
或 create_workqueue_singlethread
函数来创建。前一个函数在所有CPU上都创建一个工作线程,而后者只在系统的第一个 CPU 上创建一个线程。两个函数在内部都使用了 __create_workqueue_key:
kernel/workqueue.c
struct workqueue_struct *__create_workqueue(const char *name, int singlethread)
- name 参数表示创建的守护进程在进程列表中显示的名称。
- 如果 singlethread 设置为0,则在系统的每个 CPU 上都创建一个线程,否则只在第一个 CPU 上创建线程。
所有推送到工作队列上的任务,都必须打包为 work_struct 结构的实例,从工作队列用户的角度来看,该结构的下述成员是比较重要的:
<workqueue.h>
struct work_struct;
typedef void (*work_func_t)(struct work_struct *work);
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
}
-
entry 照例用作链表元素,用于将几个 work_struct 实例群集到一个链表中。
-
func 是一个指针,指向将延期执行的函数。该函数有一个参数,是一个指针,指向用于提交该工作的 work_struct 实例。
这使得工作函数可以获得 work_struct 的 data 成员,该成员可以指向与 work_struct 相关的任意数据。
-
data 作为指向任意数据的指针的数据类型。请注意,将 data 设置为原子数据类型,确保对该比特位的修改不会带来并发问题。
为什么内核使用 atomic_long_t 作为指向任意数据的指针的数据类型,而不是通常的 void * ?实际上,此前的内核版本定义的work_struct如下:
<workqueue.h>
struct work_struct { /* ...省略... */ void (*func)(void *); void *data; /* ...省略... */ };
正如所料,data 是用指针表示的。
但内核使用了一点小技巧,显然有点近乎于“肮脏”,以便将更多信息放入该结构,而又不付出更多代价。因为指针在所有支持的体系结构上都对齐到4字节边界,而前两个比特位保证为0。因而可以“滥用”这两个比特位,将其用作标志位。剩余的比特位照旧保存指针的信息。
以下的宏用于屏蔽标志位:
**<workqueue.h> **
#define WORK_STRUCT_FLAG_MASK (3UL) #define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
当前只定义了一个标志:WORK_STRUCT_PENDING 用来查找当前是否有待决(该标志位置位)的可延迟工作项。
辅助宏 work_pending(work) 用来检查该标志位。
为简化声明和填充该结构的静态实例所需的工作,内核提供了 INIT_WORK(work, func) 宏,它向一个现存的 work_struct 实例提供一个延期执行函数。如果需要 data 成员,则需要稍后设置。
有两种方法可以向一个工作队列添加 work_struct 实例,分别是 queue_work 和 queue_work_ delayed。
-
第一个函数的原型如下:
kernel/workqueue.c
int fastcall queue_work(struct workqueue_struct *wq, struct work_struct *work) { int ret = 0; if (!test_and_set_bit(WORK_STRUCT_PENDING, work_data_bits(work))) { BUG_ON(!list_empty(&work->entry)); __queue_work(wq_per_cpu(wq, get_cpu()), work); put_cpu(); ret = 1; } return ret; }
它将 work 添加到工作队列 wq,work 本身所指定的工作,其执行时间待定(在调度器选择该守护进程时执行)。
-
第二个函数,为确保排队的工作项将在提交后指定的一段时间内执行,需要扩展 work_struct,添加一个定时器。显然的解决方案如下:
<workqueue.h>
struct delayed_work { struct work_struct work; struct timer_list timer; };
queue_delayed_work 用于向工作队列提交 delayed_work 实例。它确保在延期工作执行之前,至少会经过由 delay 指定的一段时间(以 jiffies 为单位)
kernel/workqueue.c
int fastcall queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, unsigned long delay)
该函数首先创建一个内核定时器,它将在 delayed jiffies 之内超时。
相关的处理程序接下来使用 queue_work,按通常的方式将工作添加到工作队列。
内核创建了一个标准的工作队列,称为 events。内核的各个部分中,凡是没有必要创建独立的工作队列者,均可使用该队列。内核提供了以下两个函数,可用于将新的工作添加该标准队列,这里不会详细讨论其实现:
kernel/workqueue.c
int schedule_work(struct work_struct *work)
int schedule_delayed_work(struct delay_work *dwork, unsigned long delay)