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, &param);

	/* 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,针对以下两种情形:

  1. 在硬件中断禁用的上下文抛出 softirq
  2. 在硬件中断使能的上下文抛出 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 类型的细粒度锁。把锁的粒度细化,这也是常见的优化不必要的锁竞争的技巧之一。

posted @ 2026-03-18 11:26  JiMoKuangXiangQu  阅读(29)  评论(0)    收藏  举报