9.5 KiB
中断和中断处理。 第九部分。
延后中断(软中断,Tasklets和Workqueues)介绍
这是linux内核揭密中断部分的第九小节,在之前章节我们了解了源文件arch/x86/kernel/irqinit.c中init_IRQ的实现。接下来的这一节我们将继续深入学习和外部硬件中断相关的初始化。
在init/main.c中我们可以看到在init_IRQ函数后面调用了softirq_init函数。这个函数在源文件kernel/softirq.c中定义,从名字我们可以看出,它的作用是初始化软中断或者也可以说是初始化延后中断。那么什么是延后中断?在讲解内核初始化过程的部分第九小结我们已经对他有了一些了解,Linux内核中一共有三种'延后中断':
软中断;tasklets;工作队列;
在这一小节我们将详细介绍这三种实现。就像我说的,我们对这个主题有一些了解。那么,现在是时间深入了解一下了。
延后中断
对中断处理有一些严格的要求,总的来说有两种:
- 中断处理必须快速执行完毕
- 有时中断处理必须做很多冗长的事情
就像你所想到的,我们几乎不可能同时做到这两点,之前的中断被分为两部分:
- 前半部
- 后半部
后半部曾经是Linux内核延后中断执行的一种方式,但现在的实际情况已经不是这样了。这种遗留称谓现在作为名词代表所有延后中断执行的方式。伴随着内核对并行处理的支持,出于性能考虑,所有新的下半部实现方案都基于被称之为ksoftirqd(稍后将详细讨论)的内核线程。ksoftirqd中断处理方式几乎和硬件中断处理一样重要。中断延后处理会在系统负载较低的时候才执行一个中断的具体实现行为。如你所知,中断处理代码运行于禁止响应后续中断的中断处理上下文中,所以要避免长时间执行。但有时中断处理却又有很多的工作需要执行,所以中断处理有时会被分为两部分。第一部分中,中断处理先只做少量的最重要工作,接下来提交第二部分到内核调度,然后就结束运行。当系统比较空闲并且处理器上下文允许处理中断时,第二部分就会开始执行被延后的剩余中断任务。以上是对延后中断处理的简要介绍。
就像上面说的,延后中断(或者叫软中断)和tasklets是由一些内核线程(每个处理器一个线程)来执行的。每个处理器都有自己的内核线程,名字叫做ksoftirqd/n,n是处理器的编号。我们可以通过系统命令systemd-cgls看到它们:
$ systemd-cgls -k | grep ksoft
├─ 3 [ksoftirqd/0]
├─ 13 [ksoftirqd/1]
├─ 18 [ksoftirqd/2]
├─ 23 [ksoftirqd/3]
├─ 28 [ksoftirqd/4]
├─ 33 [ksoftirqd/5]
├─ 38 [ksoftirqd/6]
├─ 43 [ksoftirqd/7]
由spawn_ksoftirqd函数启动这些线程。就像我们看到的,这个函数在早期的initcall被调用。
early_initcall(spawn_ksoftirqd);
延后中断在Linux内核编译时就静态的确定了,open_softirq函数负责softirq初始化。open_softirq在kernel/softirq.c中定义:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
这个函数有两个参数:
softirq_vec数组的索引序号- 一个指向软中断处理函数的指针
我们首先来看softirq_vec数组:
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
它在同一个源文件中定义。softirq_vec数组包含了NR_SOFTIRQS(其值为10)个不同softirq类型的softirq_action。当前版本的Linux内核定义了十种软中断向量。其中两个tasklet相关,两个网络相关,两个块处理层相关,两个定时器相关,另外调度器和RCU也各占一个。所有这些都在一个枚举中定义:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ,
NR_SOFTIRQS
};
以上软中断的名字在如下的数组中定义:
const char * const softirq_to_name[NR_SOFTIRQS] = {
"HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "BLOCK_IOPOLL",
"TASKLET", "SCHED", "HRTIMER", "RCU"
};
我们也可以在'/proc/softirqs'的输出中看到他们:
~$ cat /proc/softirqs
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
HI: 5 0 0 0 0 0 0 0
TIMER: 332519 310498 289555 272913 282535 279467 282895 270979
NET_TX: 2320 0 0 2 1 1 0 0
NET_RX: 270221 225 338 281 311 262 430 265
BLOCK: 134282 32 40 10 12 7 8 8
BLOCK_IOPOLL: 0 0 0 0 0 0 0 0
TASKLET: 196835 2 3 0 0 0 0 0
SCHED: 161852 146745 129539 126064 127998 128014 120243 117391
HRTIMER: 0 0 0 0 0 0 0 0
RCU: 337707 289397 251874 239796 254377 254898 267497 256624
可以看到softirq_vec数组的元素类型为softirq_action。这是软中断机制里一个重要的数据结构,它只有一个指向中断处理函数的成员:
struct softirq_action
{
void (*action)(struct softirq_action *);
};
现在我们可以理解到open_softirq函数实际上用softirq_action参数填充了softirq_vec数组。由open_softirq注册的延后中断处理函数会由raise_softirq调用。这个函数只有一个参数 -- 软中断序号nr。来看下它的实现:
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
可以看到在local_irq_save和local_irq_restore两个宏中间调用了raise_softirq_irqoff函数。local_irq_save的定义位于include/linux/irqflags.h头文件,它保存了eflags寄存器中的IF标志位并且禁用了当前处理器的中断。local_irq_restore宏定义于同一头文件中,它做了完全相反的事情:装回之前保存的中断标志位然后允许中断。这里之所以要禁用中断是因为softirq中断运行于中断上下文中,并且????????????????????
raise_softirq_irqoff函数设置当前处理器上和nr参数对应的软中断标志位(__softirq_pending)。这是通过以下代码做到的:
__raise_softirq_irqoff(nr);
然后,通过in_interrupt函数获得irq_count值。我们在这一章的第一小节已经知道它是用来检测一个cpu是否已经有软中断需要处理。如果我们不处于中断上下文中,我们就退出raise_softirq_irqoff函数,装回IF标志位并允许当前处理器的中断。如果在中断上下文中,就会调用wakeup_softirqd函数:
if (!in_interrupt())
wakeup_softirqd();
wakeup_softirqd函数会激活当前处理器上的ksoftirqd内核线程:
static void wakeup_softirqd(void)
{
struct task_struct *tsk = __this_cpu_read(ksoftirqd);
if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}
每个ksoftirqd内核线程都运行run_ksoftirqd函数来检测是有有延后中断需要处理,如果有的话就会调用__do_softirq函数。__do_softirq读取当前处理器对应的__softirq_pending软中断标记,并调用所有已被标记中断对应的处理函数。在执行一个延后函数的同时,可能会发生新的软中断。这会导致用户态代码由于__do_softirq要处理很多延后中断而很长时间不能返回。为了解决这个问题,系统限制了延后中断处理的最大耗时:
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
...
...
...
restart:
while ((softirq_bit = ffs(pending))) {
...
h->action(h);
...
}
...
...
...
pending = local_softirq_pending();
if (pending) {
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
}
...