Linux 中断子系统(五)SoftIRQ

原图

软中断的定义

软中断(softirq)是中断处理程序在开启中断的情况下执行的部分,可以被硬中断抢占内核定义了一张软中断向量表,每种软中断有一个唯一的编号,对应一个 softirq_actior 实例,softirq_action 实例的成员 action 是处理函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<kernel/softirq.c>
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
...
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */

NR_SOFTIRQS
};
1
2
3
4
5
<include/linux/interrupt.h>
struct softirq_action
{
void (*action)(struct softirq_action *);
};

softirq_action 该结构体非常简单就是一个函数指针,用于指向具体定义的函数。

中断的来源很多,所以 softirq 的种类也不少。内核的限制是不能超过 32 个, 目前内核定义了 10 种软中断,各种软中断的编号如下:

  • HI_SOFTIRQ:高优先级的小任务
  • TIMER_SOFTIRQ:定时器软中断
  • NET_TX_SOFTIRQ:网络栈发送报文的软中断
  • NET_RX_SOFTIRQ:网络栈接收报文的软中断
  • BLOCK_SOFTIRO:块设备软中断
  • IRQ_POLL_SOFTIRQ:支持 I/O 轮询的块设备软中断
  • TASKLET SOFTIRQ:低优先级的小任务
  • SCHED_SOFTIRQ:调度软中断,用于在处理器之间负载均衡
  • HRTIMER SOFTIRQ:高精度定时器,这种软中断已经被废弃,目前在中断处理程序的上半部处理高精度定时器
  • RCU_SOFTIRO:RCU 软中断

软中断的编号形成了优先级顺序,编号小的软中断优先级高。

软中断的实现

注册软中断的处理函数

函数 open_softirq() 用来注册软中断的处理函数,在软中断向量表中为指定的软中断编号设置处理函数。

1
2
<kernel/softirq.c>
void open_softirq(int nr, void (*action)(struct softirq_action *))

Tips 同一种软中断的处理函数可以在多个处理器上同时执行,处理函数必须是可以重入的,需要使用锁保护临界区。

触发软中断

在中断的 top half 处理完后,就会通过 raise_softirq() 设置 softirq 的 pending 位图,这个 pending 位图由一个名为"__softirq_pending"的 per-CPU 形式的变量表示。

1
2
3
4
5
6
7
8
void raise_softirq(unsigned int nr) //nr 参数是软中断编号
{
unsigned long flags;

local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}

该函数调用 raise_softirq_irqoff(),raise_softirq_irqoff() 是在已经禁止中断的情况下调用函数来触发软中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);

/*
* If we're in an interrupt or softirq, we're done
* (this also catches softirq-disabled code). We will
* actually run the softirq once we return from
* the irq or softirq.
*
* Otherwise we wake up ksoftirqd to make sure we
* schedule the softirq soon.
*/
if (!in_interrupt() && should_wake_ksoftirqd())
wakeup_softirqd();
}

调用__raise_softirq_irqoff 函数,如下:

1
2
3
4
5
6
void __raise_softirq_irqoff(unsigned int nr)
{
lockdep_assert_irqs_disabled();
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);
}

把宏 or_softirq_pending 展开以后是:

1
irq_stat[smp processor_id()].softirq_pending |= (1UL << nr);
  • __raise_softirq_irqoff 函数设定本 CPU 上的__softirq_pending 的某个 bit 等于 1,具体的 bit 是由 soft irq number(nr 参数)指定的。
  • 如果在中断上下文,我们只要 set __softirq_pending 的某个 bit 就 OK 了,在中断返回的时候自然会进行软中断的处理。但是,如果在 context 上下文调用这个函数的时候,我们必须要调用 wakeup_softirqd 函数用来唤醒本 CPU 上的 softirqd 这个内核线程。

这样 softirq 就相当于准备好了,在合适的时机将会调用 softirq 的处理函数。

执行软中断

内核执行软中断的地方如下。

  • 在中断处理程序的后半部分执行软中断,对执行时间有限制:不能超过 2 毫秒,并且最多执行 10 次。
  • 每个处理器有一个软中断线程,调度策略是 SCHED_NORMAL,优先级是 120。
  • 开启软中断的函数 local_bh_enable()。

如果开启了强制中断线程化的配置宏 CONFIG_IRO_FORCED_THREADING,并且在引导内核的时候指定内核参数“threadirqs”,那么所有软中断由软中断线程执行

中断处理程序执行软中断

在中断处理程序的后半部分,调用函数 irq_exit() 以退出中断上下文,处理软中断,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<kernel/softirq.c>
void irq_exit(void)
{
__irq_exit_rcu();
rcu_irq_exit();
/* must be last! */
lockdep_hardirq_exit();
}
...
static inline void __irq_exit_rcu(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
local_irq_disable();
#else
lockdep_assert_irqs_disabled();
#endif
account_hardirq_exit(current);
preempt_count_sub(HARDIRQ_OFFSET);
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();

tick_irq_exit();
}

如果 in_interrupt() 为真,表示在不可屏蔽中断、硬中断或软中断上下文,或者禁止软中断。如果正在处理的硬中断没有抢占正在执行的软中断,没有禁止软中断,并且当前处理器的待处理软中断位图不是空的,那么调用函数 invoke_softirq() 来处理软中断。

函数 invoke_softirq 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<kernel/softirq.c>
static inline void invoke_softirq(void)
{
/* 1. 如果软中断线程处于就绪状态或运行状态,那么让软中断线程执行软中断 */
if (ksoftirqd_running(local_softirq_pending()))
return;

/* 2. 如果没有强制中断线程化,那么调用函数_do_softirq() 执行软中断 */
if (!force_irqthreads || !__this_cpu_read(ksoftirqd)) {
#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
/*
* We can safely execute softirq on the current stack if
* it is the irq stack, because it should be near empty
* at this stage.
*/
__do_softirq();
#else
/*
* Otherwise, irq_exit() is called on the task stack that can
* be potentially deep already. So call softirq in its own stack
* to prevent from any overrun.
*/
do_softirq_own_stack();
#endif
} else {
/* 3. 如果强制中断线程化,那么唤醒软中断线程执行软中断*/
wakeup_softirqd();
}
}

函数_do_softirq 是执行软中断的核心函数,其主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<kernel/softirq.c>
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
unsigned long old_flags = current->flags;
int max_restart = MAX_SOFTIRQ_RESTART;
struct softirq_action *h;
bool in_hardirq;
__u32 pending;
int softirq_bit;

current->flags &= ~PF_MEMALLOC;

/* 1. 把局部变量 pending 设置为当前处理器的待处理软中断位图 */
pending = local_softirq_pending();

/* 2. softirq_handle_begin 调用__local_bh_disable_ip 把抢占计数器的软中断计数加 1 */
softirq_handle_begin();
in_hardirq = lockdep_softirq_start();
account_softirq_enter(current);

restart:
/* 3. 把当前处理器的待处理软中断位图重新设置为 0 */
set_softirq_pending(0);

/* 4. 开启硬中断 */
local_irq_enable();

h = softirq_vec;

/* 5. 从低位向高位扫描待处理软中断位图,针对每个设置了对应位的转中断编号,执行软中断的处理函数*/
while ((softirq_bit = ffs(pending))) {
unsigned int vec_nr;
int prev_count;

h += softirq_bit - 1;

vec_nr = h - softirq_vec;
prev_count = preempt_count();

kstat_incr_softirqs_this_cpu(vec_nr);

trace_softirq_entry(vec_nr);
/* 调用 action */
h->action(h);
trace_softirq_exit(vec_nr);
if (unlikely(prev_count != preempt_count())) {
pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
vec_nr, softirq_to_name[vec_nr], h->action,
prev_count, preempt_count());
preempt_count_set(prev_count);
}
h++;
pending >>= softirq_bit;
}

if (!IS_ENABLED(CONFIG_PREEMPT_RT) &&
__this_cpu_read(ksoftirqd) == current)
rcu_softirq_qs();

/* 6. 禁止硬中断 */
local_irq_disable();

/* 7. 如果软中断的处理函数又触发软中断,处理如下 */
pending = local_softirq_pending();
if (pending) {
/* 8. 如果软中断的执行时间小于 2 毫秒,不需要重新调度进程,并口且软中断的执行次数没超过 10,那么跳转到 restart 继续执行软中断 */
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;

/* 9. 唤醒软中断线程执行软中断 */
wakeup_softirqd();
}

account_softirq_exit(current);
lockdep_softirq_end(in_hardirq);
/* 10. 把抢占计数器的软中断计数减 1 */
softirq_handle_end();
current_restore_flags(old_flags, PF_MEMALLOC);
}

上面就是软中断的调用流程。__do_softirq() 是紧接着"hardirq"执行的,它也是运行在中断上下文,如果非要和“hardirq 上下文”有所区分的话,可以认为这是“softirq 上下文”,在 softirq 上下文中,也是不能睡眠的。

软中断线程

每个处理器有一个软中断线程,名称是“ksofirqd/”后面跟着处理器编号,调度策略 SCHED_NORMAL,优先级是 120。软中断线程的核心函数是 run_ksoftirqd(),其代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<kernel/softirq.c>
static void run_ksoftirqd(unsigned int cpu)
{
ksoftirqd_run_begin();
if (local_softirq_pending()) {
/*
* We can safely run softirq on inline stack, as we are not deep
* in the task stack here.
*/
__do_softirq();
ksoftirqd_run_end();
cond_resched();
return;
}
ksoftirqd_run_end();
}
...
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",
};
static __init int spawn_ksoftirqd(void)
{
cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL,
takeover_tasklets);
BUG_ON(smpboot_register_percpu_thread(&softirq_threads));

return 0;
}
early_initcall(spawn_ksoftirqd);
...
static int __smpboot_create_thread(struct smp_hotplug_thread *ht, unsigned int cpu)
{
struct task_struct *tsk = *per_cpu_ptr(ht->store, cpu);
struct smpboot_thread_data *td;
...
tsk = kthread_create_on_cpu(smpboot_thread_fn, td, cpu,
ht->thread_comm);
if (IS_ERR(tsk)) {
kfree(td);
return PTR_ERR(tsk);
}
kthread_set_per_cpu(tsk, cpu);
...
return 0;
}

这里创建一个线程,然后线程中执行 run_ksoftirqd 函数,run_ksoftirqd 函数里执行__do_softirq() 函数。

抢占计数器

每个进程的 thread_info 结构体有一个抢占计数器:int preempt_count,它用来表示当前进程能不能被抢占。

抢占是指当进程在内核模式下运行的时候可以被其他进程抢占,如果优先级更高的进程处于就绪状态,强行剥夺当前进程的处理器使用权。

但是有时候进程可能在执行一些关键操作,不能被抢占,所以内核设计了抢占计数器。如果抢占计数器为 0,表示可以被抢占;如果抢占计数器不为 0,表示不能被抢占。

当中断处理程序返回的时候,如果进程在被打断的时候正在内核模式下执行,就会检查抢占计数器是否为 0。如果抢占计数器是 0,可以让优先级更高的进程抢占当前进程虽然抢占计数器不为 0 意味着禁止抢占,但是内核进一步按照各种场景对抢占计数器的位进行了划分,

其中第 0 ~ 7 位是抢占计数,第 8 ~ 15 位是软中断计数,第 16 ~ 19 位是硬中断计数第 20 位是不可屏蔽中断(Non Maskable Interrupt,NMI)计数。

1
2
3
4
5
6
7
8
9
#define PREEMPT_MASK	(__IRQ_MASK(PREEMPT_BITS) << PREEMPT_SHIFT)
#define SOFTIRQ_MASK (__IRQ_MASK(SOFTIRQ_BITS) << SOFTIRQ_SHIFT)
#define HARDIRQ_MASK (__IRQ_MASK(HARDIRQ_BITS) << HARDIRQ_SHIFT)
#define NMI_MASK (__IRQ_MASK(NMI_BITS) << NMI_SHIFT)
...
#define PREEMPT_BITS 8
#define SOFTIRQ_BITS 8
#define HARDIRQ_BITS 4
#define NMI_BITS 4

各种场景分别利用各自的位禁止或开启抢占。

  • 普通场景(PREEMPT_MASK):对应函数 preempt_disable() 和 preempt_enable()
  • 软中断场景(SOFTIRO_MASK):对应函数 local_bh_disable() 和 local_bh_enabe()
  • 硬中断场景(HARDIRQ_MASK):对应函数 _irq_enter() 和_irq_exit()
  • 不可屏蔽中断场景(NMI MASK):对应函数 nmi_enter() 和 nmi_exit()

反过来,我们可以通过抢占计数器的值判断当前处在什么场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<include/linux/preempt.h>
/*
* Macros to retrieve the current execution context:
*
* in_nmi() - We're in NMI context
* in_hardirq() - We're in hard IRQ context
* in_serving_softirq() - We're in softirq context
* in_task() - We're in task context
*/
#define in_nmi() (nmi_count())
#define in_hardirq() (hardirq_count())
#define in_serving_softirq() (softirq_count() & SOFTIRQ_OFFSET)
#define in_task() (!(in_nmi() | in_hardirq() | in_serving_softirq()))

/*
* The following macros are deprecated and should not be used in new code:
* in_irq() - Obsolete version of in_hardirq()
* in_softirq() - We have BH disabled, or are processing softirqs
* in_interrupt() - We're in NMI,IRQ,SoftIRQ context or have BH disabled
*/
#define in_irq() (hardirq_count())
#define in_softirq() (softirq_count())
#define in_interrupt() (irq_count())
  • in_irq() 表示硬中断场景,也就是正在执行硬中断
  • in_softirq() 表示软中断场景,包括禁止软中断和正在执行软中断
  • in_interrupt() 表示正在执行不可屏蔽中断、硬中断或软中断,或者禁止软中断
  • in_serving_softirq() 表示正在执行软中断
  • in_nmi() 表示不可屏蔽中断场景
  • in_task() 表示普通场景,也就是进程上下文

禁止/开启软中断

如果进程和软中断可能访问同一个对象,那么进程和软中断需要互斥,进程需要禁止软中断。禁止软中断的函数是 local_bh_disable(),注意:这个函数只能禁止本处理器的软中断,不能禁止其他处理器的软中断。该函数把抢占计数器的软中断计数加 2,其代码如下:

1
2
3
4
5
6
static inline void local_bh_disable(void)
{
__local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}
...
#define SOFTIRQ_DISABLE_OFFSET (2 * SOFTIRQ_OFFSET)

调用__local_bh_disable_ip 函数:

1
2
3
4
5
static __always_inline void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
{
preempt_count_add(cnt);
barrier();
}

开启软中断的函数是 local_bh_enable(),该函数把抢占计数器的软中断计数减 2。为什么禁止软中断的函数 local_bh_disable() 把抢占计数器的软中断计数加 2,而不是加 1 呢?目的是区分禁止软中断和正在执行软中断这两种情况。执行软中断的函数__do_sofir() 把抢占计数器的软中断计数加 1。如果软中断计数是奇数,可以确定正在执行软中断。

参考文献

http://www.wowotech.net/irq_subsystem/soft-irq.html

https://zhuanlan.zhihu.com/p/80371745

《Linux 内核深度解析》