CHAPTER 06

软中断、tasklet 与定时器

硬中断线程化之后,内核里最大的"隐形高优先级执行体"就是 softirq:它不属于任何线程,却能打断任何线程。本章讲清 softirq 的真实机制、它在非 RT 内核里为什么是延迟大户、PREEMPT_RT 怎样把 local_bh_disable() 改造成一把可抢占的 per-CPU 锁,以及与之紧密相连的定时器(timer_list、hrtimer 硬/软分流、ARM64 arch timer)和 irq_work。

6.1softirq 到底是什么

名字里的 "irq" 纯属历史包袱。softirq 跟 GIC、跟异常向量表、跟任何硬件都没有关系,它的全部家当只有三样:

/* 1. 一张全局的函数指针表,下标就是向量号 */
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

/* 2. 一个 per-CPU 的 pending 位图:irq_stat.__softirq_pending */

/* 3. "触发" 的全部实现 —— 就是一条 or 指令 */
void __raise_softirq_irqoff(unsigned int nr)
{
	lockdep_assert_irqs_disabled();
	trace_softirq_raise(nr);
	or_softirq_pending(1UL << nr);
}

向量是编译期固定的 10 个(include/linux/interrupt.h:557),全树只有核心子系统通过 open_softirq() 占用它们:

#向量注册者做什么
0HI_SOFTIRQkernel/softirq.c高优先级 tasklet
1TIMER_SOFTIRQkernel/time/timer.ctimer_list(时间轮)到期回调
2NET_TX_SOFTIRQnet/core/dev.c发包完成、qdisc 调度
3NET_RX_SOFTIRQnet/core/dev.cNAPI 收包(net_rx_action)
4BLOCK_SOFTIRQblock/blk-mq.c块 I/O 完成
5IRQ_POLL_SOFTIRQlib/irq_poll.c块设备 irq_poll
6TASKLET_SOFTIRQkernel/softirq.c普通 tasklet
7SCHED_SOFTIRQkernel/sched/fair.c负载均衡
8HRTIMER_SOFTIRQkernel/time/hrtimer.csoft 模式的 hrtimer 回调
9RCU_SOFTIRQkernel/rcu/tree.cRCU core / 回调(RT 下不用,见第 7 章)

raise 只是置位,真正执行要等内核在某个"检查点"上主动轮询这个位图。非 RT 内核有三个检查点:

  1. 硬中断返回:__irq_exit_rcu() 发现 !in_interrupt() && local_softirq_pending() 就调 invoke_softirq()(kernel/softirq.c:652)。
  2. 重新打开 BH:local_bh_enable() → __local_bh_enable_ip() 发现有 pending 就 do_softirq()。
  3. ksoftirqd/N 线程:前两处处理不完时的兜底(kernel/softirq.c:944 run_ksoftirqd())。
准确的说法

softirq 不是被"触发"的,而是在中断返回、开 BH 这些固定位置被"轮询到"的。它是一段被插在别人执行流里的回调,没有自己的 task_struct。

执行循环:handle_softirqs()

#define MAX_SOFTIRQ_TIME  msecs_to_jiffies(2)
#define MAX_SOFTIRQ_RESTART 10

static void handle_softirqs(bool ksirqd)
{
	unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
	int max_restart = MAX_SOFTIRQ_RESTART;
	...
	/* 当前任务的上下文是"借"来的 */
	current->flags &= ~PF_MEMALLOC;
	pending = local_softirq_pending();
	softirq_handle_begin();          /* 非 RT: preempt_count += SOFTIRQ_OFFSET */
restart:
	set_softirq_pending(0);
	local_irq_enable();              /* ← 开中断执行,这是"下半部"的全部意义 */

	while ((softirq_bit = ffs(pending))) {   /* 按向量号从小到大 */
		...
		h->action();
		...
	}
	local_irq_disable();

	pending = local_softirq_pending();
	if (pending) {
		if (time_before(jiffies, end) && !need_resched() && --max_restart)
			goto restart;            /* 最多重来 10 轮 / 2ms */
		wakeup_softirqd();           /* 剩下的交给 ksoftirqd */
	}
	...
}

两个关键事实:

  • "借用" current。函数开头那句注释(kernel/softirq.c:546)直说 "the current task context is borrowed for the softirq"。softirq 运行时 current 指向被打断的那个任务——这就是非 RT 下 softirq 不能睡眠的根因:一旦 schedule(),睡下去的是一个毫不知情的无辜任务。
  • 预算是 2ms 或 10 轮。非 RT 内核里,这 2ms 期间被打断的任务——不管它是不是 SCHED_FIFO 99——只能干等。

6.2非 RT 内核:softirq 是"隐形的最高优先级"

把 softirq 放到调度器的视角里看,它的地位非常奇怪:

非 RT:谁能打断谁(从上到下优先级递减) pseudo-NMI(GICv3 优先级 0x80) 硬中断 handler(关中断,不嵌套) softirq / tasklet(irq_exit 或 bh_enable 时就地执行) ≤ 2ms 或 10 轮,借用被打断任务的 current SCHED_FIFO / RR 1–99、SCHED_DEADLINE SCHED_OTHER 任务 ksoftirqd/N 也在这一层(nice 0) softirq 可以挡住任何 RT 任务 2ms 用完后剩下的 softirq 被"降级"到 OTHER 的 ksoftirqd → 被 RT 任务饿死,网络卡顿 两难 就地跑:RT 延迟爆炸 丢给 ksoftirqd: 网络/定时器饿死
非 RT 内核里 softirq 的位置:它在所有任务之上,却又在"超预算"时被扔到最底层的 ksoftirqd

由此产生三类问题:

  1. 不可抢占的执行:softirq 运行时 preempt_count 里带着 SOFTIRQ_OFFSET,任何任务都抢不了它。一个 NET_RX 风暴就能让刚被唤醒的 SCHED_FIFO 99 任务多等将近 2ms——而这还只是"一轮"的上限,下一个中断回来又是一轮。
  2. 不可抢占的保护区:local_bh_disable() 在非 RT 下就是 preempt_count += SOFTIRQ_DISABLE_OFFSET,等价于关抢占。网络栈、块层里大量 spin_lock_bh(),每一处都是一个关抢占区。
  3. 不可调度、不可观测:softirq 没有线程身份,不能 chrt、不能 taskset,top 里只能看到被它占用 CPU 的那个"无辜"任务的 si 时间。

下面是 ARM64 上一个具体场景的调用栈:CPU 正在跑一个 SCHED_FIFO 80 的用户态控制线程,网卡中断进来,中断返回前就地执行 NET_RX。注意 softirq 跑在 per-CPU IRQ 栈上(do_softirq_own_stack),从头到尾那个 RT 线程都插不上手:

非 RT:中断返回时就地执行 NET_RX ARM64 · GICv3 · !PREEMPT_RT
  1. RT 控制线程在 EL0 运行(SCHED_FIFO 80)
  2. VBAR_EL1 + 0x480 → el0t_64_irqarch/arm64/kernel/entry.SCPU 自动保存 ELR/SPSR、置 DAIF.I
  3. el0t_64_irq_handler()entry-common.c:945
  4. __el0_irq_handler_common() → el0_interrupt()entry-common.c:940 / :923
  5. irq_enter_rcu(); do_interrupt_handler()entry-common.c:304切到 per-CPU IRQ 栈
  6. gic_handle_irq() → __gic_handle_irq_from_irqson()irq-gic-v3.c:964 / :904读 ICC_IAR1_EL1 = ACK
  7. generic_handle_domain_irq() → handle_fasteoi_irq() → 网卡 handlerkernel/irq/chip.c:687handler 里 napi_schedule() → raise NET_RX_SOFTIRQ
  8. …… handler 返回,写 ICC_EOIR1_EL1 ……
  9. irq_exit_rcu() → __irq_exit_rcu()kernel/softirq.c:672 / :652preempt_count -= HARDIRQ_OFFSET,发现 pending
  10. invoke_softirq() → do_softirq_own_stack()softirq.c:444 / arch/arm64/kernel/irq.c:84call_on_irq_stack(____do_softirq)
  11. __do_softirq() → handle_softirqs(false)kernel/softirq.c:611 / :536local_irq_enable(),最多 2ms / 10 轮
  12. net_rx_action() → napi_poll() → 驱动 poll → 协议栈net/core/dev.c:7052这段时间 RT 线程完全无法运行
  13. …… softirq 结束 ……
  14. exit_to_user_mode()entry-common.c:176返回用户态前才检查 need_resched
硬件硬中断软中断内核线程/任务用户态锁

6.3RT 的解法:把 BH 变成一把可抢占的 per-CPU 锁

PREEMPT_RT 对 softirq 的改造全部集中在 kernel/softirq.c 的一个 #ifdef CONFIG_PREEMPT_RT 分支里(kernel/softirq.c:106–319),思路一句话:

核心思路

local_bh_disable() 的真实意图是"本 CPU 上的 BH 处理不能和我并发"。这本质上是一把per-CPU 互斥锁,不需要关抢占。RT 就真的给它一把锁:softirq_ctrl.lock,一个 local_lock_t(RT 下 = per-CPU spinlock_t = rtmutex,带优先级继承)。softirq 本身也只在线程上下文、并且持有这把锁时执行。

struct softirq_ctrl {
	local_lock_t	lock;   /* RT: per-CPU spinlock_t → rtmutex,可睡眠、带 PI */
	int		cnt;    /* 本 CPU 上 BH-disabled 嵌套计数 */
};

static DEFINE_PER_CPU(struct softirq_ctrl, softirq_ctrl) = {
	.lock	= INIT_LOCAL_LOCK(softirq_ctrl.lock),
};

local_bh_disable() 的 RT 实现

void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
{
	WARN_ON_ONCE(in_hardirq());
	lock_map_acquire_read(&bh_lock_map);

	/* 本任务第一次进入 BH-disabled 区? */
	if (!current->softirq_disable_cnt) {
		if (preemptible()) {
			local_lock(&softirq_ctrl.lock);   /* migrate_disable + spin_lock(rtmutex) */
			/* Required to meet the RCU bottomhalf requirements. */
			rcu_read_lock();
		} else {
			DEBUG_LOCKS_WARN_ON(this_cpu_read(softirq_ctrl.cnt));
		}
	}

	newcnt = __this_cpu_add_return(softirq_ctrl.cnt, cnt);
	current->softirq_disable_cnt = newcnt;   /* 双计数:per-CPU + per-task */
	...
}

逐行看它和非 RT 版本(kernel/softirq.c:328,就是 __preempt_count_add(cnt))的区别:

  • 不碰 preempt_count。BH-disabled 区间仍然可抢占、可睡眠。
  • 只有最外层拿锁。current->softirq_disable_cnt 为 0 才去 local_lock(),嵌套的 local_bh_disable() 只加计数,避免对同一把锁递归。
  • 顺手 rcu_read_lock()。非 RT 下 BH-disabled 天然是一个 RCU-bh 读侧(因为关了抢占);RT 下不关抢占了,就显式补一个读侧临界区,维持"BH 关闭区也是 RCU 读侧"的老语义。
  • 双计数。注释(kernel/softirq.c:108)说明了分工:per-task 的 softirq_disable_cnt 给 softirq_count()/in_softirq() 用——因为这些查询只在持锁任务正在运行时才有意义;per-CPU 的 softirq_ctrl.cnt 用来判断"本 CPU 是否有人正处于 BH-disabled 区",避免持锁任务被抢占/阻塞时无谓地唤醒 ksoftirqd。

local_bh_enable():谁关的 BH,谁来跑 softirq

void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
	bool preempt_on = preemptible();
	...
	local_irq_save(flags);
	curcnt = __this_cpu_read(softirq_ctrl.cnt);

	if (curcnt != cnt)            /* 不是最外层,不跑 */
		goto out;

	pending = local_softirq_pending();
	if (!pending)
		goto out;

	if (!preempt_on) {            /* 在关抢占区里开 BH:只能交给 ksoftirqd */
		wakeup_softirqd();
		goto out;
	}

	/* 把计数调成 SOFTIRQ_OFFSET,使 in_serving_softirq() 为真,然后就地处理 */
	cnt = SOFTIRQ_OFFSET;
	__local_bh_enable(cnt, false);
	__do_softirq();

out:
	__local_bh_enable(cnt, preempt_on);   /* 最外层:rcu_read_unlock + local_unlock */
	local_irq_restore(flags);
}

这就是 RT 下 softirq 的第一个执行点:在打开 BH 的那个任务的上下文里、仍然持有 softirq_ctrl.lock 时执行。这个任务是一个真正的、可被抢占的线程,因此 softirq 处理也变得可抢占。

硬中断返回时:只唤醒,不执行

static inline bool should_wake_ksoftirqd(void)
{
	return !this_cpu_read(softirq_ctrl.cnt);
}

static inline void invoke_softirq(void)
{
	if (should_wake_ksoftirqd())
		wakeup_softirqd();
}

第二个执行点是 ksoftirqd。注意 should_wake_ksoftirqd() 的条件:如果本 CPU 上有任务正处于 BH-disabled 区(cnt != 0),就连 ksoftirqd 都不唤醒——因为那个任务在 local_bh_enable() 时自然会把 pending 的 softirq 处理掉;此时唤醒 ksoftirqd 它也只会阻塞在 softirq_ctrl.lock 上,白白多一次上下文切换。

ksoftirqd 自己也走同一把锁:

static inline void ksoftirqd_run_begin(void)
{
	__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);   /* 拿 softirq_ctrl.lock */
	local_irq_disable();
}
flowchart TD
  R["raise_softirq: pending 位图置位"] --> Q{"在哪个上下文 raise?"}
  Q -->|"硬中断里
如 arch timer"| HI["irq_exit_rcu → invoke_softirq"] Q -->|"任务上下文, BH 已关
如 irq 线程里的 napi_schedule"| BH["记在 pending,等 local_bh_enable"] Q -->|"任务上下文, BH 开着"| W1["raise_softirq_irqoff 直接 wakeup_softirqd"] HI --> C{"softirq_ctrl.cnt == 0 ?"} C -->|"是: 本 CPU 没人关 BH"| KS["唤醒 ksoftirqd/N"] C -->|"否: 有任务在 BH 区内"| BH BH --> EN["该任务 local_bh_enable
最外层 + 可抢占"] EN --> RUN1["在该任务上下文执行 __do_softirq
仍持有 softirq_ctrl.lock"] W1 --> KS KS --> RUN2["ksoftirqd: ksoftirqd_run_begin 拿锁
handle_softirqs"] classDef softirq fill:#fbf0d9,stroke:#b7791f,color:#1d1d1f classDef task fill:#e1ecf8,stroke:#2563a8,color:#1d1d1f classDef hardirq fill:#fde8da,stroke:#c2410c,color:#1d1d1f class HI,C hardirq class RUN1,RUN2,EN,KS task class R,BH,W1 softirq
PREEMPT_RT 下 softirq 的两条执行路径:要么在"关 BH 的那个线程"的 local_bh_enable 里,要么在 ksoftirqd 里——永远不在硬中断返回路径上

"谁 raise 谁执行":irq 线程里的 NET_RX

RT 下所有设备中断都被强制线程化(第 5 章),而强制线程化的线程函数恰好包了一层 BH:

static irqreturn_t
irq_forced_thread_fn(struct irq_desc *desc, struct irqaction *action)
{
	irqreturn_t ret;

	local_bh_disable();
	if (!IS_ENABLED(CONFIG_PREEMPT_RT))
		local_irq_disable();
	ret = action->thread_fn(action->irq, action->dev_id);
	...
	irq_finalize_oneshot(desc, action);
	if (!IS_ENABLED(CONFIG_PREEMPT_RT))
		local_irq_enable();
	local_bh_enable();          /* ← 在 handler 里 raise 的 softirq 就在这里执行 */
	return ret;
}

于是网卡 handler 在 irq 线程里 napi_schedule() raise 的 NET_RX_SOFTIRQ,会在同一个 irq 线程的 local_bh_enable() 里被处理——收包工作继承了这个 irq 线程的调度优先级和 CPU 亲和性。这是 RT 设计里很漂亮的一点:调 irq/NN-eth0 的 chrt 优先级,就同时调了这块网卡的整个收包路径。

RT:网卡中断线程中执行 NET_RX ARM64 · PREEMPT_RT
  1. ret_from_fork → kthread()arch/arm64/kernel/entry.S / kernel/kthread.c
  2. irq_thread()kernel/irq/manage.c:1300irq/NN-eth0,SCHED_FIFO 50,被 primary handler 唤醒
  3. irq_thread_fn → irq_forced_thread_fn()kernel/irq/manage.c:1191
  4. local_bh_disable() → __local_bh_disable_ip()kernel/softirq.c:156local_lock(&softirq_ctrl.lock) + rcu_read_lock()
  5. 驱动原 handler → napi_schedule() → __raise_softirq_irqoff(NET_RX)只是置 pending 位
  6. irq_finalize_oneshot()unmask 中断线
  7. local_bh_enable() → __local_bh_enable_ip()kernel/softirq.c:218最外层且可抢占 → 就地处理
  8. __do_softirq() → handle_softirqs(false)kernel/softirq.c:611 / :536in_serving_softirq() 为真,但仍是普通线程上下文
  9. net_rx_action() → napi_poll() → 驱动 pollnet/core/dev.c:7052可以被更高优先级的 RT 任务随时抢占
  10. __local_bh_enable(cnt, true)rcu_read_unlock + local_unlock

BH 变成锁之后的新问题

"local_bh_disable 是一把 per-CPU 锁"带来了一个非 RT 下不存在的现象:同一 CPU 上的所有 BH 区间互相串行。

sequenceDiagram
  participant L as 低优先级任务 L
  participant K as softirq_ctrl.lock
  participant H as 高优先级任务 H
  L->>K: local_bh_disable 拿到锁
  Note over L: 在 BH 区内运行
  H-->>L: H 被唤醒并抢占 L
  H->>K: spin_lock_bh 也要 local_bh_disable
  K-->>H: 锁被 L 持有, H 阻塞
  Note over K,L: rtmutex 优先级继承, L 被提升到 H 的优先级
  L->>K: 跑完 BH 区, local_bh_enable 释放
  K->>H: 交给 H, H 继续
    
RT 下 BH 区可以被抢占,但同 CPU 另一个任务再关 BH 时会在 softirq_ctrl.lock 上等待,由 PI 保证等待有界
  • 收益:BH 区间、softirq 处理都可被抢占;最坏情况下高优先级任务只需等待持锁者跑完它的 BH 区(且持锁者被 PI 提升),而不是等一整轮 2ms 的 softirq。
  • 代价:它是一把"BH 大锁"——同 CPU 上两个毫不相关的子系统(比如网络和块层)的 BH 区间也会互相等待。这也是 6.12 之后社区继续给网络栈引入 per-子系统锁、试图拆掉这把大锁的原因(后续演进,不在 6.12 中)。
  • 语义变化:in_softirq()、softirq_count() 在 RT 下读的是 current->softirq_disable_cnt,只描述"当前任务"的状态。另外 local_bh_disable() 不再隐含关抢占——ARM64 的 FPSIMD 代码正是因此要改(见第 8 章)。
ksoftirqd 的调度策略

ksoftirqd 由 smpboot 框架创建(kernel/softirq.c:992),6.12 里没有任何代码把它设成 RT 策略,所以默认就是 SCHED_OTHER。在 RT 内核上,从硬中断上下文 raise 的 softirq(最典型的是 arch timer 中断里 raise 的 TIMER_SOFTIRQ 与 HRTIMER_SOFTIRQ)都要等 ksoftirqd 被调度——如果 CPU 被 RT 任务占满,timer_list 回调和 soft hrtimer 就会被推迟。6.12 之后主线为此增加了独立的 ktimers/N 线程,6.12 里还没有。实践中常见的做法是用 chrt 把 ksoftirqd 调成低优先级的 FIFO(见第 10 章)。

6.4tasklet 在 RT 下

tasklet 不是 softirq 的替代品,而是挂在 HI_SOFTIRQ / TASKLET_SOFTIRQ 两个向量上的链表(kernel/softirq.c:935–936)。它和 softirq 的本质区别是同一个 tasklet 不会在多个 CPU 上并行,靠 TASKLET_STATE_RUN 位实现。RT 对它有两处修改:

#if defined(CONFIG_SMP) || defined(CONFIG_PREEMPT_RT)
static inline int tasklet_trylock(struct tasklet_struct *t)
{
	return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
}

PREEMPT_RT 和 SMP 并列:单核系统原本不需要这把位锁,但 RT 下 softirq 可被抢占,单核上也可能出现同一 tasklet 的两个执行实例交错。

void tasklet_unlock_spin_wait(struct tasklet_struct *t)
{
	while (test_bit(TASKLET_STATE_RUN, &(t)->state)) {
		if (IS_ENABLED(CONFIG_PREEMPT_RT)) {
			/*
			 * Prevent a live lock when current preempted soft
			 * interrupt processing or prevents ksoftirqd from
			 * running. If the tasklet runs on a different CPU
			 * then this has no effect other than doing the BH
			 * disable/enable dance for nothing.
			 */
			local_bh_disable();
			local_bh_enable();
		} else {
			cpu_relax();
		}
	}
}

非 RT 下自旋等待别的 CPU 跑完即可;RT 下"正在跑这个 tasklet 的"可能是被当前任务抢占的、优先级更低的线程——原地自旋就是活锁。改成走一遍 local_bh_disable()/enable():这会去拿 softirq_ctrl.lock,如果锁被那个低优先级线程持有,就阻塞并通过 PI 把它提上来跑完。

一个普遍规律

"自旋等待"模式在 RT 下普遍失效:等待对象从"另一个 CPU 上正在跑的代码"变成了"一个可能被你抢占了的线程"。RT 的标准解法是把自旋换成在那个执行者持有的锁上阻塞,让 PI 去解决问题。下文 timer 的 expiry_lock 是同一个模式。

tasklet 在 6.12 中已明确标为"Do not use in new code"(include/linux/interrupt.h:737),替代品是 6.9 引入的 BH workqueue(system_bh_wq / system_bh_highpri_wq,include/linux/workqueue.h:468),或者直接用线程化中断。

6.5定时器:两套机制,一个分流开关

内核有两套定时器:

timer_list(时间轮)hrtimer(高精度)
精度jiffies(1/HZ)纳秒,由 clockevent 编程
数据结构分级时间轮,O(1) 插入per-CPU 红黑树(timerqueue)
典型用途超时(网络重传、watchdog)——大多数会在到期前被取消nanosleep、POSIX timer、调度器 tick、hrtick、timerfd
非 RT 回调上下文TIMER_SOFTIRQ硬中断(默认)或 HRTIMER_SOFTIRQ(_SOFT 模式)
RT 回调上下文TIMER_SOFTIRQ → ksoftirqd默认改成 soft,只有显式 _HARD 的留在硬中断

hrtimer 的硬/软分流

每个 CPU 的 hrtimer_cpu_base 有 8 个 clock base(MONOTONIC、REALTIME、BOOTTIME、TAI 各一份 hard + 一份 soft)。到期掩码也一分为二:

#define HRTIMER_ACTIVE_HARD	((1U << MASK_SHIFT) - 1)
#define HRTIMER_ACTIVE_SOFT	(HRTIMER_ACTIVE_HARD << MASK_SHIFT)

一个 hrtimer 进哪半边,由初始化时决定:

static void __hrtimer_init(struct hrtimer *timer, clockid_t clock_id,
			   enum hrtimer_mode mode)
{
	bool softtimer = !!(mode & HRTIMER_MODE_SOFT);
	...
	/*
	 * On PREEMPT_RT enabled kernels hrtimers which are not explicitly
	 * marked for hard interrupt expiry mode are moved into soft
	 * interrupt context for latency reasons and because the callbacks
	 * can invoke functions which might sleep on RT, e.g. spin_lock().
	 */
	if (IS_ENABLED(CONFIG_PREEMPT_RT) && !(mode & HRTIMER_MODE_HARD))
		softtimer = true;
	...
	base = softtimer ? HRTIMER_MAX_CLOCK_BASES / 2 : 0;
	timer->is_soft = softtimer;
	timer->is_hard = !!(mode & HRTIMER_MODE_HARD);
	...
}

注释给了两个理由:延迟(回调不再在硬中断里占用 CPU)和正确性(RT 下回调里的 spin_lock() 可能睡眠,硬中断里不能睡)。反过来,凡是写了 HRTIMER_MODE_*_HARD 的地方,作者都在承诺"这个回调里只用 raw 锁、执行时间极短"。6.12 中这样的显式用户包括:

  • 调度 tick:tick_sched_timer 用 HRTIMER_MODE_ABS_HARD 初始化(kernel/time/tick-sched.c:1582),回调 tick_nohz_handler()(tick-sched.c:284)。
  • 调度器自身的 hrtick、RT 带宽、deadline 定时器(kernel/sched/)。
  • ARM64 KVM 的虚拟定时器:arch/arm64/kvm/arch_timer.c:993 / :1021(HRTIMER_MODE_ABS_HARD)。
  • osnoise/timerlat tracer 的测量定时器(kernel/trace/trace_osnoise.c)。

RT 任务的 sleep:自动走硬中断

用户态 RT 程序最依赖的路径是 clock_nanosleep()——cyclictest 测的就是它。它用的是 hrtimer_sleeper,对它 RT 做了一个特判:

static void __hrtimer_init_sleeper(struct hrtimer_sleeper *sl,
				   clockid_t clock_id, enum hrtimer_mode mode)
{
	/*
	 * ... The hrtimer_sleeper callback is RT compatible in hard interrupt
	 * context, but there is a latency concern: Untrusted userspace can
	 * spawn many threads which arm timers for the same expiry time on
	 * the same CPU. That causes a latency spike due to the wakeup of
	 * a gazillion threads.
	 *
	 * OTOH, privileged real-time user space applications rely on the
	 * low latency of hard interrupt wakeups. If the current task is in
	 * a real-time scheduling class, mark the mode for hard interrupt
	 * expiry.
	 */
	if (IS_ENABLED(CONFIG_PREEMPT_RT)) {
		if (rt_or_dl_task_policy(current) && !(mode & HRTIMER_MODE_SOFT))
			mode |= HRTIMER_MODE_HARD;
	}

	__hrtimer_init(&sl->timer, clock_id, mode);
	sl->timer.function = hrtimer_wakeup;
	sl->task = current;
}
这一段决定了用户态 RT 线程的唤醒延迟

SCHED_FIFO/RR/DEADLINE 任务的睡眠定时器走 HARD:到期时在 arch timer 硬中断里直接 wake_up_process(),不经过任何线程。普通 SCHED_OTHER 任务的睡眠定时器走 SOFT:到期后要等 ksoftirqd 被调度——既防止了"非特权用户开一万个线程同时到期"拖垮硬中断,又不影响 RT 任务。也就是说:用 SCHED_OTHER 跑 cyclictest 在 RT 内核上会得到很难看的数字,这是设计如此。

flowchart TD
  I["hrtimer_init / hrtimer_setup_sleeper"] --> M{"mode 含 HRTIMER_MODE_HARD ?"}
  M -->|"是"| H["hard clock base"]
  M -->|"否"| S1{"是 sleeper 且
current 为 RT/DL 策略 ?"} S1 -->|"是, 自动补 HARD"| H S1 -->|"否"| RT{"CONFIG_PREEMPT_RT ?"} RT -->|"是: 强制 soft"| S["soft clock base"] RT -->|"否, 且未指定 SOFT"| H H --> HI["到期: hrtimer_interrupt 里
__hrtimer_run_queues ACTIVE_HARD
硬中断上下文"] S --> RS["到期: hrtimer_interrupt 只
raise HRTIMER_SOFTIRQ"] RS --> KS["ksoftirqd 或 BH 区出口
hrtimer_run_softirq
线程上下文, 可抢占"] classDef hardirq fill:#fde8da,stroke:#c2410c,color:#1d1d1f classDef softirq fill:#fbf0d9,stroke:#b7791f,color:#1d1d1f classDef rt fill:#f6e3dc,stroke:#b5371c,color:#1d1d1f class H,HI hardirq class S,RS,KS softirq class RT,S1 rt
hrtimer 在 PREEMPT_RT 下的分流规则(6.12)

hrtimer_interrupt:一次中断,两种处理

void hrtimer_interrupt(struct clock_event_device *dev)
{
	...
	raw_spin_lock_irqsave(&cpu_base->lock, flags);   /* raw 锁:硬中断上下文 */
	entry_time = now = hrtimer_update_base(cpu_base);
retry:
	cpu_base->in_hrtirq = 1;
	cpu_base->expires_next = KTIME_MAX;

	if (!ktime_before(now, cpu_base->softirq_expires_next)) {
		cpu_base->softirq_expires_next = KTIME_MAX;
		cpu_base->softirq_activated = 1;
		raise_softirq_irqoff(HRTIMER_SOFTIRQ);       /* soft 半边:只 raise */
	}

	__hrtimer_run_queues(cpu_base, now, flags, HRTIMER_ACTIVE_HARD);  /* hard 半边:就地跑 */
	...
}

soft 半边在 hrtimer_run_softirq()(kernel/time/hrtimer.c:1824)里执行,注意它在 RT 下先拿一把 softirq_expiry_lock(普通 spinlock_t):

static __latent_entropy void hrtimer_run_softirq(void)
{
	struct hrtimer_cpu_base *cpu_base = this_cpu_ptr(&hrtimer_bases);
	...
	hrtimer_cpu_base_lock_expiry(cpu_base);            /* RT: spin_lock(&softirq_expiry_lock) */
	raw_spin_lock_irqsave(&cpu_base->lock, flags);
	now = hrtimer_update_base(cpu_base);
	__hrtimer_run_queues(cpu_base, now, flags, HRTIMER_ACTIVE_SOFT);
	cpu_base->softirq_activated = 0;
	hrtimer_update_softirq_timer(cpu_base, true);
	raw_spin_unlock_irqrestore(&cpu_base->lock, flags);
	hrtimer_cpu_base_unlock_expiry(cpu_base);
}

expiry_lock:取消"正在运行的定时器"

hrtimer_cancel()/del_timer_sync() 必须等回调跑完才能返回。非 RT 下回调在 softirq 里不可抢占,自旋等一会儿就好;RT 下回调在一个可被抢占的线程里跑,这又是"自旋等待失效"的典型。源码注释(kernel/time/hrtimer.c:1441)列出两种灾难:

  • 调用者在别的 CPU 上:自旋等一个被抢占的低优先级线程 → 无界优先级反转;
  • 调用者正是在本 CPU 上抢占了 softirq 线程的那个任务:自旋永远等不到。

解法:执行 soft 回调的一方全程持有 expiry_lock,取消方在回调运行中时去 spin_lock(expiry_lock)——阻塞 + PI,让执行方被提升并跑完。执行方每跑完一个回调还会检查有没有等待者,有就短暂放锁(hrtimer_sync_wait_running(),hrtimer.c:1417),避免一批回调把等待者卡太久。timer_list 有完全对称的实现:timer_base.expiry_lock 与 del_timer_wait_running()(kernel/time/timer.c:255 / :1586)。POSIX timer 的删除路径也复用它(kernel/time/posix-timers.c:833 附近的注释)。

POSIX CPU 定时器:挪到 task_work

CLOCK_PROCESS_CPUTIME_ID 这类 CPU 时间定时器原来在 tick 硬中断里检查和投递信号,需要拿 sighand 锁。ARM64 选择了 HAVE_POSIX_CPU_TIMERS_TASK_WORK(arch/arm64/Kconfig:238),于是 POSIX_CPU_TIMERS_TASK_WORK 默认打开(kernel/time/Kconfig:56):tick 里只做快速检查,真正的到期处理推迟到该任务返回用户态前的 task_work 里执行。RT 下还要额外处理"收集到期定时器时 tick 又来了"的竞争(kernel/time/posix-cpu-timers.c:1287)。

6.6ARM64 arch timer:定时器的硬件起点

ARM64 上每个 CPU 都有自己的 Generic Timer(CNTP/CNTV/CNTHP…),通过 GICv3 的 PPI(私有外设中断,INTID 16–31)送达本 CPU。设备树里的典型写法(arch/arm64/boot/dts/arm/fvp-base-revc.dts:214):

timer {
	compatible = "arm,armv8-timer";
	interrupts = <GIC_PPI 13 ...>,   /* 安全物理定时器    INTID 29 */
		     <GIC_PPI 14 ...>,   /* 非安全物理定时器  INTID 30 */
		     <GIC_PPI 11 ...>,   /* 虚拟定时器 CNTV    INTID 27 */
		     <GIC_PPI 10 ...>;   /* EL2 物理定时器     INTID 26 */
};

Linux 默认用虚拟定时器(arch_timer_uses_ppi = ARCH_TIMER_VIRT_PPI,drivers/clocksource/arm_arch_timer.c:86);内核运行在 EL2(VHE)时改用 EL2 物理定时器(arm_arch_timer.c:1401)。它通过 request_percpu_irq() 注册(arm_arch_timer.c:1251),是 per-CPU devid 中断,流控是 handle_percpu_devid_irq()(kernel/irq/chip.c:924)——per-CPU 中断不会被强制线程化,所以在 RT 下它依然在硬中断里执行:

static __always_inline irqreturn_t timer_handler(const int access,
					struct clock_event_device *evt)
{
	unsigned long ctrl;

	ctrl = arch_timer_reg_read(access, ARCH_TIMER_REG_CTRL, evt);
	if (ctrl & ARCH_TIMER_CTRL_IT_STAT) {
		ctrl |= ARCH_TIMER_CTRL_IT_MASK;      /* 先在定时器上屏蔽,避免电平中断重复触发 */
		arch_timer_reg_write(access, ARCH_TIMER_REG_CTRL, ctrl, evt);
		evt->event_handler(evt);              /* 高精度模式下 = hrtimer_interrupt */
		return IRQ_HANDLED;
	}
	return IRQ_NONE;
}
CNTV 比较器到期 PPI INTID 27 GICv3 Redistributor → 本 CPU interface el1/el0 IRQ → gic_handle_irq handle_percpu_devid_irq → arch_timer_handler_virt hrtimer_interrupt() raw_spin_lock(cpu_base→lock),硬中断上下文 HRTIMER_ACTIVE_HARD:就地执行 tick_nohz_handler:调度 tick、jiffies、RCU 检查 hrtimer_wakeup:RT/DL 任务的 nanosleep sched hrtick / rt_bandwidth / dl_timer、KVM vtimer 只能用 raw 锁,必须很短 HRTIMER_ACTIVE_SOFT + timer_list:推迟 raise HRTIMER_SOFTIRQ / TIMER_SOFTIRQ ksoftirqd/N(SCHED_OTHER)唤醒 hrtimer_run_softirq / run_timer_softirq 持 expiry_lock,可被抢占,可用 spinlock_t PREEMPT_RT 默认去这边
一次 arch timer 中断在 RT 下的分流:只有显式 HARD 的回调留在硬中断,其余全部推迟到线程上下文

cyclictest / 用户态 RT 线程的唤醒就走左半边。下面是完整调用栈:

RT:arch timer 到期 → 唤醒 SCHED_FIFO 用户线程 ARM64 · GICv3 · PREEMPT_RT
  1. CPU 正在运行某个低优先级任务(或 idle)
  2. CNTV 到期 → PPI 27 → GIC → VBAR_EL1+0x280 el1h_64_irqarch/arm64/kernel/entry.S
  3. el1h_64_irq_handler() → el1_interrupt() → __el1_irq()entry-common.c:643 / :632 / :619
  4. do_interrupt_handler() → gic_handle_irq()entry-common.c:304 / irq-gic-v3.c:964
  5. __gic_handle_irq() → generic_handle_domain_irq()irq-gic-v3.c:867
  6. handle_percpu_devid_irq()kernel/irq/chip.c:924per-CPU 中断:不线程化
  7. arch_timer_handler_virt() → timer_handler()arm_arch_timer.c:681 / :665
  8. hrtimer_interrupt()kernel/time/hrtimer.c:1849soft 部分只 raise HRTIMER_SOFTIRQ
  9. __hrtimer_run_queues(..., HRTIMER_ACTIVE_HARD)hrtimer.c:1785
  10. hrtimer_wakeup() → wake_up_process() → try_to_wake_up()hrtimer.c:1981RT 线程入队,resched_curr 设置 need_resched
  11. …… 硬中断收尾 ……
  12. irq_exit_rcu() → invoke_softirq() [RT 版]kernel/softirq.c:295只唤醒 ksoftirqd(若 HRTIMER/TIMER pending)
  13. arm64_preempt_schedule_irq() → preempt_schedule_irq() → __schedule()entry-common.c:628中断返回点立即切换到 RT 线程
  14. RT 线程从 clock_nanosleep() 返回 EL0

与之对比,一个 soft hrtimer(比如普通进程的 timerfd)在 RT 下的路径:

RT:soft hrtimer 在 ksoftirqd 中到期 PREEMPT_RT
  1. hrtimer_interrupt() → raise_softirq_irqoff(HRTIMER_SOFTIRQ)hrtimer.c:1876
  2. irq_exit_rcu() → invoke_softirq() → wakeup_softirqd()softirq.c:295 / :75
  3. …… 等 ksoftirqd 被调度(SCHED_OTHER,可能被 RT 负载推迟)……
  4. smpboot_thread_fn() → run_ksoftirqd()kernel/smpboot.c / softirq.c:944
  5. ksoftirqd_run_begin() → __local_bh_disable_ip(SOFTIRQ_OFFSET)softirq.c:271拿 softirq_ctrl.lock
  6. handle_softirqs(true)softirq.c:536
  7. hrtimer_run_softirq()hrtimer.c:1824spin_lock(softirq_expiry_lock)
  8. __hrtimer_run_queues(..., HRTIMER_ACTIVE_SOFT) → 回调可被抢占;回调里可以拿 spinlock_t

6.7irq_work:同样的硬/软二分

irq_work 是"在任意上下文(包括 NMI)登记一个回调,尽快在硬中断上下文里执行"的机制,典型用户是 printk 唤醒、perf、RCU、调度器。ARM64 上它通过一个自 IPI(IPI_IRQ_WORK)或下一次 tick 触发。RT 下的问题与 hrtimer 完全一样:回调可能拿 spinlock_t。解法也一样——按标志二分:

static void __irq_work_queue_local(struct irq_work *work)
{
	...
	work_flags = atomic_read(&work->node.a_flags);
	if (work_flags & IRQ_WORK_LAZY)
		lazy_work = true;
	else if (IS_ENABLED(CONFIG_PREEMPT_RT) &&
		 !(work_flags & IRQ_WORK_HARD_IRQ))
		rt_lazy_work = true;           /* RT:非 HARD 的一律当 lazy */

	if (lazy_work || rt_lazy_work)
		list = this_cpu_ptr(&lazy_list);
	else
		list = this_cpu_ptr(&raised_list);
	...
}
  • raised_list:标了 IRQ_WORK_HARD_IRQ 的,依然在硬中断里执行(kernel/irq_work.c:259 irq_work_run())。
  • lazy_list:RT 下交给 per-CPU 线程 irq_work/N 执行(kernel/irq_work.c:325),它被设为 sched_set_fifo_low(),即 SCHED_FIFO 1(kernel/irq_work.c:317,kernel/sched/syscalls.c:866)。

例如 printk 的 wake_up_klogd_work 是 IRQ_WORK_INIT_LAZY(kernel/printk/printk.c:4516),它在 RT 下负责唤醒 printk 线程(见第 7 章)。

6.8变化与代价一览

非 RT
  • softirq 在 irq_exit 时就地执行,优先级事实上高于所有任务
  • local_bh_disable() = 关抢占
  • 超预算的 softirq 落到 SCHED_OTHER 的 ksoftirqd
  • hrtimer 默认硬中断回调
  • 非 HARD irq_work 在硬中断/tick 里执行
  • 取消运行中的定时器:自旋等
PREEMPT_RT(6.12)
  • softirq 只在线程上下文执行:关 BH 者的 local_bh_enable() 或 ksoftirqd
  • local_bh_disable() = per-CPU local_lock(rtmutex + PI)+ rcu_read_lock()
  • irq 线程里 raise 的 softirq 在该 irq 线程里执行,继承其优先级
  • hrtimer 默认 soft;RT/DL 任务的 sleeper 自动 HARD
  • 非 HARD irq_work 交给 irq_work/N(FIFO 1)
  • 取消运行中的定时器:在 expiry_lock 上阻塞 + PI
代价原因缓解
网络吞吐下降、softirq 处理多了上下文切换每个中断都要唤醒线程;BH 区间可被抢占NAPI busy-poll、调整 irq 线程亲和性
同 CPU 上不相关的 BH 区互相等待softirq_ctrl.lock 是一把 per-CPU 大锁(6.12 之后主线在逐步拆分)
timer_list / soft hrtimer 可能被 RT 负载推迟它们跑在 SCHED_OTHER 的 ksoftirqd 里给 ksoftirqd 设低优先级 FIFO;隔离核上减少定时器
非 RT 策略任务的 nanosleep 延迟变差它们的 sleeper 是 soft 模式需要低延迟的线程必须用 RT/DL 策略
本章小结

RT 对 softirq 的改造不是"把 softirq 挪到线程里"这么简单,而是重新定义了 local_bh_disable():它从"关抢占"变成"拿一把带 PI 的 per-CPU 锁"。有了这个定义,softirq 就可以在任何持锁的线程里安全执行,于是"谁 raise 谁执行"、"irq 线程优先级决定收包优先级"都顺理成章。定时器沿用同样的哲学——默认推迟到线程,只有显式承诺"短且只用 raw 锁"的 HARD 回调留在硬中断;而 RT 任务的睡眠唤醒正是被特意留在硬中断里的那一类。