Linunx RT 内核 (8):软中断, 中断底半部处理
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. RT 内核下 softirq 的线程化
在应用了 PREEMPT_RT 补丁后,Linux 内核(后文统称 RT 内核)的 softirq、或者说中断底半部,统一作线程化处理,目的的是将更多的代码逻辑放到调度器的控制下,以保证更快的响应实时优先级进程。
2.1 处理 softirq 专用的内核线程
来看下 RT 内核下 softirq、或者说中断底半部 线程化的细节(本文基于 Linux 4.14.336 + patches-4.14.336-rt159 分析):
// kernel/softirq.c
/*
* Exit an interrupt context. Process softirqs if needed and possible:
*/
void irq_exit(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
local_irq_disable();
#else
WARN_ON_ONCE(!irqs_disabled());
#endif
...
preempt_count_sub(HARDIRQ_OFFSET);
/* 非硬件中断上下文 && 当前 CPU 有挂起的 softirq 待处理 */
if (!in_interrupt() && local_softirq_pending())
invoke_softirq(); /* 处理 softirq */
...
}
DEFINE_PER_CPU(struct task_struct *, ksoftirqd);
#ifdef CONFIG_PREEMPT_RT_FULL
#define TIMER_SOFTIRQS ((1 << TIMER_SOFTIRQ) | (1 << HRTIMER_SOFTIRQ))
DEFINE_PER_CPU(struct task_struct *, ktimer_softirqd);
#endif
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.setup = ksoftirqd_set_sched_params,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",
};
#ifdef CONFIG_PREEMPT_RT_FULL
static struct smp_hotplug_thread softirq_timer_threads = {
.store = &ktimer_softirqd,
.setup = ktimer_softirqd_set_sched_params,
.cleanup = ktimer_softirqd_clr_sched_params,
.thread_should_run = ktimer_softirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ktimersoftd/%u",
};
#endif
static inline void invoke_softirq(void)
{
#ifndef CONFIG_PREEMPT_RT_FULL
...
#else /* PREEMPT_RT_FULL */
unsigned long flags;
/*
* RT 内核下, 唤醒 ksoftirqd 处理 softirq, 即所有的
* softirq (bottom half) 都在线程上下文处理.
*
* 两组 per-cpu softirq 处理线程:
* . 一组是普通 kthread 线程优先级, 处理非 timer 的 softirq
* . 一组是实时 kthread 线程优先级, SCHED_FIFO 调度策略,
* 用来处理 hrtimer/timer
*/
local_irq_save(flags);
if (__this_cpu_read(ksoftirqd) &&
__this_cpu_read(ksoftirqd)->softirqs_raised)
wakeup_softirqd(); /* 处理非 timer 的 softirq */
if (__this_cpu_read(ktimer_softirqd) &&
__this_cpu_read(ktimer_softirqd)->softirqs_raised)
wakeup_timer_softirqd(); /* 只用来处理 hrtimer/timer */
local_irq_restore(flags);
#endif
}
static void wakeup_softirqd(void)
{
/* Interrupts are disabled: no need to stop preemption */
struct task_struct *tsk = __this_cpu_read(ksoftirqd);
if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}
#ifdef CONFIG_PREEMPT_RT_FULL
static void wakeup_timer_softirqd(void)
{
/* Interrupts are disabled: no need to stop preemption */
struct task_struct *tsk = __this_cpu_read(ktimer_softirqd);
if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}
从上面的代码可以看到,有 2 组每 CPU 的内核线程,用来处理 softirq:
- 每 CPU 的内核线程
ksoftirqd,用于处理除 timer 外的 softirq - 每 CPU 的内核线程
ktimer_softirqd,专用于处理 timer 的 softirq
2 组每 CPU 的 softirq 内核线程的入口都是 run_ksoftirqd(),它们通过各自的 .setup 和 .thread_should_run 回调决定了处理 timer softirq 还是非 timer softirq:
// ksoftirqd 内核线程 处理除 timer softirq 外的其它 softirq
static inline void ksoftirqd_set_sched_params(unsigned int cpu)
{
/* Take over all but timer pending softirqs when starting */
local_irq_disable();
/* ksoftirqd 内核线程将 timer softirq 排除在外 */
current->softirqs_raised = local_softirq_pending() & ~TIMER_SOFTIRQS;
local_irq_enable();
}
static int ksoftirqd_should_run(unsigned int cpu)
{
return ksoftirqd_softirq_pending();
}
static inline int ksoftirqd_softirq_pending(void)
{
return current->softirqs_raised;
}
// ktimer_softirqd 内核线程 只处理 timer softirq
static inline void ktimer_softirqd_set_sched_params(unsigned int cpu)
{
struct sched_param param = { .sched_priority = 1 };
sched_setscheduler(current, SCHED_FIFO, ¶m);
/* Take over timer pending softirqs when starting */
local_irq_disable();
/* ktimer_softirqd 内核线程 只处理 timer softirq */
current->softirqs_raised = local_softirq_pending() & TIMER_SOFTIRQS;
local_irq_enable();
}
static int ktimer_softirqd_should_run(unsigned int cpu)
{
return current->softirqs_raised;
}
这里牵涉到内核线程的一些基础知识,如果对此不了解的读者可参考 Linux:内核线程简析。
虽然 RT 内核下创建 ksoftirqd 和 ktimer_softirqd 内核线程专门用来 softirq 事件,但并非所有的 softirq 事件都在这两组线程下处理,还有一种情形,softirq 可能任意当前进程上下文处理。在讨论这种情形之前,先看下 RT 内核下 softirq 的抛出过程。
RT 内核下 softirq 的抛出有两组主要 API,针对以下两种情形:
- 在
硬件中断禁用的上下文抛出 softirq - 在
硬件中断使能的上下文抛出 softirq
先看情形 1. 抛出 softirq 的主要 API:
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags); /* 禁用中断 */
raise_softirq_irqoff(nr);
local_irq_restore(flags); /* 重启中断 */
}
可以看到,raise_softirq() 在抛出 softirq 期间禁用硬件中断,raise_softirq_irqoff() 调用位于情形 2. 之下;同时,raise_softirq_irqoff() 正是情形 2. 的主要 API,所以只需要分析 raise_softirq_irqoff() 就等于分析了 softirq 抛出的所有典型场景,来看 raise_softirq_irqoff() 的逻辑:
/*
* This function must run with irqs disabled!
*/
/* 在 [当前 CPU 中断禁用的上下文] 抛出 softirq @nr */
void raise_softirq_irqoff(unsigned int nr)
{
do_raise_softirq_irqoff(nr);
/*
* If we're in an hard interrupt we let irq return code deal
* with the wakeup of ksoftirqd.
*/
/* 如果当前处于硬件中断上下文, 在 irq_exit() 退出时会做 softirq 内核线程
* 唤醒的合适动作, 没有必要在这里做处理, 可以直接返回了 */
if (in_irq())
return;
/*
* If we are in thread context but outside of a bh disabled
* region, we need to wake ksoftirqd as well.
*
* CHECKME: Some of the places which do that could be wrapped
* into local_bh_disable/enable pairs. Though it's unclear
* whether this is worth the effort. To find those places just
* raise a WARN() if the condition is met.
*/
/*
* 如果当前进程的 softirq 处于使能状态, 则不会在当前进程处理 softirq,
* 此时需要唤醒专用的、合适的 softirq 内核线程来处理 softirq @nr.
*/
if (!current->softirq_nestcnt)
wakeup_proper_softirq(nr);
}
#ifndef __ARCH_SET_SOFTIRQ_PENDING
/* 设置 cpu local softirq pengding 状态 */
/* irq_stat[smp_processor_id()].__softirq_pending = x */
#define set_softirq_pending(x) (local_softirq_pending() = (x))
/* irq_stat[smp_processor_id()].__softirq_pending |= x */
#define or_softirq_pending(x) (local_softirq_pending() |= (x))
#endif
static void do_raise_softirq_irqoff(unsigned int nr)
{
unsigned int mask;
mask = 1UL << nr; /* 生成 softirq @nr 的掩码 */
...
/*
* local cpu pending softirq 设置: 告知当前 CPU 有 @nr 类型的 softirq 挂起:
* irq_stat[smp_processor_id()].__softirq_pending |= mask
*/
or_softirq_pending(mask);
/*
* If we are not in a hard interrupt and inside a bh disabled
* region, we simply raise the flag on current. local_bh_enable()
* will make sure that the softirq is executed. Otherwise we
* delegate it to ksoftirqd.
*/
/*
* - 如果当前 [不位于硬中断上下文 && 当前进程的 softirq 置于禁用状态],
* 则将抛出的 softirq @nr 记录到当前进程, 因为是当前进程将自身的 softirq
* 置于禁用状态, 所以进程迟早会再次将自身的 softirq 使能, 在使能时
* 将会触发对进程内抛出的 softirq @nr 的在进程内上下文内处理, 毕竟
* 期间禁用了一段时间的 softirq, 要尽快的处理挂起的 softirq;
* - 反之, 如果 [当前位于硬中断上下文 || 当前进程的 softirq 置于使能状态],
* 1) 如果 [当前位于硬中断上下文], 此函数最后面会记录 softirq @nr 到对应
* 的 k*softirqd 线程, 然后退出 硬中断上下文 时调用 irq_exit() 唤醒对
* 应的 k*softirqd 线程进行处理;
* 2) 如果 当前进程的 softirq 置于使能状态, 抛出的 softirq @nr 仍然会在函
* 数最后记录到对应的 k*softirqd 线程, 然后在某个硬件中断退出的 irq_exit()
* 唤醒对应的 k*softirqd 线程进行处理.
*/
if (!in_irq() && current->softirq_nestcnt)
current->softirqs_raised |= mask;
else if (!__this_cpu_read(ksoftirqd) || !__this_cpu_read(ktimer_softirqd))
return;
/* 告知 k[timer_,]softirqd 挂起的 softirq @nr 要处理 */
if (mask & TIMER_SOFTIRQS) /* 如果是 hrtimer/timer 的 softirq: HRTIMER_SOFTIRQ/TIMER_SOFTIRQS */
/* 告知 ktimer_softirqd 有 hrtimer/timer softirq 要处理 */
__this_cpu_read(ktimer_softirqd)->softirqs_raised |= mask;
else
__this_cpu_read(ksoftirqd)->softirqs_raised |= mask;
}
在 do_raise_softirq_irqoff() 中,current->softirq_nestcnt 和 current->softirqs_raised 正是 RT 内核下的一个重大差异,先看下数据结构:
struct task_struct {
...
#ifdef CONFIG_PREEMPT_RT_BASE
...
int softirq_nestcnt; /* softirq 的使能状态: < 0: BUG; 0:使能; > 0:禁用 */
unsigned int softirqs_raised; /* softirq 禁用期间,进程自身抛出的 softirq */
#endif
...
};
那 current->softirq_nestcnt 是在什么情形下设置的呢?答案是禁用 softirq(或者说 bottom half)时设置:
static inline void local_bh_disable(void)
{
__local_bh_disable();
}
void __local_bh_disable(void)
{
if (++current->softirq_nestcnt == 1)
migrate_disable();
}
void migrate_disable(void)
{
preempt_disable(); /* 先禁用抢占 */
if (++current->migrate_disable == 1) { /* 不是 migrate_disable() 嵌套调用, 即首次调用 */
/*
* pin 到当前 (CPU 的) 运行队列的进程数加 1,
* 递归调用(即 ++current->migrate_disable > 1)不再计数, 因为指向同一进程.
*/
this_rq()->nr_pinned++;
preempt_lazy_disable();
#ifdef CONFIG_SCHED_DEBUG
WARN_ON_ONCE(current->pinned_on_cpu >= 0);
current->pinned_on_cpu = smp_processor_id();
#endif
}
preempt_enable(); /* 再启用抢占 */
}
从上面可以看到,softirq 的禁用操作,不再使用 thread_info::preempt_count,而是使用一个独立的 int 变量 task_struct::softirq_nestcnt,这可以支持更多的嵌套深度而不会溢出。同时,我们注意到,禁用 softirq 期间有一个 migrate_disable(),该函数用来临时禁止进程迁移到其它 CPU,这主要是为了保证进程禁用再启用 softirq 时处理的是其所在的当前 CPU 上自己抛出的 softirq,后面会对此做进一步的描述。
本小节最后,看一个笔者在 NanoPi-NEO-Core 移植 RT 补丁后的,softirq 内核线程的实际情形:
root@NanoPi-NEO-Core:~# uname -a
Linux NanoPi-NEO-Core 4.14.336-rt159 #3 SMP PREEMPT RT Tue Mar 17 13:25:16 CST 2026 armv7l armv7l armv7l GNU/Linux
root@NanoPi-NEO-Core:~# ps -ef | grep -v grep | grep ksoft*
root 8 2 0 16:56 ? 00:00:00 [ksoftirqd/0]
root 23 2 0 16:56 ? 00:00:00 [ksoftirqd/1]
root 31 2 0 16:56 ? 00:00:00 [ksoftirqd/2]
root 39 2 0 16:56 ? 00:00:00 [ksoftirqd/3]
root@NanoPi-NEO-Core:~# ps -ef | grep -v grep | grep ktimersoft*
root 9 2 0 16:56 ? 00:00:00 [ktimersoftd/0]
root 22 2 0 16:56 ? 00:00:00 [ktimersoftd/1]
root 30 2 0 16:56 ? 00:00:00 [ktimersoftd/2]
root 38 2 0 16:56 ? 00:00:00 [ktimersoftd/3]
2.2 任意进程下的 softirq 处理
前面有提到,softirq 的处理并非一定发生在内核专用线程 ksoftirqd 或 ktimer_softirqd 下,也可能发生任意进程的上下文。假定有如下的代码片段:
local_bh_disable();
// 需要禁用 softirq 处理的代码片段。
// 期间抛出了 softirq 事件。
local_bh_enable();
local_bh_disable() 前面已经分析过了,这里不再赘述,重点看 local_bh_enable():
static inline void local_bh_enable(void)
{
__local_bh_enable();
}
void __local_bh_enable(void)
{
if (WARN_ON(current->softirq_nestcnt == 0))
return;
local_irq_disable(); /* 禁用 CPU 上的中断 */
/*
* 如果 softirq 禁用嵌套计数将归 0(即 softirq 将使能)时,
* 如果 softirq 禁用期间当前 CPU 上抛出了 softirq 时间,
* 则处理一波 softirq 事件.
*/
if (current->softirq_nestcnt == 1 && current->softirqs_raised)
do_current_softirqs(); /* 处理当前 CPU 上挂起的 softirq 事件 */
local_irq_enable(); /* 重新使能 CPU 上的中断 */
if (--current->softirq_nestcnt == 0)
migrate_enable(); /* softirq 重新启用时, 使能临时禁用的 CPU 迁移 */
}
2.3 softirq 处理细节
前面两个小节讲述了 softirq 处理所在的上下文,抛出、禁用使能逻辑,但还没有描述 softirq 处理的细节,本小节对此加以描述。
ksoftirqd/ktimer_softirqd内核线程处理 softirq
/* Called with preemption disabled */
static void run_ksoftirqd(unsigned int cpu)
{
local_irq_disable(); /* 禁用中断 */
current->softirq_nestcnt++; /* 禁用 softirq */
do_current_softirqs();
current->softirq_nestcnt--; /* 重启 softirq */
local_irq_enable(); /* 重启中断 */
cond_resched_rcu_qs();
}
- 任意进程上下文处理 softirq
任意进程(非 ksoftirqd/ktimer_softirqd 内核线程上下文)处理 softirq 由 local_bh_enable() 触发(假设 local_bh_enable() 是 softirq_nestcnt 嵌套计数归 0):
local_bh_enable()
__local_bh_enable()
void __local_bh_enable(void)
{
if (WARN_ON(current->softirq_nestcnt == 0))
return;
local_irq_disable(); /* 禁用 CPU 上的中断 */
/*
* 如果 softirq 禁用嵌套计数将归 0(即 softirq 将使能)时,
* 如果 softirq 禁用期间当前 CPU 上抛出了 softirq 时间,
* 则处理一波 softirq 事件.
*/
if (current->softirq_nestcnt == 1 && current->softirqs_raised)
do_current_softirqs(); /* 处理当前 CPU 上挂起的 softirq 事件 */
local_irq_enable(); /* 重新使能 CPU 上的中断 */
if (--current->softirq_nestcnt == 0)
migrate_enable(); /* softirq 重新启用时, 使能临时禁用的 CPU 迁移 */
}
可见,RT 内核下的 softirq 处理都调用同一接口 do_current_softirqs():
/*
* On RT we serialize softirq execution with a cpu local lock per softirq
*/
static DEFINE_PER_CPU(struct local_irq_lock [NR_SOFTIRQS], local_softirq_locks);
void __init softirq_early_init(void)
{
int i;
for (i = 0; i < NR_SOFTIRQS; i++)
local_irq_lock_init(local_softirq_locks[i]);
}
static void lock_softirq(int which)
{
local_lock(local_softirq_locks[which]);
}
static void unlock_softirq(int which)
{
local_unlock(local_softirq_locks[which]);
}
/*
* Called with interrupts disabled. Process softirqs which were raised
* in current context (or on behalf of ksoftirqd).
*/
/* 处理当前 CPU 上挂起的 softirq 事件 */
static void do_current_softirqs(void)
{
while (current->softirqs_raised) {
int i = __ffs(current->softirqs_raised);
unsigned int pending, mask = (1U << i);
current->softirqs_raised &= ~mask;
local_irq_enable(); /* 临时使能中断,好了,现在可以 softirq 处理上下文可以被抢占了 */
...
/*
* If the lock is contended, we boost the owner to
* process the softirq or leave the critical section
* now.
*/
/*
* 由于 softirq 处理在线程中, 且可抢占, 所以对 @i 类型的 softirq
* 上锁, 防止多个进程/线程同时处理同一个 softirq 事件.
*/
lock_softirq(i);
local_irq_disable(); /* 抢到当前 CPU 上 @i 类型的 softirq 锁了, 禁用中断处理一个 @i 类型的 softirq */
softirq_set_runner(i); /* 标记 @i 类型的 softirq 的当前处理 softirq 进程 */
/*
* Check with the local_softirq_pending() bits,
* whether we need to process this still or if someone
* else took care of it.
*/
pending = local_softirq_pending();
if (pending & mask) { /* 只处理当前 CPU 上的 softirq 事件 */
set_softirq_pending(pending & ~mask); /* 清除 cpu 上挂起的 @i 类型的 softirq 事件 */
do_single_softirq(i); /* 处理一个 @i 类型的 softirq */
}
/* 处理 @i 类型的 softirq 事件完毕, 清除 @i 类型的 softirq 的当前处理 softirq 的任务 */
softirq_clr_runner(i);
WARN_ON(current->softirq_nestcnt != 1);
local_irq_enable(); /* 重启中断 */
unlock_softirq(i); /* 解除当前 CPU 对 @i 类型的 softirq 的锁定 */
local_irq_disable(); /* 再次禁用中断,准备处理下一 softirq 事件 */
}
}
这里的 softirq 不光有 per-CPU 的 spinlock(即 local_irq_lock),还有 per-CPU 每 softirq 类型的锁,这样就不会形成一把 per-CPU 的 softirq 大锁,而是针对每个 softirq 类型的细粒度锁。把锁的粒度细化,这也是常见的优化不必要的锁竞争的技巧之一。

浙公网安备 33010602011771号