软中断、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() 占用它们:
| # | 向量 | 注册者 | 做什么 |
|---|---|---|---|
| 0 | HI_SOFTIRQ | kernel/softirq.c | 高优先级 tasklet |
| 1 | TIMER_SOFTIRQ | kernel/time/timer.c | timer_list(时间轮)到期回调 |
| 2 | NET_TX_SOFTIRQ | net/core/dev.c | 发包完成、qdisc 调度 |
| 3 | NET_RX_SOFTIRQ | net/core/dev.c | NAPI 收包(net_rx_action) |
| 4 | BLOCK_SOFTIRQ | block/blk-mq.c | 块 I/O 完成 |
| 5 | IRQ_POLL_SOFTIRQ | lib/irq_poll.c | 块设备 irq_poll |
| 6 | TASKLET_SOFTIRQ | kernel/softirq.c | 普通 tasklet |
| 7 | SCHED_SOFTIRQ | kernel/sched/fair.c | 负载均衡 |
| 8 | HRTIMER_SOFTIRQ | kernel/time/hrtimer.c | soft 模式的 hrtimer 回调 |
| 9 | RCU_SOFTIRQ | kernel/rcu/tree.c | RCU core / 回调(RT 下不用,见第 7 章) |
raise 只是置位,真正执行要等内核在某个"检查点"上主动轮询这个位图。非 RT 内核有三个检查点:
- 硬中断返回:
__irq_exit_rcu()发现!in_interrupt() && local_softirq_pending()就调invoke_softirq()(kernel/softirq.c:652)。 - 重新打开 BH:
local_bh_enable()→__local_bh_enable_ip()发现有 pending 就do_softirq()。 - 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 放到调度器的视角里看,它的地位非常奇怪:
由此产生三类问题:
- 不可抢占的执行:softirq 运行时
preempt_count里带着SOFTIRQ_OFFSET,任何任务都抢不了它。一个NET_RX风暴就能让刚被唤醒的SCHED_FIFO 99任务多等将近 2ms——而这还只是"一轮"的上限,下一个中断回来又是一轮。 - 不可抢占的保护区:
local_bh_disable()在非 RT 下就是preempt_count += SOFTIRQ_DISABLE_OFFSET,等价于关抢占。网络栈、块层里大量spin_lock_bh(),每一处都是一个关抢占区。 - 不可调度、不可观测:softirq 没有线程身份,不能
chrt、不能taskset,top里只能看到被它占用 CPU 的那个"无辜"任务的 si 时间。
下面是 ARM64 上一个具体场景的调用栈:CPU 正在跑一个 SCHED_FIFO 80 的用户态控制线程,网卡中断进来,中断返回前就地执行 NET_RX。注意 softirq 跑在 per-CPU IRQ 栈上(do_softirq_own_stack),从头到尾那个 RT 线程都插不上手:
- RT 控制线程在 EL0 运行(SCHED_FIFO 80)
- VBAR_EL1 + 0x480 → el0t_64_irqarch/arm64/kernel/entry.SCPU 自动保存 ELR/SPSR、置 DAIF.I
- el0t_64_irq_handler()entry-common.c:945
- __el0_irq_handler_common() → el0_interrupt()entry-common.c:940 / :923
- irq_enter_rcu(); do_interrupt_handler()entry-common.c:304切到 per-CPU IRQ 栈
- gic_handle_irq() → __gic_handle_irq_from_irqson()irq-gic-v3.c:964 / :904读 ICC_IAR1_EL1 = ACK
- generic_handle_domain_irq() → handle_fasteoi_irq() → 网卡 handlerkernel/irq/chip.c:687handler 里 napi_schedule() → raise NET_RX_SOFTIRQ
- …… handler 返回,写 ICC_EOIR1_EL1 ……
- irq_exit_rcu() → __irq_exit_rcu()kernel/softirq.c:672 / :652preempt_count -= HARDIRQ_OFFSET,发现 pending
- invoke_softirq() → do_softirq_own_stack()softirq.c:444 / arch/arm64/kernel/irq.c:84call_on_irq_stack(____do_softirq)
- __do_softirq() → handle_softirqs(false)kernel/softirq.c:611 / :536local_irq_enable(),最多 2ms / 10 轮
- net_rx_action() → napi_poll() → 驱动 poll → 协议栈net/core/dev.c:7052这段时间 RT 线程完全无法运行
- …… softirq 结束 ……
- 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
"谁 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 优先级,就同时调了这块网卡的整个收包路径。
- ret_from_fork → kthread()arch/arm64/kernel/entry.S / kernel/kthread.c
- irq_thread()kernel/irq/manage.c:1300irq/NN-eth0,SCHED_FIFO 50,被 primary handler 唤醒
- irq_thread_fn → irq_forced_thread_fn()kernel/irq/manage.c:1191
- local_bh_disable() → __local_bh_disable_ip()kernel/softirq.c:156local_lock(&softirq_ctrl.lock) + rcu_read_lock()
- 驱动原 handler → napi_schedule() → __raise_softirq_irqoff(NET_RX)只是置 pending 位
- irq_finalize_oneshot()unmask 中断线
- local_bh_enable() → __local_bh_enable_ip()kernel/softirq.c:218最外层且可抢占 → 就地处理
- __do_softirq() → handle_softirqs(false)kernel/softirq.c:611 / :536in_serving_softirq() 为真,但仍是普通线程上下文
- net_rx_action() → napi_poll() → 驱动 pollnet/core/dev.c:7052可以被更高优先级的 RT 任务随时抢占
- __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 继续
- 收益: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 由 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;
}
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_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;
}
cyclictest / 用户态 RT 线程的唤醒就走左半边。下面是完整调用栈:
- CPU 正在运行某个低优先级任务(或 idle)
- CNTV 到期 → PPI 27 → GIC → VBAR_EL1+0x280 el1h_64_irqarch/arm64/kernel/entry.S
- el1h_64_irq_handler() → el1_interrupt() → __el1_irq()entry-common.c:643 / :632 / :619
- do_interrupt_handler() → gic_handle_irq()entry-common.c:304 / irq-gic-v3.c:964
- __gic_handle_irq() → generic_handle_domain_irq()irq-gic-v3.c:867
- handle_percpu_devid_irq()kernel/irq/chip.c:924per-CPU 中断:不线程化
- arch_timer_handler_virt() → timer_handler()arm_arch_timer.c:681 / :665
- hrtimer_interrupt()kernel/time/hrtimer.c:1849soft 部分只 raise HRTIMER_SOFTIRQ
- __hrtimer_run_queues(..., HRTIMER_ACTIVE_HARD)hrtimer.c:1785
- hrtimer_wakeup() → wake_up_process() → try_to_wake_up()hrtimer.c:1981RT 线程入队,resched_curr 设置 need_resched
- …… 硬中断收尾 ……
- irq_exit_rcu() → invoke_softirq() [RT 版]kernel/softirq.c:295只唤醒 ksoftirqd(若 HRTIMER/TIMER pending)
- arm64_preempt_schedule_irq() → preempt_schedule_irq() → __schedule()entry-common.c:628中断返回点立即切换到 RT 线程
- RT 线程从 clock_nanosleep() 返回 EL0
与之对比,一个 soft hrtimer(比如普通进程的 timerfd)在 RT 下的路径:
- hrtimer_interrupt() → raise_softirq_irqoff(HRTIMER_SOFTIRQ)hrtimer.c:1876
- irq_exit_rcu() → invoke_softirq() → wakeup_softirqd()softirq.c:295 / :75
- …… 等 ksoftirqd 被调度(SCHED_OTHER,可能被 RT 负载推迟)……
- smpboot_thread_fn() → run_ksoftirqd()kernel/smpboot.c / softirq.c:944
- ksoftirqd_run_begin() → __local_bh_disable_ip(SOFTIRQ_OFFSET)softirq.c:271拿 softirq_ctrl.lock
- handle_softirqs(true)softirq.c:536
- hrtimer_run_softirq()hrtimer.c:1824spin_lock(softirq_expiry_lock)
- __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:259irq_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变化与代价一览
- softirq 在
irq_exit时就地执行,优先级事实上高于所有任务 local_bh_disable()= 关抢占- 超预算的 softirq 落到 SCHED_OTHER 的 ksoftirqd
- hrtimer 默认硬中断回调
- 非 HARD irq_work 在硬中断/tick 里执行
- 取消运行中的定时器:自旋等
- softirq 只在线程上下文执行:关 BH 者的
local_bh_enable()或 ksoftirqd local_bh_disable()= per-CPUlocal_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 任务的睡眠唤醒正是被特意留在硬中断里的那一类。