首页/目录/全部文章

全部文章

八个专题的源码、算法与协议笔记都在这里。

笔记列表

kernel/sched 文档索引

kernel/sched 文档索引

Linux 6.1(RK3588)调度子系统源码分析与原理说明。

文档 说明
sched调度机制与原理详解.md 主文档:调度方法、原理、SMP/EAS、RK3588 定制、源码索引
调度算法原理详解.md 算法深入:CFS/RT/DL 公式推导、数值示例、PELT、负载均衡、EAS(深入版)
上下文切换与状态保存详解.md 切换时 task_struct / mm / 寄存器如何保存与恢复

源码路径

1
rk3588/kernel-6.1/kernel/sched/

调度方法速查

用户策略 调度类 文件 算法
SCHED_OTHER / NORMAL fair fair.c CFS (vruntime)
SCHED_BATCH fair fair.c CFS
SCHED_IDLE fair fair.c CFS 低权重
SCHED_FIFO / SCHED_RR rt rt.c 优先级队列
SCHED_DEADLINE dl deadline.c EDF + CBS
(idle 线程) idle idle.c cpuidle
(stop_machine) stop stop_task.c 最高优先级

统一入口:core.c__schedule()pick_next_task()

源码注释

内核源码中搜索 可定位中文模块/函数注释(core.cfair.crt.cdeadline.c 等)。

kernel/sched 调度机制与原理详解

kernel/sched 调度机制与原理详解

源码路径rk3588/kernel-6.1/kernel/sched/
内核版本:Linux 6.1(RK3588 平台)
平台:RK3588(4×Cortex-A76 + 4×Cortex-A55 big.LITTLE)
文档目录linuxDoc/kernel/sched/

该目录实现 Linux 6.1 的 通用多策略调度框架。所有策略通过 struct sched_class 虚函数表接入统一入口 __schedule(),在 RK3588 上额外叠加 big.LITTLE、EAS、Rockchip 性能档位等机制。


目录


一、源码目录结构

1.1 编译单元(Makefile)

为平衡编译时间,源码拆成 4 个 .o 文件:

编译单元 包含内容 规模
core.o 调度核心:__schedule、唤醒、迁移、上下文切换 ~11,293 行
fair.o CFS 公平调度、负载均衡、EAS ~12,520 行
build_policy.o idle / rt / deadline / pelt / cputime ~8,000 行
build_utility.o topology / psi / cpufreq / wait / debug 等 ~8,000 行

1.2 主要源文件

文件 功能
core.c 调度主入口、任务唤醒/阻塞、上下文切换、迁移
fair.c CFS 完全公平调度、负载均衡、EAS
rt.c 实时调度 SCHED_FIFO / SCHED_RR
deadline.c Deadline 调度 SCHED_DEADLINE(EDF + CBS)
idle.c per-CPU idle 线程
stop_task.c 最高优先级 stop 任务
pelt.c PELT 负载跟踪
topology.c 调度域拓扑、EAS 初始化
cpufreq_schedutil.c schedutil CPU 调频 governor
sched.h 调度器内部类型与 inline 方法
core_sched.c SMT 核心调度(cookie 机制)
psi.c Pressure Stall Information
clock.c 调度器时钟 sched_clock()
loadavg.c /proc/loadavg 计算
wait.c / completion.c 等待队列、完成量

二、调度方法总览

Linux 调度分为两层概念:

  1. 用户可见调度策略sched_setscheduler() / sched_attr.policy):进程属于哪种策略。
  2. 内核调度类sched_class):策略在 kernel/sched 中的实现入口,按优先级链式选取。

2.0 用户策略 ↔ 调度类对照表

策略常量 数值 调度类 源文件 调度算法 典型用途
SCHED_OTHER / SCHED_NORMAL 0 fair (CFS) fair.c vruntime 红黑树 + 权重 普通用户进程(默认)
SCHED_FIFO 1 rt rt.c 100 级优先级 FIFO 队列 软实时,同优先级跑到底
SCHED_RR 2 rt rt.c 同优先级时间片轮转 软实时,同优先级公平轮转
SCHED_BATCH 3 fair (CFS) fair.c CFS + batch 提示 批处理,减少唤醒抢占
SCHED_IDLE 5 fair (CFS) fair.c CFS 极低权重 低优先级后台任务
SCHED_DEADLINE 6 deadline (dl) deadline.c EDF + CBS 硬实时周期任务
(无用户策略) stop stop_task.c 每 CPU 唯一 stop 任务 stop_machine 等内核关键路径
(无用户策略) idle idle.c 最低优先级 idle 线程 CPU 空闲时进入 cpuidle

注意区分SCHED_IDLE 策略任务仍在 CFS(fair.c)中调度;idle 线程是每 CPU 的内核线程,使用 idle_sched_classidle.c),两者完全不同。

2.0.1 调度类优先级链(选任务顺序)

链接器通过 __sched_class_highest__sched_class_lowest 将调度类串成链表,从高到低遍历:

1
stop → deadline → rt → fair → idle

源码(sched.h):

1
2
3
4
5
extern const struct sched_class stop_sched_class;
extern const struct sched_class dl_sched_class;
extern const struct sched_class rt_sched_class;
extern const struct sched_class fair_sched_class;
extern const struct sched_class idle_sched_class;

抢占规则:高调度类任务存在时,低调度类任务无法获得 CPU。例如 RT 队列非空时 CFS 任务不会运行;CFS/RT/DL 都空时才运行 idle 线程。

2.1 五大调度类(按优先级从高到低)

调度类 源文件 策略 用途
stop stop_task.c 内核 stop 任务 CPU 热插拔、迁移等,绝对最高优先级
deadline (dl) deadline.c SCHED_DEADLINE 硬实时,EDF + CBS 带宽控制
rt rt.c SCHED_FIFO / SCHED_RR 软实时,静态优先级 0–99
fair (CFS) fair.c SCHED_NORMAL / SCHED_BATCH / SCHED_IDLE 普通进程,完全公平调度
idle idle.c per-CPU idle 线程 CPU 空闲时进入 cpuidle 省电

优先级遍历逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// core.c: __pick_next_task()
static inline struct task_struct *
__pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
// 快速路径:若全是 CFS 任务,直接 pick_next_task_fair
if (likely(!sched_class_above(prev->sched_class, &fair_sched_class) &&
rq->nr_running == rq->cfs.h_nr_running)) {
p = pick_next_task_fair(rq, prev, rf);
// ...
}
// 通用路径:从高到低遍历所有调度类
for_each_class(class) {
p = class->pick_next_task(rq);
if (p) return p;
}
}

2.2 辅助/联动机制

机制 文件 作用
PELT 负载跟踪 pelt.c 指数衰减估算 CPU 利用率
拓扑与负载均衡 topology.c + fair.c 构建 sched_domain,跨 CPU 迁移任务
EAS 能耗感知调度 fair.c + topology.c big.LITTLE 上按能耗选 CPU
schedutil 调频 cpufreq_schedutil.c 根据 PELT 利用率调节 CPU 频率
uclamp 利用率钳制 core.c + fair.c 限制任务最低/最高 CPU 利用率
PSI 压力监控 psi.c CPU/内存/IO 压力 stall 信息
CFS 带宽控制 fair.c cgroup CPU 配额 throttle
RT 带宽控制 rt.c RT 任务最多占用 95% CPU
核心调度 SMT core_sched.c 同物理核超线程任务的 cookie 协同
Rockchip 性能档位 rockchip_performance.c 低/中/高性能模式,影响 uclamp 与 RT 选核

三、sched_class 调度类接口

各调度策略实现 struct sched_class 虚函数表,由 __pick_next_task() 按优先级遍历。

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
// sched.h
struct sched_class {
/* 将任务 @p 加入运行队列 @rq;@flags 为 ENQUEUE_* */
void (*enqueue_task)(struct rq *rq, struct task_struct *p, int flags);
/* 将任务 @p 从运行队列 @rq 移除;@flags 为 DEQUEUE_* */
void (*dequeue_task)(struct rq *rq, struct task_struct *p, int flags);
/* 主动让出 CPU(如 sched_yield) */
void (*yield_task)(struct rq *rq);

/* 判断唤醒/入队的 @p 是否应抢占 rq->curr */
void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);

/* 在本调度类中选取 @rq 上优先级最高的可运行任务 */
struct task_struct *(*pick_next_task)(struct rq *rq);

/* rq->curr 即将停止运行时的统计更新(上下文切换前) */
void (*put_prev_task)(struct rq *rq, struct task_struct *p);
/* 准备 @p 作为 rq->curr 运行(选中后、context_switch 前) */
void (*set_next_task)(struct rq *rq, struct task_struct *p, bool first);

#ifdef CONFIG_SMP
/* 周期性或空闲时负载均衡 */
int (*balance)(struct rq *rq, struct task_struct *prev, struct rq_flags *rf);
/* 为任务 @p 选择最合适的 CPU(唤醒、fork 或迁移时) */
int (*select_task_rq)(struct task_struct *p, int task_cpu, int flags);
/* 轻量级选任务,仅用于核心调度 CONFIG_SCHED_CORE */
struct task_struct *(*pick_task)(struct rq *rq);
/* 通知调度类:@p 迁移后运行队列已变为 @new_cpu */
void (*migrate_task_rq)(struct task_struct *p, int new_cpu);
#endif

/* 运行任务的定时器 tick 处理 */
void (*task_tick)(struct rq *rq, struct task_struct *p, int queued);
/* fork 时初始化调度状态 */
void (*task_fork)(struct task_struct *p);
/* 进程退出时清理调度状态 */
void (*task_dead)(struct task_struct *p);

/* 任务切换调度类或优先级时的钩子 */
void (*switched_from)(struct rq *this_rq, struct task_struct *task);
void (*switched_to)(struct rq *this_rq, struct task_struct *task);
void (*prio_changed)(struct rq *this_rq, struct task_struct *task, int oldprio);

/* 更新 rq->curr 的运行时间统计(vruntime、dl runtime 等) */
void (*update_curr)(struct rq *rq);
};

各调度类的实现注册:

调度类 注册变量 源文件
stop stop_sched_class stop_task.c
deadline dl_sched_class deadline.c
rt rt_sched_class rt.c
fair fair_sched_class fair.c
idle idle_sched_class idle.c

四、统一调度框架原理

4.1 核心数据结构

每个 CPU 一个运行队列 struct rq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct rq {
raw_spinlock_t __lock; // 运行队列锁
task_struct *curr; // 当前运行任务
unsigned int nr_running; // 可运行任务数

cfs_rq cfs; // CFS 公平调度队列
rt_rq rt; // 实时调度队列
dl_rq dl; // Deadline 调度队列

u64 clock; // 调度器时钟
u64 clock_pelt; // PELT 专用时钟
sched_domain *sd; // 调度域(负载均衡范围)
root_domain *rd; // 根域(SMP 全局状态)
};

每个任务的 task_struct->sched_class 指向其所属调度类的 vtable。

4.2 调度主流程 __schedule()

触发调度__schedulerq_lock 加锁update_rq_clock 更新时钟prev 是否阻塞?deactivate_task 移出 rq继续pick_next_task 选下一个任务prev != next?context_switch 上下文切换解锁返回switch_to 汇编切换寄存器/栈

关键代码路径(core.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void __sched notrace __schedule(unsigned int sched_mode)
{
rq = cpu_rq(smp_processor_id());
prev = rq->curr;

rq_lock(rq, &rf);
update_rq_clock(rq);

// 非抢占路径:任务主动阻塞
if (!(sched_mode & SM_MASK_PREEMPT) && prev_state)
deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);

next = pick_next_task(rq, prev, &rf);

if (likely(prev != next)) {
RCU_INIT_POINTER(rq->curr, next);
rq = context_switch(rq, prev, next, &rf); // MM 切换 + switch_to
}
}

4.3 调度触发时机

触发方式 机制 代码路径
主动阻塞 mutex、waitqueue、sleep schedule()__schedule(SM_NONE)
tick 抢占 定时器中断 scheduler_tick()entity_tick()resched_curr()
唤醒抢占 高优先级任务被唤醒 try_to_wake_up()check_preempt_curr()
主动让出 sched_yield() yield_task_fair()resched_curr()
内核抢占 中断/系统调用返回 检查 TIF_NEED_RESCHED 标志

4.4 任务生命周期:入队/出队

1
2
3
4
5
6
7
8
9
10
11
12
// core.c
void activate_task(struct rq *rq, struct task_struct *p, int flags)
{
enqueue_task(rq, p, flags); // 调用 sched_class->enqueue_task
p->on_rq = TASK_ON_RQ_QUEUED;
}

void deactivate_task(struct rq *rq, struct task_struct *p, int flags)
{
p->on_rq = (flags & DEQUEUE_SLEEP) ? 0 : TASK_ON_RQ_MIGRATING;
dequeue_task(rq, p, flags); // 调用 sched_class->dequeue_task
}

唤醒路径:

1
2
3
4
try_to_wake_up()
→ select_task_rq() 选 CPU
→ activate_task() 入队
→ check_preempt_curr() 判断是否抢占

跨调度类抢占(core.c):

1
2
3
4
5
6
7
void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
{
if (p->sched_class == rq->curr->sched_class)
rq->curr->sched_class->check_preempt_curr(rq, p, flags);
else if (sched_class_above(p->sched_class, rq->curr->sched_class))
resched_curr(rq); // 高优先级调度类直接抢占
}

五、各调度策略原理详解

算法级深入描述(公式、伪代码、流程图、函数对照)见:调度算法原理详解.md

5.1 CFS 完全公平调度(fair.c

CFS 是 SCHED_NORMAL(普通进程)和 SCHED_BATCH(批处理)的实现,占日常调度主体。

核心思想:虚拟运行时间 vruntime

每个任务维护一个 vruntime(虚拟运行时间)。CPU 时间按权重折算后累加到 vruntime:

1
vruntime += delta_exec × (NICE_0_LOAD / task_weight)
1
2
3
4
5
6
7
8
// fair.c: update_curr()
static void update_curr(struct cfs_rq *cfs_rq)
{
delta_exec = now - curr->exec_start;
curr->exec_start = now;
curr->vruntime += calc_delta_fair(delta_exec, curr); // 按权重折算
update_min_vruntime(cfs_rq);
}
  • nice 值越小(优先级越高)→ weight 越大 → 同样物理时间 vruntime 增长越慢 → 获得更多 CPU
  • nice 0 权重 1024,nice +19 约 15,nice -20 约 88761

红黑树选任务

所有可运行任务按 vruntime 排序存入 红黑树 cfs_rq.tasks_timeline

1
2
3
       [vruntime=100]
/ \
[vruntime=50] [vruntime=150]
  • 左子树 vruntime 最小 → 最”该运行”的任务
  • pick_next_entity() 取 leftmost 节点,同时考虑 buddy 机制

调度周期与时间片

CFS 没有固定时间片,而是按”调度周期”分配:

1
2
3
4
5
调度周期 = sched_latency(默认 6ms × (1+ilog(ncpus)))
每个任务时间片 = 周期 × (task_weight / 总weight)

若 nr_running > sched_nr_latency:
周期 = nr_running × sched_min_granularity // 防止时间片过小

关键 sysctl 参数:

参数 默认值 含义
sched_latency_ns 6ms × (1+ilog(ncpus)) 目标抢占延迟
sched_min_granularity_ns 0.75ms × (1+ilog(ncpus)) 最小抢占粒度
sched_wakeup_granularity_ns 1ms 唤醒抢占粒度

唤醒抢占粒度

新唤醒的任务是否立即抢占当前任务,取决于 vruntime 差距是否超过 wakeup_granularity

1
2
3
4
5
6
7
8
9
// fair.c: wakeup_preempt_entity()
static int wakeup_preempt_entity(struct sched_entity *curr, struct sched_entity *se)
{
s64 vdiff = curr->vruntime - se->vruntime;
if (vdiff <= 0) return -1; // 当前任务更该运行
gran = wakeup_gran(se);
if (vdiff > gran) return 1; // 唤醒任务明显更该运行 → 抢占
return 0; // 差距不够 → 不抢占
}

这保证了 交互式任务(频繁 sleep/wake)比 CPU 密集型任务 获得更快响应。

place_entity:新任务/唤醒任务的 vruntime 放置

1
2
3
4
5
6
7
8
9
10
11
12
// fair.c: place_entity()
static void place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
vruntime = cfs_rq->min_vruntime;
if (initial && sched_feat(START_DEBIT))
vruntime += sched_vslice(cfs_rq, se); // 新任务:预扣一个时间片
if (!initial) {
// 唤醒任务:vruntime 减去 sleep 补偿(最多一个 latency)
vruntime -= thresh;
}
se->vruntime = max_vruntime(se->vruntime, vruntime);
}
  • 新 fork 的任务:vruntime = min_vruntime + 一个时间片(START_DEBIT),避免立即抢占
  • 唤醒的任务:vruntime 适当减小,补偿 sleep 期间”欠”的 CPU 时间

Buddy 机制(缓存局部性优化)

pick_next_entity() 在公平性允许范围内优先选 buddy:

1
2
3
4
5
6
7
// 优先级:next buddy > last buddy > leftmost
if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1)
se = cfs_rq->next; // 刚被唤醒的任务(缓存热)
else if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1)
se = cfs_rq->last; // 刚被抢占的任务(缓存热)
else
se = left; // 标准:vruntime 最小

5.2 RT 实时调度(rt.c

数据结构

RT 任务按 静态优先级(0–99,数字越大优先级越高)组织:

1
2
rt_rq.active[]  —  100 个优先级数组,每个数组是一个链表
rt_rq.bitmap — 位图快速找到最高优先级非空数组

调度规则

  1. SCHED_FIFO:同优先级 FIFO,运行直到主动阻塞或被更高优先级抢占
  2. SCHED_RR:同优先级轮转,时间片 sched_rr_timeslice(默认 100ms)用完重新排队

抢占逻辑

1
2
3
4
5
6
// rt.c: check_preempt_curr_rt()
static void check_preempt_curr_rt(struct rq *rq, struct task_struct *p, int flags)
{
if (p->prio < rq->curr->prio) // 数字越小优先级越高
resched_curr(rq);
}

RT 任务 绝对优先于 CFS 任务:只要 RT 队列非空,CFS 任务无法运行。

RT 带宽控制

防止 RT 任务占满 CPU:

1
2
3
4
默认:sched_rt_period_us = 1,000,000(1 秒)
sched_rt_runtime_us = 950,000(950 毫秒)

RT 任务在一个周期内最多运行 950ms,剩余 50ms 留给 CFS

SMP 负载均衡

RT 任务过载时(一个 CPU 上有多个 RT 任务),通过 RT Push IPI 机制将任务推到其他 CPU 的 RT 队列。


5.3 Deadline 调度(deadline.c

算法:EDF + CBS

每个任务有三个参数:

  • runtime:每个周期内需要的 CPU 时间
  • period:周期长度
  • deadline:deadline = 当前时间 + period
1
2
任务参数示例:runtime=2ms, period=10ms, deadline=10ms
→ 每 10ms 内必须完成 2ms 的计算,deadline 为 10ms

数据结构

1
dl_rq.root  —  红黑树,按 deadline 排序(最早 deadline 优先)

CBS(Constant Bandwidth Server)带宽控制

  • 任务在一个 period 内 runtime 用完 → throttle(暂停运行)
  • 下一个 period 开始 → replenish(补充 runtime)
  • 超出 bandwidth 的任务被限速,不影响其他 deadline 任务

抢占规则

Deadline 任务的优先级 动态计算:deadline 越早,优先级越高(MAX_DL_PRIO - 1 - deadline)。


5.4 Idle 调度(idle.c

每个 CPU 有一个 idle 线程(内核线程,非用户进程),当没有其他可运行任务时运行:

  1. cpu_startup_entry()do_idle() 循环
  2. 调用 cpuidle_idle_call() 进入低功耗(WFI/WFE)
  3. 被 tick/中断唤醒后检查 need_resched,必要时 schedule_idle() 切到正常任务
  4. 优先级最低,由 idle_sched_class 选中

5.5 Stop 调度(stop_task.c

绝对最高优先级,供 stop_machine() 等内核机制使用:

  • 每个 rq 至多一个 stop 任务(rq->stop
  • 不迁移、不让出、不被抢占
  • pick_next_task_stop() 直接返回 rq->stop

5.6 辅助调度机制(非独立 sched_class)

机制 文件 原理
Autogroup autogroup.c 按 TTY 会话自动创建 task_group,同终端前台/后台 CFS 带宽隔离
Core Scheduling core_sched.c + core.c SMT sibling 上仅相同 core_cookie 任务可并行,防侧信道
CFS Bandwidth fair.c cgroup CPU quota,cfs_bandwidth throttle
RT Bandwidth rt.c 每周期 RT 最多跑 950ms(默认),防 RT 饿死 CFS
DL Bandwidth deadline.c root_domain 上 DL 总带宽准入控制
uclamp core.c + fair.c 限制任务 util 上下界,影响 EAS/选核/调频
Housekeeping isolation.c 隔离 CPU 仅跑内核 housekeeping 任务

Autogroup 原理

1
2
3
打开 TTY → sched_autogroup_create_attach() 创建 autogroup
fork → sched_autogroup_fork() 继承父 autogroup
每个 autogroup → 一个 task_group → CFS shares(nice 映射权重)

可通过 /proc/<pid>/autogroup 调整组内 nice;开关 /proc/sys/kernel/sched_autogroup_enabled

Core Scheduling 原理

1
2
3
task A (cookie=1)  ─┐
task B (cookie=1) ─┼─ 可同时在 SMT sibling 上运行
task C (cookie=2) ─┘─ 与 cookie=1 互斥,后者 forced idle

用户接口:prctl(PR_SCHED_CORE_*);选任务逻辑在 core.cCONFIG_SCHED_CORE 分支。


六、SMP 多核调度原理

6.1 PELT 负载跟踪(pelt.c

Per-Entity Load Tracking 用指数衰减几何级数估算历史负载:

1
load_avg ≈ load × y^n    (y^32 ≈ 0.5,约 32ms 半衰期)

每个 sched_entity 和每个 rq 维护三个平均值:

字段 含义 用途
load_avg 加权可运行负载 CFS 权重计算
runnable_avg 可运行任务数 负载均衡决策
util_avg CPU 利用率 schedutil 调频、EAS 能耗估算

PELT 在 update_load_avg() 中更新,触发点:enqueue、dequeue、tick、migration。

6.2 调度域与负载均衡

sched_domain 层次结构

topology.c 根据 CPU 拓扑构建多层调度域:

1
2
3
4
RK3588 示例:
DIE 域(8 CPUs,跨 cluster)
└── MC 域(4 CPUs,同 cluster 共享 L3)
└── CPU 域(1 CPU)

每层域有 SD_* 标志控制行为:

  • SD_LOAD_BALANCE:允许负载均衡
  • SD_ASYM_CPUCAPACITY:非对称 CPU 容量(A76 vs A55)
  • SD_SHARE_CPUCAPACITY:共享 L2/L3 cache

load_balance 流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// fair.c: load_balance()
static int load_balance(int this_cpu, struct rq *this_rq,
struct sched_domain *sd, ...)
{
// 1. 判断是否需要均衡
if (!should_we_balance(&env)) goto out_balanced;

// 2. 找最忙的调度组
group = find_busiest_group(&env);

// 3. 找最忙的运行队列
busiest = find_busiest_queue(&env, group);

// 4. 从 busy 队列拉取任务到本地
cur_ld_moved = detach_tasks(&env); // 从 busiest 摘下
attach_tasks(&env); // 挂到 this_rq
}

触发时机:

  • 周期性scheduler_tick()trigger_load_balance()(默认每 4ms)
  • 空闲时newidle_balance() — CPU 即将 idle 前主动拉任务
  • 主动均衡:misfit 任务(负载超过 CPU 容量)触发 active_balance

Misfit 检测

当任务的 util_avg 超过当前 CPU 的 cpu_capacity 时,标记为 misfit

1
2
例:util_avg=800 的任务跑在 A55(capacity=512)上
→ misfit → 主动迁移到 A76(capacity=1024)

6.3 EAS 能耗感知调度(RK3588 关键)

RK3588 为 4×A76 + 4×A55 big.LITTLE,find_energy_efficient_cpu() 在唤醒时选最省电的 CPU:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// fair.c: find_energy_efficient_cpu()
static int find_energy_efficient_cpu(struct task_struct *p, int prev_cpu)
{
// 1. 检查 EAS 是否可用(Energy Model + schedutil + 非 overutilized)
pd = rcu_dereference(rd->pd);
if (!pd || rd->overutilized) goto unlock;

// 2. 遍历每个 performance domain(A76 域、A55 域)
for (; pd; pd = pd->next) {
// 3. 在每个域内找 spare capacity 最大的 CPU
// 4. 用 Energy Model 计算迁移前后的能耗差
base_energy = compute_energy(..., prev_cpu);
cur_delta = compute_energy(..., target) - base_energy;
// 5. 选能耗增量最小的 CPU
}
}

EAS 启用条件(topology.c):

  1. 系统有 Energy Model(EM)
  2. 使用 schedutil governor
  3. 存在 SD_ASYM_CPUCAPACITY 调度域
  4. EM 复杂度 < 2048(EM_MAX_COMPLEXITY

策略:cluster-packing(优先在同一 cluster 内分配)+ 大任务上大核

6.4 唤醒选 CPU 路径

1
2
3
4
5
6
try_to_wake_up()
└─ select_task_rq_fair()
├─ sync wake / affine 唤醒 → 原 CPU
├─ EAS 启用 → find_energy_efficient_cpu()
├─ 节能模式 → find_idlest_cpu()
└─ 默认 → find_idlest_group() + find_idlest_cpu()

七、调频联动(schedutil)

cpufreq_schedutil.c 将 PELT 利用率映射为 CPU 频率:

1
2
util = cpu_util_cfs() + cpu_util_rt() + cpu_util_dl()
freq = map_util_freq(util, max_freq, max_capacity)

Rockchip 定制:引入 target_load 参数(默认 80%),调整映射曲线:

1
2
3
4
5
6
// cpufreq_schedutil.c
#ifdef CONFIG_ARCH_ROCKCHIP
util = 100 * util / sg_policy->tunables->target_load;
#else
util = map_util_perf(util);
#endif

含义:当 CPU 利用率达到 80% 时就请求接近最高频率,提升响应速度。

可通过 sysfs 调节:/sys/devices/system/cpu/cpufreq/policyX/schedutil/target_load


八、完整调度时序

以普通进程 read() 阻塞与唤醒为例:

CPU 硬件__schedulefair.c (CFS)try_to_wake_up用户进程CPU 硬件__schedulefair.c (CFS)try_to_wake_up用户进程磁盘 IO 完成,中断唤醒中断返回 / preempt_enableread() 阻塞,schedule()deactivate_task (出队)pick_next_task_fairpick_next_entity (红黑树 leftmost)context_switch → idle 线程中断/工作队列select_task_rq_fair (EAS 选 CPU)enqueue_task_fair (入队)check_preempt_wakeup (是否抢占?)resched_curr (设置 TIF_NEED_RESCHED)__schedule (抢占)pick_next_task_faircontext_switch → 唤醒的进程继续执行 read() 返回

九、RK3588 平台特有机制

9.1 Rockchip 性能档位

源码:drivers/soc/rockchip/rockchip_performance.c
头文件:include/soc/rockchip/rockchip_performance.h
调度器侧通过 sched.h 引入,提供三档性能模式:

档位 名称 uclamp_min_rt RT 选核策略 场景
0 LOW 0 RT 倾向小核 (A55) 省电
1 NORMAL 1024 (满) 默认 平衡
2 HIGH 1024 (满) RT 倾向大核 (A76) 高性能

切换方式:module_param level=N 或对应 sysfs。

初始化时按 arch_scale_cpu_capacity() 划分大/小核 mask:

1
2
3
4
5
6
7
// rockchip_performance.c
for_each_possible_cpu(cpu) {
if (arch_scale_cpu_capacity(cpu) > cpub_min_cap)
cpumask_set_cpu(cpu, cpub_mask); // A76 大核
else
cpumask_set_cpu(cpu, cpul_mask); // A55 小核
}

提供的接口:

接口 作用
rockchip_perf_get_level() 获取当前性能档位
rockchip_perf_get_cpul_mask() 获取小核 cpumask
rockchip_perf_get_cpub_mask() 获取大核 cpumask
rockchip_perf_select_rt_cpu() RT 任务选 CPU
rockchip_perf_misfit_rt() 判断 RT 是否跑在错误核心
rockchip_perf_uclamp_sync_util_min_rt_default() 同步 uclamp 默认值

9.2 schedutil target_load

Rockchip 在 schedutil governor 中新增 target_load(默认 80),使 CPU 在 80% 利用率时即接近最高频,提升交互响应。

9.3 EAS + big.LITTLE

RK3588 调度适配要点:

  1. 非对称容量:A76 capacity ≈ 1024,A55 ≈ 512
  2. EAS 迁移find_energy_efficient_cpu() 比较迁移前后能耗
  3. Misfit 检测:高负载任务在小核上标记 misfit 并迁移到大核
  4. uclamp:限制任务最低/最高利用率,避免小核跑重负载

十、调试与观测接口

接口 内容
/proc/sched_debug 各 CPU rq、CFS 树、负载详情
/proc/schedstat 调度统计(迁移次数、唤醒延迟等)
trace/events/sched/ ftrace 调度事件
/sys/kernel/debug/sched/ debugfs 调度特性开关
/sys/kernel/sched_energy_aware EAS 开关
/proc/sys/kernel/sched_* 调度 sysctl 参数
module rockchip_performance level=N Rockchip 性能档位

常用 sysctl:

路径 含义
/proc/sys/kernel/sched_latency_ns CFS 调度周期
/proc/sys/kernel/sched_min_granularity_ns CFS 最小粒度
/proc/sys/kernel/sched_rt_runtime_us RT 带宽上限
/proc/sys/kernel/sched_energy_aware EAS 开关

十一、总结

调度原理核心要点

层次 原理 关键数据结构
框架层 sched_class vtable + per-CPU rq struct sched_class, struct rq
选任务 按调度类优先级遍历,类内按策略选 红黑树(CFS/DL)、优先级数组(RT)
公平性 vruntime 虚拟时间 + 权重 sched_entity.vruntime
抢占 唤醒粒度 + tick 检查 + 调度类优先级 wakeup_granularity, TIF_NEED_RESCHED
负载感知 PELT 指数衰减平均 sched_avg.util_avg
多核均衡 sched_domain 层次 + load_balance sched_domain, root_domain
能耗优化 EAS + Energy Model + schedutil perf_domain, em_perf_domain
带宽控制 CFS cgroup quota + RT/DL bandwidth cfs_bandwidth, rt_bandwidth, dl_bw

RK3588 调度数据流

任务生命周期调度决策调频联动平台定制任务创建 forkenqueue_task_fair更新 PELT 负载check_preempt_curr需要调度?__schedulepick_next_task_fairEAS 启用?find_energy_efficient_cpuload_balancesugov_update_singleschedutil 选频cpufreq 驱动rockchip_performance leveluclamp_min_rttarget_load=80

附录 A:源码文件与关键函数索引

源码中搜索 可定位全部中文注释。

A.1 按文件

文件 调度方法/职责 关键函数
core.c 统一框架 __schedule, try_to_wake_up, pick_next_task, context_switch, check_preempt_curr
fair.c CFS (NORMAL/BATCH/IDLE) enqueue_entity, pick_next_entity, check_preempt_wakeup, load_balance, select_task_rq_fair, find_energy_efficient_cpu
rt.c RT (FIFO/RR) enqueue_task_rt, pick_next_task_rt, check_preempt_curr_rt, _pick_next_task_rt
deadline.c DL (EDF+CBS) enqueue_task_dl, pick_next_task_dl, update_curr_dl, dl_task_offline_migration
idle.c idle 线程 do_idle, cpu_idle_poll, pick_next_task_idle
stop_task.c stop 任务 pick_next_task_stop
pelt.c 负载跟踪 ___update_load_sum, ___update_load_avg
topology.c 调度域 init_sched_domains, build_sched_domains
cpufreq_schedutil.c 调频 sugov_update_single, sugov_should_update_freq
autogroup.c 终端分组 sched_autogroup_create_attach, sched_autogroup_fork
core_sched.c SMT cookie sched_core_alloc_cookie, sched_core_free
cpupri.c RT 迁移 cpupri_find, cpupri_set
cpudeadline.c DL 迁移 cpudl_find, cpudl_set
sched.h 内部类型 struct sched_class, struct rq, struct cfs_rq

A.2 核心调用链

阻塞 → 唤醒 → 抢占 → 切换

1
2
3
4
5
6
7
8
9
10
11
12
13
schedule() / preempt
└─ __schedule() [core.c:6569]
├─ deactivate_task() 睡眠出队
├─ pick_next_task() [core.c:6039]
│ └─ __pick_next_task() [core.c:5957]
│ ├─ 快速路径: pick_next_task_fair()
│ └─ 慢路径: for_each_class → pick_next_task
└─ context_switch() MM + switch_to

try_to_wake_up() [core.c]
├─ select_task_rq() 各 class->select_task_rq
├─ activate_task() → enqueue_task()
└─ check_preempt_curr() 可能 resched_curr()

Tick 驱动

1
2
3
4
5
6
7
timer interrupt
└─ scheduler_tick() [core.c]
├─ curr->sched_class->task_tick()
│ ├─ entity_tick() [fair.c] CFS 时间片检查
│ ├─ task_tick_rt() [rt.c] RR 时间片
│ └─ task_tick_dl() [deadline.c]
└─ trigger_load_balance() [fair.c] 周期性负载均衡

A.3 编译单元(Makefile 拆分)

.o 文件 包含源文件
core.o core.c, clock.c, completion.c, cpuacct.c, cputime.c, loadavg.c, stats.c
fair.o fair.c
build_policy.o idle.c, rt.c, deadline.c, pelt.c, stop_task.c
build_utility.o topology.c, psi.c, cpufreq*.c, wait.c, debug.c, autogroup.c

文档基于 RK3588 / Linux 6.1 内核 kernel/sched 源码整理。

上下文切换与状态保存详解

上下文切换与状态保存详解

源码路径rk3588/kernel-6.1/kernel/sched/arch/arm64/
内核版本:Linux 6.1(RK3588 / arm64)
关联文档sched调度机制与原理详解.md

调度切换时,不会把「整个进程」拷贝一份。上一任务的状态分三层保存:

  1. 一直在内存里的进程数据(堆栈、文件描述符等)
  2. 调度器在切换前更新的调度状态(vruntime、运行队列位置等)
  3. CPU 寄存器 / 栈等执行上下文switch_to 时保存)

目录


一、什么要保存,什么不用动

数据类型 存放位置 切换时是否拷贝
堆、栈、全局变量(用户内存) 进程的 mm 页表里,物理内存中 不拷贝,仍在原处
打开的文件、信号、fd 表 task_struct / files_struct 已在内核,不用每次切换保存
调度信息(vruntime、优先级、运行队列位置) task_struct + sched_entity 切换前由调度类更新
CPU 寄存器、内核栈指针、返回地址 task_struct->thread switch_to 时保存/恢复
页表(地址空间) mm_struct 切换 mm,不复制页表内容

用户代码里的局部变量、全局变量在用户栈/堆里,切换后仍在原物理页上;下次再调度到该 task,只要恢复寄存器和页表,就能从断点继续。


二、完整切换流程

__schedule 选中 nextprev 要睡眠?deactivate_task 出队<br/>task_struct 仍存活pick_next_taskput_prev_task<br/>CFS 更新 vruntime 等context_switchswitch_mm 换页表switch_to / cpu_switch_to<br/>保存 prev 寄存器在 next 上继续执行finish_task_switch<br/>在 prev 栈上收尾

关键代码路径

core.c__schedule()prev != next 时调用 context_switch()

1
2
3
4
5
6
7
8
// core.c: __schedule()
if (likely(prev != next)) {
rq->nr_switches++;
RCU_INIT_POINTER(rq->curr, next);
++*switch_count;
// ...
rq = context_switch(rq, prev, next, &rf);
}

context_switch() 步骤(core.c 注释):

  1. prepare_task_switch:perf / rseq / switched_from 钩子
  2. 切换 mm(用户态 / 内核态 / 惰性 TLB 等分支)
  3. switch_to:汇编切换栈与寄存器
  4. finish_task_switch:返回时在 prev 上下文执行清理

三、task_struct:进程的档案袋

每个进程/线程对应一个 task_struct,长期保存在内核内存,切换时不销毁

字段/子结构 作用
mm 地址空间(页表根)
thread CPU 上下文(寄存器、FPU 等)
policy / prio / sched_class 调度策略与类
sched_entity CFS 的 vruntime、红黑树节点
filessignalfs 文件、信号、文件系统状态

出队 vs 销毁

  • 被抢占(仍 TASK_RUNNING):仍在运行队列,task_struct 不变,只是暂时不是 rq->curr
  • 主动阻塞(sleep / mutex):deactivate_task() 出队,但 task_structmm 都还在
  • 进程退出TASK_DEAD 后,在 finish_task_switch() 中释放栈和 task_struct

四、调度类保存调度状态

在真正换 CPU 上下文前,各调度类的 put_prev_task() 会更新统计信息。

CFS(fair.cput_prev_entity

1
2
3
4
5
6
7
8
9
10
11
static void put_prev_entity(struct cfs_rq *cfs_rq, struct sched_entity *prev)
{
if (prev->on_rq)
update_curr(cfs_rq); // 累加 vruntime、exec_start 等

if (prev->on_rq) {
__enqueue_entity(cfs_rq, prev); // 仍 runnable 则插回红黑树
update_load_avg(cfs_rq, prev, 0); // 更新 PELT 负载
}
cfs_rq->curr = NULL;
}

保存的是:vruntime、PELT 负载、在 CFS 红黑树中的位置,不是用户变量。

RT / Deadline

类似地更新运行时间、RR 时间片、DL runtime/deadline 预算等,写入 task_struct / sched_dl_entity 相关字段。


五、context_switch:地址空间与寄存器

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
// core.c: context_switch()
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
{
prepare_task_switch(rq, prev, next);
arch_start_context_switch(prev);

/* 地址空间切换四种情况:
* kernel → kernel 惰性 TLB,复用 active_mm
* user → kernel 惰性 TLB,mmgrab active_mm
* kernel → user switch_mm 加载页表
* user → user switch_mm 切换页表
*/
if (!next->mm) { // to kernel thread
enter_lazy_tlb(prev->active_mm, next);
next->active_mm = prev->active_mm;
// ...
} else { // to user task
switch_mm_irqs_off(prev->active_mm, next->mm, next);
// ...
}

prepare_lock_switch(rq, next, rf);

/* Here we just switch the register state and the stack. */
switch_to(prev, next, prev);
barrier();

return finish_task_switch(prev);
}

地址空间(mm)

  • 用户 task → 用户 taskswitch_mm() 加载 next 的页表(arm64 上如 TTBR0_EL1
  • 同进程多线程:共享同一 mm,页表往往不变,切换更轻
  • 切到内核线程(无 mm):惰性 TLB,借用 active_mm

用户内存数据在页表里,只换「当前用哪套页表」,不搬数据。


六、arm64 寄存器保存(cpu_switch_to)

调用链:switch_to__switch_to()arch/arm64/kernel/process.c)→ cpu_switch_toarch/arm64/kernel/entry.S

cpu_context 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// arch/arm64/include/asm/processor.h
struct cpu_context {
unsigned long x19, x20, x21, x22, x23, x24;
unsigned long x25, x26, x27, x28;
unsigned long fp; // x29
unsigned long sp; // 内核栈指针
unsigned long pc; // 返回地址 (lr)
};

struct thread_struct {
struct cpu_context cpu_context;
// TLS、FPSIMD/SVE 浮点与向量状态、断点、MTE 等
// ...
};

汇编核心逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// arch/arm64/kernel/entry.S: cpu_switch_to
// x0 = prev task_struct, x1 = next task_struct
SYM_FUNC_START(cpu_switch_to)
mov x10, #THREAD_CPU_CONTEXT
add x8, x0, x10 // &prev->thread.cpu_context
mov x9, sp
stp x19, x20, [x8], #16 // 保存 callee-saved 寄存器
stp x21, x22, [x8], #16
stp x23, x24, [x8], #16
stp x25, x26, [x8], #16
stp x27, x28, [x8], #16
stp x29, x9, [x8], #16 // fp, sp
str lr, [x8] // pc

add x8, x1, x10 // &next->thread.cpu_context
ldp x19, x20, [x8], #16 // 恢复 next 的寄存器
// ... 对称 ldp ...
mov sp, x9 // 切换到 next 的内核栈
msr sp_el0, x1 // 用户态 SP 关联 next 的 task_struct
ret // 跳到 next 上次保存的 lr 继续执行
SYM_FUNC_END(cpu_switch_to)

__switch_to 额外切换的状态

__switch_to()cpu_switch_to 前后还处理:

函数 保存/恢复内容
fpsimd_thread_switch() NEON / FP 寄存器 → thread.uw.fpsimd_state
tls_thread_switch() TLS 寄存器 tpidr_el0
hw_breakpoint_thread_switch() 硬件断点
mte_thread_switch() MTE 标签检查状态
ptrauth_thread_switch_user() 指针认证密钥

七、用户态寄存器何时保存

若上一任务在用户态被 tick 中断或 syscall 打断:

  1. 进内核时:异常入口(entry.S)把 x0–x30、pc、pstate 等压到该 task 的内核栈struct pt_regs
  2. 调度时cpu_switch_to 再保存 callee-saved + sp/lrthread.cpu_context
  3. 下次恢复:先 cpu_switch_to 恢复内核上下文,再从内核栈 pt_regs 恢复用户寄存器,返回用户态

若在内核态被抢占(如持锁路径),主要保存内核 callee-saved 和栈;用户寄存器通常已在更早的入口保存过,或本就在内核执行。


八、finish_task_switch 收尾

switch_to 返回时,CPU 已在 next 上运行,但代码仍借用 prev 的内核栈 做收尾:

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
// core.c: finish_task_switch()
static struct rq *finish_task_switch(struct task_struct *prev)
{
struct rq *rq = this_rq();
struct mm_struct *mm = rq->prev_mm;

rq->prev_mm = NULL;
prev_state = READ_ONCE(prev->__state);

vtime_task_switch(prev); // CPU 时间统计
perf_event_task_sched_in(prev, current);
finish_task(prev);
finish_lock_switch(rq); // 释放 rq->lock
finish_arch_post_lock_switch();

if (mm)
mmdrop_sched(mm); // 释放借用的 mm 引用

if (unlikely(prev_state == TASK_DEAD)) {
put_task_stack(prev);
put_task_struct_rcu_user(prev); // 进程真正退出时释放
}

return rq;
}

九、典型场景对照

场景 保存/切换内容
线程 A → 线程 B(同 mm 各自 task_struct->thread 的寄存器/FPU;页表不变
进程 A → 进程 B 寄存器 + 切换 mm(页表)
运行 → sleep 出队 + 保存上下文;唤醒后重新入队再恢复
运行 → 运行(抢占) TASK_RUNNING;CFS 插回红黑树 + 保存寄存器
进程退出 TASK_DEADfinish_task_switch 释放栈和 task_struct

十、源码阅读顺序

建议按以下顺序对照阅读:

顺序 文件 函数/符号
1 kernel/sched/core.c __schedule()
2 kernel/sched/core.c pick_next_task() / context_switch()
3 kernel/sched/fair.c put_prev_entity() / pick_next_entity()
4 arch/arm64/kernel/process.c __switch_to()
5 arch/arm64/kernel/entry.S cpu_switch_to
6 arch/arm64/include/asm/processor.h struct thread_struct / cpu_context
7 kernel/sched/core.c finish_task_switch()

总结

用户数据(堆栈变量)一直在进程的地址空间里,不用在切换时拷贝;内核把「还能不能跑、该跑多久」写在 task_struct 和调度队列里;真正「断点续跑」靠的是把 CPU 寄存器、内核栈、页表指针存进/读出 task_struct->threadmm,下次 cpu_switch_to 恢复后继续执行。


文档基于 RK3588 / Linux 6.1 内核 kernel/schedarch/arm64 源码整理。

调度算法原理详解(深入版)

调度算法原理详解(深入版)

源码路径rk3588/kernel-6.1/kernel/sched/
内核版本:Linux 6.1(RK3588 / arm64)
关联文档sched调度机制与原理详解.md · 上下文切换与状态保存详解.md

本文从算法与数学模型层面详细描述 Linux 6.1 调度器:公式、数据结构、逐步算法、数值示例、边界条件及 SMP 扩展。阅读时可对照 fair.c / rt.c / deadline.c / pelt.c 源码(搜索 可定位中文注释)。


目录


符号与约定

符号 含义
w / weight CFS 权重,nice 0 时为 NICE_0_LOAD = 1024
vruntime 虚拟运行时间(ns),CFS 公平性核心
delta_exec 本次实际运行时间(ns)
period / P CFS 调度周期或 DL 任务周期
slice CFS 物理时间片(ns)
C, T, D DL 任务 runtime、period、deadline(ns)
prio 内核优先级,数值越小优先级越高(RT/DL)
util_avg PELT 利用率,1024 = 100% 单核

一、CFS 完全公平调度算法

策略SCHED_NORMAL / SCHED_OTHER / SCHED_BATCH / SCHED_IDLE
文件fair.c · 调度类fair_sched_class

1.1 理论背景:加权公平队列(WFQ)

CFS 可视为 Weighted Fair Queueing 的近似实现:

  • 理想目标:每个任务 i 在窗口 T 内获得 CPU 时间 T × (w_i / Σw_j)
  • 实现手段:维护虚拟时间 vruntime始终让 vruntime 最小的任务运行
  • 与传统固定时间片轮转的区别:时间片随任务数和权重动态变化,无全局固定 quantum

1.2 权重与 nice 映射

nice 范围 -20 ~ +19,对应 prio_to_weight[] 查表:

nice weight(约) 相对 nice 0 的 CPU 份额
-20 88761 ~86.7×
-10 9548 ~9.3×
0 1024 1×(基准)
+10 110 ~0.11×
+19 15 ~0.015×

用户态 nice(2) / setpriority() 修改的是 动态优先级 static_prio,CFS 将其映射为 load.weight

1.3 vruntime 更新公式(核心)

任务运行 delta_exec 纳秒后:

1
2
3
Δvruntime = calc_delta_fair(delta_exec, se)
= delta_exec × (NICE_0_LOAD / se.load.weight) // weight ≠ 1024 时
= delta_exec // weight = 1024 时

update_curr() 完整步骤(每次 tick、切换、入队前):

1
2
3
4
5
6
1. delta_exec = rq_clock_task() - curr->exec_start
2. curr->exec_start = now
3. curr->sum_exec_runtime += delta_exec // 累计物理 CPU 时间
4. curr->vruntime += calc_delta_fair(delta_exec, curr)
5. update_min_vruntime(cfs_rq) // 推进队列基准
6. account_cfs_rq_runtime(cfs_rq, delta_exec) // cgroup 配额统计

数值示例:两任务公平性

假设 nice 0(w=1024)与 nice 10(w≈110)同队列运行:

事件 Task A (nice 0) Task B (nice 10)
各跑 10ms 物理时间 vruntime += 10ms vruntime += 10ms × (1024/110) ≈ 93ms
谁更该运行? vruntime 较小者先运行 B 的 vruntime 涨得快 → A 更容易被选中
长期比例 A 获得 CPU ≈ 1024/(1024+110) ≈ 90% B ≈ 10%

这与权重比例 w_A : w_B 一致。

1.4 min_vruntime 与 vruntime 归一化

min_vruntime 更新

1
min_vruntime = max(旧 min_vruntime, leftmost.vruntime, curr.vruntime)

作用:

  1. 新任务放置基准:防止 vruntime=0 的新任务长期霸占 CPU
  2. 跨 CPU 迁移:出队/入队时做加减归一化

跨 rq 迁移规则(enqueue_entity / dequeue_entity

1
2
3
4
5
6
7
8
9
10
出队(dequeue):
update_curr()
update_min_vruntime()
se->vruntime -= cfs_rq->min_vruntime // 存相对值

入队(enqueue):
update_curr()
update_min_vruntime()
se->vruntime += cfs_rq->min_vruntime // 加目标 rq 基准
place_entity() // 唤醒时再调整

保证不同 CPU 上 vruntime 可比,且迁移瞬间 min_vruntime 已在两侧同步更新。

1.5 调度周期与时间片(详细)

周期 period

1
2
3
4
5
6
7
sched_nr_latency = sched_latency / sched_min_granularity    // 默认 6ms/0.75ms = 8

__sched_period(nr_running):
if nr_running > sched_nr_latency:
return nr_running × sched_min_granularity // 扩展周期,避免 slice 过小
else:
return sched_latency // 默认 6ms × (1 + ilog2(ncpus))

RK3588 8 核示例ilog2(8)=3):

1
2
3
4
5
6
sched_latency           = 6ms × 4 = 24ms
sched_min_granularity = 0.75ms × 4 = 3ms
sched_nr_latency = 8

若 nr_running = 4: period = 24ms
若 nr_running = 16: period = 16 × 3ms = 48ms(而非 24ms)

时间片 slice

1
2
3
4
5
6
sched_slice(cfs_rq, se):
period = __sched_period(nr_running)
slice = period × (se.weight / cfs_rq.load.weight)

// cgroup 层次:沿 for_each_sched_entity(se) 逐层按比例切分
// BASE_SLICE:slice = max(slice, sched_min_granularity)

数值示例:period=24ms,3 个 nice 0 任务(各 w=1024,总 weight=3072):

1
每人 slice = 24ms × (1024/3072) = 8ms

若其中 1 个 nice 10(w=110),总 weight=2134:

1
2
nice 0 任务 slice ≈ 24ms × (1024/2134) ≈ 11.5ms
nice 10 任务 slice ≈ 24ms × (110/2134) ≈ 1.2ms → 钳位到 min_granularity 3ms

vslice(虚拟时间片)

1
sched_vslice(cfs_rq, se) = calc_delta_fair(sched_slice(cfs_rq, se), se)

用于 place_entity(START_DEBIT) 预扣一片,以及 tick 抢占比较。

1.6 place_entity:vruntime 放置(完整规则)

1
2
3
4
5
6
7
8
9
10
11
12
13
// fair.c: place_entity(cfs_rq, se, initial)
vruntime = cfs_rq->min_vruntime;

if (initial && START_DEBIT):
vruntime += sched_vslice(cfs_rq, se); // fork:预扣一片,不立刻抢占父进程

if (!initial): // 唤醒
thresh = (SCHED_IDLE) ? min_granularity : sched_latency;
if (GENTLE_FAIR_SLEEPERS): thresh >>= 1; // 睡眠补偿减半,更温和
vruntime -= thresh; // 向前借 vruntime,补偿 sleep

se->vruntime = max_vruntime(se->vruntime, vruntime);
// 超长睡眠任务 entity_is_long_sleeper:直接用 vruntime,防 s64 溢出
场景 initial 效果
fork 1 vruntime = min + vslice,新任务略”吃亏”
唤醒 0 vruntime 最多向前借 latency(或一半),IO 完成后更快运行
迁移唤醒 0 + MIGRATED exec_start 清零,cache 不视为 hot

1.7 红黑树与 enqueue / dequeue

数据结构

1
2
3
4
5
6
7
8
struct cfs_rq {
struct rb_root_cached tasks_timeline; // vruntime 排序
struct sched_entity *curr; // 当前运行,不在树中
u64 min_vruntime;
struct sched_entity *next, *last, *skip; // buddy
unsigned int nr_running;
u64 load; // 总 weight
};

enqueue_entity 逐步算法

1
2
3
4
5
6
7
8
1. 若 WAKEUP/MIGRATED:renorm vruntime += min_vruntime
2. update_curr(cfs_rq)
3. 若非 curr:renorm vruntime += min_vruntime
4. update_load_avg() // PELT
5. account_entity_enqueue() // nr_running++, load.weight += se.weight
6. 若 ENQUEUE_WAKEUP:place_entity(se, 0)
7. 若 !curr:__enqueue_entity() 插入红黑树
8. se->on_rq = 1

dequeue_entity 要点

1
2
3
4
5
1. update_curr()
2. 若 WAKEUP/MIGRATED:vruntime -= min_vruntime(归一化)
3. 若 se != curr:从树移除
4. update_load_avg(DO_DETACH)
5. account_entity_dequeue()

set_next_entity / put_prev_entity

1
2
3
4
5
6
7
8
set_next_entity(选中运行):
__dequeue_entity() // curr 移出树
exec_start = now
cfs_rq->curr = se

put_prev_entity(停止运行):
if (on_rq): update_curr(); __enqueue_entity() // 插回树
cfs_rq->curr = NULL

1.8 pick_next_entity(完整优先级)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. left = 红黑树最左(vruntime 最小)
2. 若 curr 存在且 entity_before(curr, left):left = curr
3. se = left

4. 若 skip buddy 存在且 skip 不是 left:
尝试选 __pick_next_entity(skip) 作为 second
若 second 存在且 wakeup_preempt_entity(second, left) < 1:
se = second // 跳过 yield 标记的任务

5. 若 next buddy 且 wakeup_preempt_entity(next, left) <= 0:
se = next // 刚唤醒,cache 热
6. else 若 last buddy 且 wakeup_preempt_entity(last, left) <= 0:
se = last // 刚被抢占,cache 热

7. return se

wakeup_preempt_entity(a, b) 返回值:

1
2
3
4
vdiff = a.vruntime - b.vruntime
vdiff <= 0 → -1 (a 不应抢占 b,a 的 vruntime 更大或相等)
vdiff > gran → 1 (a 应抢占 b)
else → 0 (差距不够,不抢占)

1.9 三类抢占(详细条件)

(1)Tick 抢占 — check_preempt_tick

1
2
3
4
5
6
7
8
9
10
11
12
13
ideal_runtime = sched_slice(cfs_rq, curr)
delta_exec = curr.sum_exec_runtime - curr.prev_sum_exec_runtime

条件 A:delta_exec > ideal_runtime
→ resched_curr(); clear_buddies(); return

条件 B:delta_exec < min_granularity
→ return(运行太短,不抢占)

条件 C:leftmost = __pick_first_entity()
delta = curr.vruntime - leftmost.vruntime
delta > ideal_runtime
→ resched_curr()(curr vruntime 已领先太多,该让 leftmost 运行)

条件 C 防止 wake 抢占 narrowly missed 时还要等满一个 slice。

(2)唤醒抢占 — check_preempt_wakeup

1
2
3
4
5
6
7
8
9
1. 若 cgroup throttle:return
2. 若 nr_running >= sched_nr_latency:set_next_buddy(wakee)
3. 若 curr 已有 TIF_NEED_RESCHED:return
4. 若 curr 是 IDLE 策略且 wakee 不是:preempt
5. 若 wakee 是 BATCH 或 WAKEUP_PREEMPTION 关闭:return(靠 tick 驱动)
6. update_curr()
7. 若 wakeup_preempt_entity(curr, wakee) == 1:
set_next_buddy(wakee); resched_curr()
8. 若 LAST_BUDDY:set_last_buddy(curr)(被抢占者标记 cache hot)

wakeup_gran(se)sysctl_sched_wakeup_granularity 按 weight 缩放,高权重任务 gran 更大(更难被抢占)。

(3)主动让出 — yield_task_fair

1
2
3
set_skip_buddy(curr)           // 标记 skip
若 curr 仍在树中:requeue
resched_curr()

下次 pick_next_entity 会尽量跳过 skip buddy。

1.10 CFS Bandwidth Control(cgroup CPU 配额)

1
2
3
4
5
6
7
8
9
10
11
cfs_bandwidth:
quota = 每 period 允许运行的 ns(如 50000us)
period = 统计周期(如 100000us)

运行时:account_cfs_rq_runtime(cfs_rq, delta_exec)
超限:throttle_cfs_rq() → 组内任务 dequeue,hrtimer 下一 period 解除

与 vruntime 关系:
- quota 是硬上限(cgroup 层面)
- vruntime 是组内/全局公平(权重层面)
二者独立同时生效

1.11 SCHED_BATCH / SCHED_IDLE

策略 算法差异
SCHED_BATCH check_preempt_wakeup 直接 return;减少 wake 抢占;倾向连续运行
SCHED_IDLE 极低 weight;se_is_idle() 路径;sched_idle_min_granularity;仅当无正常 CFS 任务时运行

1.12 CFS 完整生命周期示例

场景:Task X read() 阻塞,IO 完成后唤醒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. read() 阻塞 → schedule() → deactivate_task(DEQUEUE_SLEEP)
put_prev_entity → update_curr → 插回红黑树

2. IO 完成 → try_to_wake_up(X)
select_task_rq_fair() 选 CPU(可能 EAS)
activate_task → enqueue_entity(ENQUEUE_WAKEUP)
place_entity:vruntime -= latency(补偿 sleep)
check_preempt_wakeup → 若 vdiff > gran → resched_curr

3. 内核抢占点 → __schedule()
pick_next_entity → 可能选 next_buddy(X)
context_switch → X 运行

4. 每个 tick → entity_tick → update_curr → check_preempt_tick
入队运行抢占enqueue_entityrenorm vruntimeupdate_load_avg PELTplace_entity 唤醒补偿插入红黑树set_next_entity 出树运行 on CPUupdate_curr 累加 vruntimetick / wake / yieldcheck_preempt_*put_prev_entity 插回树pick_next_entity

二、RT 实时调度算法

策略SCHED_FIFO / SCHED_RR
文件rt.c · 调度类rt_sched_class

2.1 优先级体系(易混淆点)

层级 范围 说明
用户 sched_param.sched_priority 1–99 sched_setscheduler() 传入
内核 task_struct->prio 0–99 数值越小优先级越高
映射 prio = MAX_RT_PRIO - 1 - user_prio 或等价变换

CFS 任务 prio = 120 + nice(约 100–139),永远低于 RT

2.2 数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
struct rt_rq {
struct rt_prio_array active; // active.queue[100] + active.bitmap
unsigned int rt_nr_running;
u64 rt_time; // 本周期已运行 RT 时间
int rt_throttled;
struct rt_bandwidth rt_bandwidth;
};

struct sched_rt_entity {
struct list_head run_list; // 同优先级 FIFO 链
unsigned long timeout; // RR 用
struct sched_rt_entity *back; // group scheduling
};

选任务 _pick_next_task_rt

1
2
3
4
5
6
7
8
rt_se = pick_next_rt_entity(&rq->rt):
idx = find_first_bit(active.bitmap) // 最高优先级非空档
return list_first_entry(active.queue[idx])

若 CONFIG_RT_GROUP_SCHED:
while (group_rt_rq(rt_se)):
沿组层次向下递归 pick
return rt_task_of(rt_se)

复杂度:O(1)(bitmap 常数 100 + 链表头)。

2.3 SCHED_FIFO 详细行为

1
2
3
4
5
6
7
入队:enqueue_task_rt → 插入 active[prio] 链表尾部
运行:一直运行直到
(a) sched_yield / 阻塞(mutex、wait)
(b) 更高 prio RT 唤醒 → check_preempt_curr_rt
(c) RT bandwidth 耗尽 → 整组 throttle
(d) 更高调度类(DL)抢占
不出队:除非主动阻塞;不会因时间片到而轮转

2.4 SCHED_RR 详细行为

1
2
3
4
5
6
7
8
9
10
11
12
sched_rr_timeslice = RR_TIMESLICE / HZ ≈ 100ms(可 sysctl 调节)

fork/唤醒时:p->rt.time_slice = sched_rr_timeslice

task_tick_rt(每个 tick):
update_curr_rt() // 统计 + 带宽检查
if (policy != SCHED_RR) return
if (--time_slice > 0) return
time_slice = sched_rr_timeslice
if (同优先级队列中不只有自己):
requeue_task_rt() // 移到队尾
resched_curr()

同优先级多任务 RR 时间线(prio=50,slice=100ms):

1
2
3
4
T0: Task1 运行
T100ms: tick → Task1 队尾,Task2 运行
T200ms: Task2 队尾,Task1 运行
...

2.5 update_curr_rt 与带宽 throttle

1
2
3
4
5
6
7
8
9
10
11
update_curr_rt():
delta_exec = now - exec_start
exec_start = now

for each rt_rq in hierarchy:
rt_rq->rt_time += delta_exec
if sched_rt_runtime_exceeded(rt_rq):
rt_rq->rt_throttled = 1
sched_rt_rq_dequeue() // 整组 RT 出队
do_start_rt_bandwidth() // hrtimer 下一 period
resched_curr()

sched_rt_runtime_exceeded

1
2
3
4
5
6
runtime_limit = sched_rt_runtime_us(默认 950ms)
period = sched_rt_period_us(默认 1s)

if rt_rq->rt_time > runtime_limit:
rt_throttled = 1
return 1

效果:每 1 秒窗口内 RT 最多跑 950ms,剩余 ≥50ms 给 CFS/idle,防锁死。

2.6 SMP:RT 迁移(cpupri + push/pull)

1
2
3
4
5
6
7
8
9
10
11
12
问题:多 RT 任务挤在同一 CPU,其他核空闲

cpupri:
每个 CPU 记录该核 RT 队列最高 prio
cpupri_find() O(1) 找能运行 p 的最低 prio CPU

push:
put_prev_task_rt → enqueue_pushable_task()
RT push IPI → 目标核 pull_rt_task()

pull:
空闲核 balance_rt() 从 busy 核拉 RT 任务

2.7 RT 与 CFS/DL 关系

1
2
3
4
5
选任务顺序:stop > dl > rt > fair > idle

RT 运行中:
CFS 任务无法被选中(除非 RT throttle 且 DL 为空)
DL 任务 deadline 更早者可以抢占 RT(动态 prio 计算)

三、Deadline 调度算法(EDF + CBS)

策略SCHED_DEADLINE
文件deadline.c · 调度类dl_sched_class

3.1 任务模型与参数

通过 sched_setattr() 设置 struct sched_attr

字段 符号 含义
sched_runtime C 每个 job 需要的 CPU 时间(预算,ns)
sched_period T 任务周期(ns)
sched_deadline D 相对 deadline(ns),通常 D ≤ T

语义:每 T 时间内需完成 C 单位 CPU 工作;每个 job 须在绝对 deadline 前完成。

参数示例

1
2
3
C = 2ms, T = 10ms, D = 10ms(implicit deadline,D = T)
→ 带宽 U = C/T = 20%
→ 每 10ms 窗口至少跑 2ms CPU,否则 miss deadline
1
2
C = 3ms, T = 10ms, D = 5ms(constrained deadline,D < T)
→ 更紧的 deadline,带宽仍 U = 30%,但 job 须 5ms 内完成

3.2 EDF(Earliest Deadline First)

1
2
3
4
5
dl_rq 红黑树按 sched_dl_entity.deadline(绝对时间)升序

pick_next_task_dl():
se = 树中 deadline 最小的实体
return task_of(se)

单核最优性:若任务集在单核上可调度,EDF 可找到可行调度(利用率 ≤ 100% 时)。

动态内核 prio

1
2
dl_prio(deadline) = MAX_DL_PRIO - 1 - (deadline >> DL_SCALE)
deadline 越早 → prio 数值越小 → 优先级越高

3.3 运行时:update_curr_dl

1
2
3
4
5
6
7
8
9
now = rq_clock_task(rq)
delta_exec = now - dl_se->exec_start
exec_start = now

dl_se->runtime -= delta_exec // 消耗当前 job 预算

if (runtime <= 0):
__dequeue_dl_entity() // throttle,移出运行队列
start_dl_timer() // hrtimer 在 deadline 触发 replenish

3.4 CBS(Constant Bandwidth Server)详解

设计动机

纯 EDF 下,任务 overrun(跑超 C)会推迟后续 job 的 deadline,可能拖累其他任务。CBS 保证每个任务带宽不超过 U = C/T,overrun 只影响自身。

replenish_dl_entity(预算补充)

1
2
3
4
5
6
7
8
while (runtime <= 0):
deadline += dl_period // 推迟绝对 deadline
runtime += dl_runtime // 补充预算

if (deadline < rq_clock): // 滞后过多
replenish_dl_new_period() // 重置:deadline = now + D, runtime = C

dl_throttled = 0

关键:overrun 时通过 deadline += period 把任务「推」到未来,而非无限占用 CPU。

唤醒规则 update_dl_entity

1
2
3
4
5
6
7
8
if (deadline 已过期 || dl_entity_overflow(now)):

if (constrained deadline: D < T) && !implicit && !boosted:
update_dl_revised_wakeup() // Revised CBS:缩减 runtime,不超 U
else:
replenish_dl_new_period() // Original CBS
deadline = now + D
runtime = C

overflow 判定 dl_entity_overflow

概念上检查:

1
runtime / (deadline - now)  >  dl_runtime / dl_deadline

即:剩余预算相对剩余时间的比例超过声明带宽 → 不能沿用当前 deadline/runtime,必须 replenish 或 revised wakeup。

3.5 准入控制(Admission Control)

1
2
3
4
5
6
7
8
root_domain->dl_bw 维护系统 DL 总带宽

新任务入队前:
Σ (C_i / T_i) + C_new/T_new <= GLOBAL_DL_BW

GLOBAL_DL_BW 默认约为 0.95 × 总 CPU 容量(留 5% 给 CFS)

sched_setattr() 失败 → EINVAL,防止不可调度任务集

3.6 完整 CBS 时间线示例

任务:C=2ms, T=10ms, D=10ms,t=0 启动。

1
2
3
4
5
6
t=0ms:   enqueue, deadline=10ms, runtime=2ms, 开始运行
t=2ms: runtime=0, throttle 出队, 启动 timer(deadline=10ms)
t=2~10ms: 不运行(即使 CPU 空闲,CBS 限速;除非 GRUB 回收策略)
t=10ms: hrtimer → replenish: deadline=20ms, runtime=2ms, 重新入队
t=10ms: 若 EDF 最高则运行
...

若 t=0~3ms 跑满 3ms(overrun 1ms):

1
2
3
4
t=3ms:   runtime=-1ms → replenish 循环:
deadline: 10→20ms, runtime: -1+2=1ms
继续跑 1ms 至 runtime=0
→ 总占用 3ms,但 deadline 被推到 20ms,带宽仍 ≤ C/T
CBSdl_rqDL JobCBSdl_rqDL Jobloop[每次 update_curr_dl]等待至 deadlineenqueue(deadline=D0, runtime=C)EDF 选中运行runtime -= deltaruntime<=0 throttlehrtimer(deadline)replenish deadline+=T runtime+=C重新入队竞争

3.7 DL 与 RT/CFS 优先级

1
2
3
4
5
6
调度类链:stop > dl > rt > fair

同 CPU 上:
DL 任务按 deadline 与 RT prio 动态比较
DL 通常高于 CFS
多个 DL 任务之间纯 EDF

四、PELT 负载跟踪算法

文件pelt.c · 头文件sched.h / pelt.h

4.1 数学模型

将历史负载表示为几何级数

1
2
3
4
load_avg = u_0 + u_1·y + u_2·y² + u_3·y³ + ...

y = 0.5^(1/32) ≈ 0.9786
y^32 = 0.5 → 约 32ms 前的贡献权重减半

时间轴按 1024μs(≈1ms) 分段:

1
2
3
[--1024us--][--1024us--][--1024us--]...
p0 p1 p2
(当前) (~1ms前) (~2ms前)

u_i = 第 i 段内实体可运行比例 × 权重(load)或运行比例(util)。

4.2 三个平均值

字段 计算来源 用途
load_sumload_avg runnable × weight CFS 迁移权重
runnable_sumrunnable_avg 是否 runnable 负载均衡 imbalance
util_sumutil_avg 是否 actually running schedutil 调频、EAS、misfit

转换(简化):

1
2
load_avg = load_sum × load_avg_inv >> LOAD_AVG_SHIFT
util_avg = util_sum × LOAD_AVG_MAX >> SCHED_CAPACITY_SHIFT // 1024 = 100%

4.3 accumulate_sum 逐步算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
输入:delta(ns), load, runnable, running 标志

1. delta += period_contrib
2. periods = delta / 1024
3. if periods > 0:
load_sum = decay_load(load_sum, periods)
runnable_sum = decay_load(runnable_sum, periods)
util_sum = decay_load(util_sum, periods)
delta %= 1024
contrib = __accumulate_pelt_segments(periods, ...) // 跨段几何求和
4. period_contrib = delta
5. load_sum += load * contrib
6. runnable_sum += runnable * contrib << SCHED_CAPACITY_SHIFT
7. util_sum += running * contrib << SCHED_CAPACITY_SHIFT

4.4 decay_load O(1) 实现

1
2
3
4
5
6
decay_load(val, n):
if n >= 32*63: return 0
val >>= n / 32 // 每 32 period 减半
n %= 32
val = val * yN_inv[n] >> 32 // 余数查表
return val

避免 O(n) 循环,tick 路径高效。

4.5 更新时机与意义

事件 更新
enqueue_entity DO_ATTACH,runnable 增加
dequeue_entity DO_DETACH
update_curr / entity_tick running 贡献 util
migration 双方 rq 的 cfs_rq avg

物理含义util_avg=512 ≈ 该 task 近期平均占用 50% 单核算力(已按 CPU capacity 缩放)。


五、SMP 负载均衡算法

文件fair.cload_balance)、topology.c

5.1 sched_domain 层次(RK3588)

1
2
3
DIE 域(8 CPUs,全芯片)
└── MC 域(4 CPUs,同 cluster:A76×4 或 A55×4)
└── CPU 域(单逻辑 CPU)

域标志:

标志 含义
SD_LOAD_BALANCE 允许本层 load balance
SD_ASYM_CPUCAPACITY 非对称容量(big.LITTLE)
SD_SHARE_CPUCAPACITY 共享 L2/L3 cache
SD_WAKE_AFFINE 唤醒亲和

5.2 负载不平衡度量

1
2
3
4
5
6
load = runnable_avg(或 misfit 时用 util)

imbalance = busiest_load - dst_load
(经 capacity 缩放、group 权重、NUMA 因子 adjust)

目标:使各 CPU load/capacity 趋于均衡

5.3 load_balance 完整步骤

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
load_balance(this_cpu, sd, idle_type):

1. should_we_balance(env)
- 本 CPU 是否 busy/idle 符合 idle_type
- 域 busy_idx 与 idle_idx 是否有效
- nohz 平衡是否允许

2. find_busiest_group(env)
遍历 sched_group,算 group_load / group_capacity
选 load 最高且超过阈值的组

3. find_busiest_queue(env, group)
组内找 load 最高的 rq

4. detach_tasks(env)
从 busiest 摘下可迁移任务(can_migrate_task)
最多 sysctl_sched_nr_migrate 个
任务标记 TASK_ON_RQ_MIGRATING

5. attach_tasks(env)
挂到 this_rq,触发 remote enqueue

6. 若 active_balance 仍不平衡:
stop_one_cpu_nowait(busiest, active_balance)
在 busy 核上强制 push

5.4 can_migrate_task 约束

1
2
3
4
5
- cpus_allowed 掩码
- cache_hot:刚运行任务不迁移(除非 idle balance 紧急)
- nr_running:busy 核仅 1 个任务时不 pull(避免 ping-pong)
- misfit:高 util 任务在小核 → 优先迁大核
- throttled cgroup 任务不迁移

5.5 触发路径

路径 函数 场景
周期 tick scheduler_ticktrigger_load_balancerebalance_domains 每 ~4ms softirq
newidle newidle_balance CPU 即将 idle 前主动 pull
唤醒 select_task_rq_fair 选 CPU,非严格 load_balance
active active_balance misfit / 持续不平衡

5.6 wake_affine

1
2
3
4
5
唤醒时 select_task_rq_fair:
if (prev_cpu 空闲 && wake_affine 域标志 && sync 唤醒):
倾向 prev_cpu(L1/L2 cache 仍热)
else:
find_idlest_cpu / find_energy_efficient_cpu

六、EAS 能耗感知选核算法

文件fair.cfind_energy_efficient_cpu
平台:RK3588 4×A76 + 4×A55

6.1 启用条件(全部满足)

  1. root_domain->pd(Energy Model perf_domain)非空
  2. !rd->overutilized(系统未全局过载)
  3. cpufreq 为 schedutil
  4. 存在 SD_ASYM_CPUCAPACITY 调度域
  5. EM 复杂度 < EM_MAX_COMPLEXITY(2048)

6.2 算法逐步(find_energy_efficient_cpu)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
输入:task p, prev_cpu

1. 若 !pd || overutilized || util_est=0 && uclamp_min=0 → fallback

2. sync_entity_load_avg(&p->se)
eenv_task_busy_time(&eenv, p, prev_cpu)

3. for each perf_domain pd(A76 域、A55 域):
for each cpu in pd:
util = cpu_util_next(cpu, p) // 预测放置 p 后的 util
if !util_fits_cpu(util, uclamp): // 容量不够则 skip
continue
spare_cap = capacity - util
记录 max_spare_cap_cpu

base_energy = compute_energy(prev_cpu, pd)
for candidate in pd:
cur_delta = compute_energy(candidate) - base_energy
选 cur_delta 最小的 candidate

4. 在 prev_cpu 与 best_energy_cpu 间比较 energy_delta
5. return 能耗增量最小的 CPU

6.3 compute_energy 概念

1
2
3
4
5
6
7
Energy = Σ_cpu  power(freq(cpu)) × time

power 来自 Energy Model 表(频率-功耗曲线)
freq 由预测 util 经 schedutil 映射

迁移收益 = E(run on candidate) - E(run on prev_cpu)
选 ΔE 最小(或为负且绝对值最大)的核

6.4 RK3588 典型决策

任务 util 典型选择 原因
低(<512) A55 小核能效高
高(>512) A76 A55 capacity 不足,misfit
比较 ΔE EAS 能量差决定
1
cpu_capacity: A76 ≈ 1024, A55 ≈ 512(相对值,arch_scale_cpu_capacity)

七、Idle / Stop 调度

7.1 idle_sched_class(idle.c

1
2
3
4
5
6
7
每 CPU 一个 idle 线程,非 SCHED_IDLE 策略

do_idle() 循环:
cpuidle_idle_call() 或 poll
if need_resched: schedule_idle()

仅当 fair/rt/dl 均无 runnable 时被 pick_next_task 选中

7.2 stop_sched_class(stop_task.c

1
2
3
stop_machine 等内核操作使用
绝对最高优先级,不抢占、不让出、不迁移
pick_next_task_stop → rq->stop

八、算法对比与选型

维度 CFS RT Deadline
目标 比例公平 + 交互响应 低延迟确定性 周期 deadline 保证
优先级 动态 vruntime 静态 1–99 动态 deadline
数据结构 vruntime 红黑树 100 级 bitmap+FIFO deadline 红黑树
时间模型 动态 slice RR: ~100ms 固定 C/T 预算周期
带宽限制 cgroup quota 950ms/s 全局 CBS + 准入 95%
抢占 gran + tick 立即(高 prio) 更早 deadline
过载行为 公平分摊 throttle 全 RT CBS 推迟 deadline
SMP PELT + LB + EAS cpupri push/pull cpudl 迁移
典型用例 普通 App 音视频 pipeline 工业控制、PLC

选型建议

1
2
3
4
普通线程 / 99% 场景     → SCHED_OTHER (CFS)
已知优先级、可接受秒级抖动 → SCHED_FIFO/RR + 带宽意识
硬实时、周期可建模 → SCHED_DEADLINE(需正确 C/T/D)
极低优先级后台 → SCHED_IDLE

九、源码函数与算法步骤对照

CFS(fair.c)

函数 算法步骤
calc_delta_fair delta × NICE_0_LOAD / weight
__sched_period 按 nr_running 选 latency 或 nr×min_gran
sched_slice period × weight/total,cgroup 分层
update_curr vruntime += calc_delta_fair;update_min_vruntime
place_entity START_DEBIT / 唤醒补偿
enqueue_entity / dequeue_entity 归一化 vruntime;PELT;红黑树
pick_next_entity leftmost + skip/next/last buddy
wakeup_preempt_entity vdiff vs gran → {-1,0,1}
check_preempt_tick slice 耗尽 / vruntime 领先过多
check_preempt_wakeup BATCH 过滤;wake 抢占
load_balance busiest → detach → attach
find_energy_efficient_cpu EM ΔE 最小 CPU

RT(rt.c)

函数 算法步骤
enqueue_task_rt 插入 active[prio]
_pick_next_task_rt bitmap + FIFO
check_preempt_curr_rt wakee.prio < curr.prio
task_tick_rt RR time_slice–;requeue
update_curr_rt rt_time += delta;throttle 检查
sched_rt_runtime_exceeded rt_time > 950ms

Deadline(deadline.c)

函数 算法步骤
enqueue_task_dl update_dl_entity;EDF 入树
pick_next_task_dl 最小 deadline
update_curr_dl runtime -= delta;throttle
replenish_dl_entity while runtime≤0: deadline+=T, runtime+=C
update_dl_entity overflow → Original/Revised CBS
dl_entity_overflow 带宽比例比较
start_dl_timer hrtimer @ deadline

PELT(pelt.c)

函数 算法步骤
decay_load O(1) × y^n
accumulate_sum 跨 1024us 段衰减+累加
___update_load_avg sum → avg

附录:sysctl 与默认参数

CFS

sysctl 默认(单核基准) 多核缩放
sched_latency_ns 6ms × (1 + ilog2(ncpus))
sched_min_granularity_ns 0.75ms × (1 + ilog2(ncpus))
sched_wakeup_granularity_ns 1ms × (1 + ilog2(ncpus))
sched_nr_migrate 32 单次 LB 最多迁移任务数
sched_autogroup_enabled 1 终端 autogroup

RT

sysctl 默认
sched_rt_period_us 1,000,000(1s)
sched_rt_runtime_us 950,000(950ms)
RR timeslice ~100ms

DL

sysctl 默认
sched_deadline_period_max_us ~4s
sched_deadline_period_min_us 100us
全局 DL 带宽 ~95% CPU

EAS / 其他

接口 说明
/proc/sys/kernel/sched_energy_aware EAS 开关
schedutil/target_load Rockchip 调频曲线(默认 80)

文档基于 RK3588 / Linux 6.1 内核 kernel/sched 源码整理。

kernel/time 内核时间与定时器机制与原理详解

kernel/time 内核时间与定时器机制与原理详解

源码路径rk3588/kernel-6.1/kernel/time/
内核版本:Linux 6.1(RK3588 平台)
平台:RK3588(4×Cortex-A76 + 4×Cortex-A55 big.LITTLE,ARM64)

该目录实现 Linux 内核 时间子系统核心:wall clock 维护(timekeeping)、时钟源/时钟事件设备管理、jiffies、调度时钟 sched_clock()、高精度定时器(hrtimer)、低精度定时器轮(timer wheel)、POSIX 定时器、NTP 调整、NO_HZ tickless idle、alarmtimer 以及 VDSO 时间页更新。

定位:内核「时间基础设施」;硬件时钟源驱动在 drivers/clocksource/(如 ARM Generic Timer),架构入口在 arch/arm64/


目录


一、时间子系统概览

Linux 时间子系统分为 四个层次

层次 职责 典型组件
硬件 提供自由运行计数器与可编程定时中断 ARM Generic Timer(CNTPCT/CNTVCT)
抽象层 注册/选择 clocksource 与 clockevent clocksource.c / clockevents.c
时间维护 将硬件 tick 转换为 wall clock / monotonic timekeeping.c / ntp.c
定时器 API 内核/用户态定时需求 hrtimer / timer wheel / POSIX timers

两类硬件抽象

  • Clocksource(时钟源):只读、单调递增,用于 读当前时间(如 ktime_get()
  • Clockevent(时钟事件):可编程,用于 产生 tick 中断(调度、hrtimer、jiffies 推进)

二、源码目录结构

2.1 编译依赖(Makefile)

1
2
3
4
5
6
7
8
9
10
11
obj-y += time.o timer.o hrtimer.o
obj-y += timekeeping.o ntp.o clocksource.o jiffies.o timer_list.o
obj-y += timeconv.o timecounter.o alarmtimer.o

obj-$(CONFIG_GENERIC_CLOCKEVENTS) += clockevents.o tick-common.o
obj-$(CONFIG_GENERIC_CLOCKEVENTS_BROADCAST) += tick-broadcast.o
obj-$(CONFIG_TICK_ONESHOT) += tick-oneshot.o tick-sched.o
obj-$(CONFIG_GENERIC_SCHED_CLOCK) += sched_clock.o
obj-$(CONFIG_HAVE_GENERIC_VDSO) += vsyscall.o
obj-$(CONFIG_POSIX_TIMERS) += posix-timers.o posix-cpu-timers.o ...
obj-$(CONFIG_TIME_NS) += namespace.o
文件 作用
timekeeping.c 核心时间维护:xtime、monotonic、raw、fast timekeeper
clocksource.c 时钟源注册、rating 选择、watchdog
clockevents.c 时钟事件设备管理、状态切换
hrtimer.c 高精度 per-CPU 红黑树定时器
timer.c 多级 timer wheel 低精度定时器
tick-common.c tick 设备、do_timer()、jiffies 推进协调
tick-sched.c NO_HZ / HIGH_RES_TIMERS tick 调度
tick-broadcast.c 深度 idle CPU 的 broadcast tick
sched_clock.c 扩展 sched_clock() 到 64 位 ns
ntp.c NTP PLL 频率/相位调整
time.c gettimeofday / clock_settime / adjtimex 等 syscall
posix-timers.c timer_create / clock_nanosleep
alarmtimer.c 可唤醒系统的 alarm 定时器
vsyscall.c VDSO datapage 更新(ARM64 通用实现)

三、Kconfig 与 RK3588 默认配置

3.1 关键 Kconfig 选项

配置项 说明
CONFIG_GENERIC_CLOCKEVENTS 通用 clockevent 框架(非 LEGACY)
CONFIG_GENERIC_CLOCKEVENTS_BROADCAST 深度 idle 时 broadcast tick
CONFIG_TICK_ONESHOT 单次触发 clockevent(hrtimer/NO_HZ 依赖)
CONFIG_NO_HZ_IDLE Tickless idle(默认 NO_HZ=y 时)
CONFIG_NO_HZ_FULL 全动态 tick(需 CPU 隔离)
CONFIG_HIGH_RES_TIMERS 高精度 hrtimer
CONFIG_GENERIC_SCHED_CLOCK 通用 sched_clock 扩展
CONFIG_HAVE_GENERIC_VDSO VDSO 时间页
CONFIG_POSIX_TIMERS POSIX 定时器
CONFIG_TIME_NS 时间命名空间

3.2 RK3588 Rockchip defconfig

arch/arm64/configs/rockchip_linux_defconfig 为例:

1
2
CONFIG_NO_HZ=y
CONFIG_HIGH_RES_TIMERS=y

ARM64 Kconfig 还 select:

  • GENERIC_CLOCKEVENTS_BROADCAST — big.LITTLE 深 idle 时需要 broadcast
  • ARCH_HAS_TICK_BROADCAST
  • HAVE_GENERIC_VDSO — 用户态 clock_gettime 走 VDSO

含义:RK3588 启用 tickless idle + 高精度定时器;空闲时停止 periodic tick 以省电,唤醒时由 hrtimer/clockevent 补偿 jiffies。


四、整体架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 ┌─────────────────────────────────────────────────────────────┐
│ drivers/clocksource/ │
│ arm_arch_timer.c (CNTPCT, ~24MHz) │
└───────────────┬─────────────────────────┬───────────────────┘
│ clocksource │ clockevent (PPI)
▼ ▼
┌───────────────────────┐ ┌──────────────────────────────┐
│ clocksource.c │ │ clockevents.c + tick-*.c │
│ curr_clocksource │ │ tick_handler → hrtimer │
└───────────┬───────────┘ └──────────────┬───────────────┘
│ │
▼ ▼
┌───────────────────────────────────────────────────────────┐
│ timekeeping.c │
│ tk_core (seqlock) + tk_fast (NMI-safe latch) │
│ xtime_sec / wall_to_monotonic / raw_sec │
└───────────┬───────────────────────────────┬───────────────┘
│ │
┌────────┴────────┐ ┌────────┴────────┐
▼ ▼ ▼ ▼
ktime_get() vsyscall.c hrtimer.c timer.c
sched_clock() VDSO update 高精度定时器 timer wheel
ntp.c 调整 用户态零 syscall nanosleep 等 mod_timer 等

数据流:硬件 counter → clocksource 读取消耗 → timekeeper 累加 → ktime_get* / VDSO;clockevent 中断 → tick → hrtimer 到期处理 +(必要时)jiffies 推进。


五、Clocksource 时钟源(clocksource.c)

5.1 核心概念

1
2
3
4
5
6
7
8
struct clocksource {
const char *name;
u64 (*read)(struct clocksource *cs);
u64 mask;
u32 mult, shift; /* 硬件 tick → ns 的缩放因子 */
int rating; /* 越高越优先 */
...
};
  • clocks_calc_mult_shift():计算 mult/shift,保证 maxsec 内不 64 位溢出
  • clocksource_register() / clocksource_select():按 rating 选择 curr_clocksource
  • Watchdog:周期性比对两 clocksource,检测 TSC/arch_timer 漂移

5.2 时钟源层次(RK3588)

名称 rating 来源 用途
arch_sys_counter 450+ ARM Generic Timer 主 clocksource
jiffies 1 jiffies.c 兜底、boot 早期

驱动位于 drivers/clocksource/arm_arch_timer.c

  • CNTPCT(物理计数器)或 CNTVCT(虚拟计数器,KVM guest)
  • 频率由 CNTFRQ 寄存器或 DT clock-frequency 提供(通常 24 MHz)
  • 同时注册 clocksource 与 per-CPU clockevent(PPI 中断)
  • 支持 event streamCONFIG_ARM_ARCH_TIMER_EVTSTREAM)辅助 NO_HZ

六、Clockevents 时钟事件(clockevents.c)

6.1 设备状态机

1
2
3
4
5
6
7
enum clock_event_state {
CLOCK_EVT_STATE_DETACHED,
CLOCK_EVT_STATE_SHUTDOWN,
CLOCK_EVT_STATE_PERIODIC, /* 周期性 HZ tick */
CLOCK_EVT_STATE_ONESHOT, /* 单次触发(hrtimer/NO_HZ) */
CLOCK_EVT_STATE_ONESHOT_STOPPED,
};
  • clockevents_register_device():注册到 per-CPU tick_cpu_device
  • clockevents_switch_state():在 periodic ↔ oneshot 间切换
  • clockevent_delta2ns():设备 tick 数 → 纳秒

6.2 与 Tick 的关系

每个 CPU 有一个 struct tick_devicetick-common.c),绑定本 CPU 的 clockevent。Tick 处理函数 tick_handle_periodictick_handle_oneshottick-common.c / tick-sched.c 注册。


七、Timekeeping 时间维护(timekeeping.c)

7.1 核心结构

1
2
3
4
5
6
7
8
9
static struct {
seqcount_raw_spinlock_t seq;
struct timekeeper timekeeper;
} tk_core;

struct tk_fast { /* NMI 安全快速路径 */
seqcount_latch_t seq;
struct tk_read_base base[2]; /* 双缓冲 latch */
};

struct timekeeper 包含:

  • xtime_sec / tkr_mono.xtime_nsecCLOCK_REALTIME
  • wall_to_monotonic — realtime ↔ monotonic 偏移
  • raw_sec / tkr_rawCLOCK_MONOTONIC_RAW(不受 NTP 调整)
  • tkr_mono / tkr_raw — 关联 clocksource 及 cycle_last

7.2 时间更新路径

  1. Tick 路径update_wall_time() — 每个 tick(或 NO_HZ 补偿多个 tick)读 clocksource delta,累加 xtime
  2. Fast pathktime_get() / ktime_get_mono_fast() — 读 tk_fast_mono,无锁 latch 序列
  3. Settimetimekeeping_set_tk_clock() / do_settimeofday64() — 持 timekeeper_lock 写 shadow copy 后 swap

7.3 关键 API

函数 时钟类型
ktime_get() CLOCK_MONOTONIC
ktime_get_real() CLOCK_REALTIME
ktime_get_boottime() CLOCK_BOOTTIME
ktime_get_raw() CLOCK_MONOTONIC_RAW
ktime_get_coarse() 粗粒度 monotonic(仅读 xtime_sec)

八、Jiffies 与低精度定时器(jiffies.c / timer.c)

8.1 Jiffies

1
__visible u64 jiffies_64 __cacheline_aligned_in_smp = INITIAL_JIFFIES;
  • 全局 tick 计数,精度 = 1/HZ
  • jiffies_lock + jiffies_seq 保护 32 位架构上的 64 位读
  • clocksource_jiffies:rating=1 的兜底 clocksource

8.2 Timer Wheel(timer.c)

Linux 6.1 使用 多级 timer wheel(非 classic 级联 wheel):

1
2
3
4
#define LVL_CLK_SHIFT  3
#define LVL_CLK_DIV 8
#define LVL_DEPTH 8 (或 9,64 位)
#define LVL_SIZE 64
层级 粒度(HZ=250 时约) 覆盖范围
Level 0 4 ms 短超时
Level 1 32 ms
每级 ×8
Level 7/8 ~18 天 最大超时
  • mod_timer() / add_timer() — 内核低精度定时器
  • 基于 struct timer_list,在 run_timer_softirq 中处理
  • 适合网络/块 I/O 超时等 大量可能被 cancel 的 timer

九、高精度定时器 hrtimer(hrtimer.c)

9.1 Per-CPU 基础结构

1
DEFINE_PER_CPU(struct hrtimer_cpu_base, hrtimer_bases);

每个 CPU 有 8 个 clock base(hard + soft):

Base Clock ID 说明
MONOTONIC CLOCK_MONOTONIC 常用
REALTIME CLOCK_REALTIME
BOOTTIME CLOCK_BOOTTIME 含 suspend 时间
TAI CLOCK_TAI
*_SOFT 同上 在 softirq 上下文执行 callback

定时器按 expiry 插入 红黑树;最近 expiry 编程到 clockevent oneshot。

9.2 核心 API

1
2
3
hrtimer_init(&timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
hrtimer_start(&timer, ns, HRTIMER_MODE_REL);
hrtimer_cancel(&timer);
  • schedule_hrtimeout() — 可睡眠的精确延时
  • nanosleep / clock_nanosleep syscall 底层
  • hrtimer_interrupt — tick 或 clockevent 触发,处理到期 timer

9.3 Softtimer

HRTIMER_BASE_*_SOFT 的 callback 在 HRTIMER_SOFTIRQ 执行,允许 callback 中使用 mutex 等可能睡眠的操作(仍不可真正 schedule)。


十、Tick 子系统(tick-*.c)

10.1 tick-common.c

  • tick_handle_periodic / tick_do_timer — 全局 jiffies 推进(仅一个 CPU 负责 do_timer()tick_do_timer_cpu
  • tick_setup_periodic — 初始化 periodic tick
  • tick_program_event — 编程 oneshot 下次事件

10.2 tick-sched.c(NO_HZ / HIGH_RES_TIMERS)

1
static DEFINE_PER_CPU(struct tick_sched, tick_cpu_sched);
  • NO_HZ idle:CPU idle 时停止 tick;唤醒时 tick_do_update_jiffies64() 补偿多个 jiffies
  • NO_HZ full:运行用户态任务时也停 tick(需 nohz_full= 启动参数)
  • tick_nohz_idle_stop_tick() / tick_nohz_idle_restart_tick()
  • 与 RCU、scheduler loadavg、cputime Accounting 协同

10.3 tick-broadcast.c

当 CPU 进入 C3stop(arch timer 在 deep idle 停止)时,local clockevent 不可用,由 broadcast 设备 代发 tick。RK3588 big.LITTLE 中 A55 深 idle 常走此路径。

10.4 tick-oneshot.c

Clockevent 切换到 oneshot 模式的辅助;hrtimer 编程 next event 时使用。


十一、sched_clock 调度时钟(sched_clock.c)

1
2
3
4
5
6
unsigned long long notrace sched_clock(void)
{
/* seqcount_latch 双缓冲读 */
cyc = read_sched_clock() - epoch_cyc;
return epoch_ns + cyc_to_ns(cyc, mult, shift);
}
  • 提供 单调 ns 计数,供 scheduler、tracing、perf 使用
  • 早期用 jiffies;sched_clock_register() 后切换为 arch_timer
  • sched_clock_tick() — 定期 extend 64 位范围,防 counter 回绕
  • 与 timekeeping 独立:sched_clock 偏性能(可 slightly 漂移),wall clock 偏准确

十二、NTP 时间调整(ntp.c)

实现 adjtimex(2) / NTP PLL:

变量 含义
time_offset 相位偏移(ns)
time_freq 频率偏移(scaled ppm)
time_constant PLL 时间常数
time_status STA_PLL / STA_UNSYNC 等标志
tick_length 每 tick 实际纳秒长度(含调整)
  • ntp_update_frequency() — 应用频率校正
  • second_overflow() — 每秒边界处理 leap second
  • timekeeping.ctk_set_xtime / timekeeping_adjust 协同
  • RK3588 嵌入式场景通常 不跑 ntpd,但 adjtimex 仍可用于手动校时

十三、系统调用与用户接口(time.c)

Syscall 功能
gettimeofday / clock_gettime 读时间(优先 VDSO)
settimeofday / clock_settime 设置系统时间
adjtimex NTP 参数调整
clock_getres 时钟分辨率
times 进程时间统计

sys_tz 导出时区;实际 wall clock 以 UTC 存储,用户态 libc 转换时区。


十四、POSIX 定时器(posix-timers.c)

  • timer_create / timer_settime / timer_gettime / timer_delete
  • clock_nanosleep — 线程级 nanosleep
  • 基于 hrtimertimerqueue 实现
  • 哈希表管理 per-process timer ID(512 桶)
  • posix-cpu-timers.cCLOCK_PROCESS_CPUTIME_ID / setitimer CPU 时间限制
  • itimer.c — 传统 interval timer(SIGALRM 等)

十五、Alarmtimer 与 RTC 唤醒(alarmtimer.c)

  • 类似 hrtimer,但 suspend 时可借助 RTC 硬件唤醒
  • Android 风格 alarm 接口;支持 CLOCK_REALTIME_ALARM / CLOCK_BOOTTIME_ALARM
  • drivers/rtc/ 协作,系统 suspend 前设置 RTC alarm
  • RK3588 上 RTC 通常为 RK808/RK809 等 PMIC 内 RTC

十六、VDSO 时间页(vsyscall.c)

ARM64 通用 VDSO 更新(CONFIG_HAVE_GENERIC_VDSO):

1
2
3
4
5
6
7
8
9
void update_vsyscall(struct timekeeper *tk)
{
vdata[CS_HRES_COARSE].cycle_last = tk->tkr_mono.cycle_last;
vdata[CS_HRES_COARSE].mult = tk->tkr_mono.mult;
/* CLOCK_MONOTONIC / BOOTTIME / TAI / REALTIME basetime */
vdso_write_begin(vdata);
...
vdso_write_end(vdata);
}
  • 用户态 clock_gettime() 直接读 shared page(vdso_data),零 syscall
  • CS_HRES_COARSE:高精度 coarse 模式;CS_RAW:MONOTONIC_RAW
  • timekeeper 更新时调用 update_vsyscall();seqlock 保证读者一致性
  • ARM64 VDSO 使用 arch timer 模式(VDSO_CLOCKMODE_ARCHTIMER

十七、辅助模块

文件 作用
timecounter.c 通用 timecounter/cyclecounter 辅助(驱动常用)
timeconv.c 时间单位转换
timer_list.c /proc/timer_list 调试接口
namespace.c 时间命名空间(CONFIG_TIME_NS
posix-clock.c 动态 POSIX clock 设备
timekeeping_debug.c debugfs 时间统计
clocksource-wdtest.c Watchdog 测试
test_udelay.c udelay 校准测试

十八、RK3588/ARM64 平台说明

18.1 硬件时间源

组件 说明
ARM Generic Timer 每核 CNTPCT @ CNTFRQ(通常 24 MHz);PPI 27/30 定时中断
Architected Timer 系统 counter ≥56 bit,40 年不回绕
RK PMU Timer 平台特定,非主 timekeeping
RTC PMIC RTC,仅 wall clock 备份与 wakealarm

驱动:drivers/clocksource/arm_arch_timer.c
DT binding:Documentation/devicetree/bindings/timer/arm,arch_timer.yaml

18.2 配置总结

项目 RK3588 典型值
NO_HZ y(tickless idle)
HIGH_RES_TIMERS y
HZ 通常 100/250/300(由 CONFIG_HZ 决定)
Clocksource arch_sys_counter
Clockevent arch_timer per-CPU
Broadcast 启用(deep idle A55 核)
VDSO 启用(clock_gettime 用户态 fast path)
sched_clock arch_timer 扩展

18.3 big.LITTLE 注意点

  • Per-CPU tick:每核独立 clockevent 与 hrtimer base;NO_HZ 各核独立停/启 tick
  • Broadcast tick:little 核深 idle 时 timer 停,需 broadcast 维持 RCU/sched deadline
  • Timekeeping CPUtick_do_timer_cpu 指定单一 CPU 推进 global jiffies,避免 thundering herd
  • CPU hotplug:核下线/上线时 tick 设备迁移,timekeeping 不变
  • Virtual counter:KVM guest 使用 CNTVCT;host 用 CNTPCT

18.4 与调度/电源的交叉

  • CFS 调度sched_clock() 计算 vruntime
  • NO_HZ idle:与 cpuidle 配合,A55 核 idle 停 tick 省电
  • Hrtimer:multimedia pipeline、音频(ALSA)等依赖高精度超时
  • Printk timestamp:可选 PRINTK_TIME_FROM_ARM_ARCH_TIMER 直接用 arch timer

十九、完整 Tick 中断时序

timer wheeltimekeepinghrtimertick_handlerclockeventARM Generic Timertimer wheeltimekeepinghrtimertick_handlerclockeventARM Generic TimerNO_HZ: 编程下次最近 event,可能跳过 tickPPI 中断 (oneshot/periodic)tick_handle_oneshot()hrtimer_interrupt()红黑树取出到期 timer,执行 callbackupdate_wall_time() (若需推进)clocksource delta → xtime_nsecrun_timer_softirq (via raise_softirq)clockevents_program_event(next)

二十、总结

要点 说明
双抽象 clocksource 读时间,clockevent 产生事件
timekeeping 单一 timekeeper + fast latch,NMI 安全
双定时器 hrtimer(高精度 RB-tree)+ timer wheel(低精度海量 timeout)
NO_HZ idle 停 tick 省电,唤醒补偿 jiffies
VDSO 用户态无 syscall 读时间
RK3588 ARM arch timer + tickless + hrtimer;8 核 broadcast 支持 deep idle

kernel/time/ 是调度、RCU tick、POSIX、tracing、media 等子系统的时间基础;性能与功耗调优常涉及 NO_HZ、hrtimer 密度与 broadcast 行为。


附录:源文件清单

文件 行数 说明
timekeeping.c 2503 时间维护核心
hrtimer.c 2394 高精度定时器
timer.c 2166 Timer wheel
posix-cpu-timers.c 1693 CPU 时间定时器
tick-sched.c 1626 NO_HZ tick 调度
clocksource.c 1519 时钟源管理
posix-timers.c 1458 POSIX 定时器
tick-broadcast.c 1235 Broadcast tick
ntp.c 1095 NTP 调整
alarmtimer.c 964 Alarm 定时器
time.c 909 系统调用
clockevents.c 778 时钟事件
tick-common.c 564 Tick 公共逻辑
namespace.c 467 时间命名空间
itimer.c 403 Interval timer
timer_list.c 360 /proc 调试
posix-clock.c 320 动态 POSIX clock
sched_clock.c 296 sched_clock
posix-stubs.c 254 无 POSIX 时的 stub
clocksource-wdtest.c 202 Watchdog 测试
tick-internal.h 199 Tick 内部头文件
vsyscall.c 170 VDSO 更新
test_udelay.c 159 udelay 测试
timeconv.c 141 时间转换
tick-oneshot.c 128 Oneshot tick
tick-broadcast-hrtimer.c 111 Broadcast hrtimer
jiffies.c 104 Jiffies clocksource
time_test.c 99 KUnit 测试
timecounter.c 99 timecounter 辅助
timekeeping_debug.c 55 debugfs
posix-timers.h 45 POSIX 内部头
timekeeping_internal.h 39 timekeeping 内部
tick-legacy.c 37 遗留 tick
timekeeping.h 34 timekeeping 头
ntp_internal.h 22 NTP 内部
合计 22753

行数统计:Linux 6.1,wc -l kernel/time/*.{c,h}

kernel/trace 内核追踪(ftrace/tracing)机制与原理详解

kernel/trace 内核追踪(ftrace/tracing)机制与原理详解

源码路径rk3588/kernel-6.1/kernel/trace/
内核版本:Linux 6.1(RK3588 平台)
平台:RK3588(4×Cortex-A76 + 4×Cortex-A55 big.LITTLE,ARM64)

该目录实现 Linux 内核 ftrace / tracing 子系统:函数追踪、tracepoint 事件、延迟分析、动态探针(kprobe/uprobe)、BPF 挂载、块 I/O 追踪、Runtime Verification 等。数据经 per-CPU ring buffer 缓冲,通过 tracefs/sys/kernel/debug/tracing/)对用户空间暴露控制与读取接口。

定位:内核可观测性核心;tracepoint 定义分散于各子系统 include/trace/events/,架构 ftrace 入口在 arch/arm64/kernel/ftrace.centry-common.S


目录


一、追踪子系统概览

Linux tracing 解决「在不改内核源码的前提下,观测内核运行时行为」:

能力 机制 典型用途
函数追踪 mcount/fentry + dynamic ftrace 调用链、性能热点
静态事件 TRACE_EVENT tracepoint sched、irq、block、power
动态探针 kprobe/uprobe 任意内核/用户函数探测
延迟分析 irqsoff/wakeup/hwlat tracer 实时性、中断延迟
BPF bpf_trace.c 可编程追踪、perf 集成
聚合 hist trigger + tracing_map 直方图、延迟分布

设计原则:写路径 per-CPU 无锁;关闭 tracing 时 dynamic ftrace patch 为 NOP,接近零开销。


二、源码目录结构

2.1 Kconfig 依赖树

1
2
3
4
5
6
7
8
9
10
11
12
TRACING_SUPPORT
└── FTRACE (menuconfig "Tracers")
├── RING_BUFFER ← 环形缓冲区
├── TRACING ← trace.c 核心框架
├── FUNCTION_TRACER ← ftrace.c 函数追踪
├── EVENT_TRACING ← trace_events.c
├── KPROBE_EVENTS ← trace_kprobe.c
├── UPROBE_EVENTS ← trace_uprobe.c
├── BPF_EVENTS ← bpf_trace.c
├── HIST_TRIGGERS ← trace_events_hist.c
├── FPROBE / RETHOOK ← fprobe.c / rethook.c
└── RV ← rv/ 运行时验证

2.2 编译依赖(Makefile)

1
2
3
4
5
6
7
8
9
obj-$(CONFIG_FUNCTION_TRACER) += libftrace.o          # ftrace.c
obj-$(CONFIG_RING_BUFFER) += ring_buffer.o
obj-$(CONFIG_TRACING) += trace.o trace_output.o trace_seq.o ...
obj-$(CONFIG_EVENT_TRACING) += trace_events.o trace_events_filter.o ...
obj-$(CONFIG_KPROBE_EVENTS) += trace_kprobe.o
obj-$(CONFIG_BPF_EVENTS) += bpf_trace.o
obj-$(CONFIG_FPROBE) += fprobe.o
obj-$(CONFIG_RETHOOK) += rethook.o
obj-$(CONFIG_RV) += rv/

注意ccflags-remove-$(CONFIG_FUNCTION_TRACER) += $(CC_FLAGS_FTRACE) — trace 目录自身 不被 ftrace 插桩,避免递归。

2.3 源文件分类

分类 主要文件 说明
核心 trace.c, trace.h tracefs、tracer 管理、读写迭代器
存储 ring_buffer.c per-CPU 环形缓冲区
ftrace ftrace.c, fgraph.c dynamic patch、filter、function graph 辅助
输出 trace_output.c, trace_seq.c entry → 文本格式化
事件 trace_events.c, trace_events_*.c 注册、filter、trigger、hist
Tracer trace_functions*.c, trace_irqsoff.c, … 各 tracer 插件
探针 trace_kprobe.c, trace_uprobe.c, trace_probe.c 动态 event
BPF bpf_trace.c BPF 挂载 tracepoint/kprobe
钩子 fprobe.c, rethook.c 基于 ftrace 的 entry/exit 探针
RV rv/rv.c, rv/monitors/ 运行时形式化验证
辅助 trace_clock.c, pid_list.c, trace_boot.c 时钟、PID 过滤、启动 tracing

三、Kconfig 与 RK3588 默认配置

3.1 关键配置项

配置项 默认 说明
CONFIG_FUNCTION_TRACER 架构 select 函数入口插桩
CONFIG_DYNAMIC_FTRACE y 运行时 NOP ↔ call patch
CONFIG_DYNAMIC_FTRACE_WITH_REGS ARM64 y 回调可获 pt_regs
CONFIG_FUNCTION_GRAPH_TRACER y 函数调用图 + 耗时
CONFIG_EVENT_TRACING FUNCTION_TRACER 时 tracepoint 框架
CONFIG_KPROBE_EVENTS y(KPROBES) 动态 kprobe event
CONFIG_UPROBE_EVENTS y(ARM64) 用户态探针
CONFIG_BPF_EVENTS BPF+PERF BPF 程序挂载
CONFIG_FPROBE n 轻量 ftrace 探针
CONFIG_RV n Runtime Verification

3.2 RK3588 defconfig

arch/arm64/configs/rockchip_linux_defconfig

1
CONFIG_FUNCTION_TRACER=y

ARM64 Kconfig 自动 select:

  • HAVE_FUNCTION_TRACER / HAVE_DYNAMIC_FTRACE
  • HAVE_DYNAMIC_FTRACE_WITH_REGS(GCC/Clang)
  • FTRACE_MCOUNT_USE_PATCHABLE_FUNCTION_ENTRY — 使用 __patchable_function_entries
  • HAVE_FTRACE_MCOUNT_RECORD

启用 FUNCTION_TRACER 后,通常连带启用 DYNAMIC_FTRACEFUNCTION_GRAPH_TRACEREVENT_TRACINGKPROBE_EVENTS 等(Kconfig default y)。


四、整体架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────────────────────┐
│ 用户空间: trace-cmd / perf / bpftool / cat trace_pipe │
└──────────────────────────┬──────────────────────────────────┘
│ tracefs (/sys/kernel/debug/tracing)
┌──────────────────────────▼──────────────────────────────────┐
│ trace.c — trace_array / tracer / trace_iterator / instances │
└───┬──────────┬───────────┬──────────┬─────────────────────┘
│ │ │ │
┌───▼───┐ ┌────▼────┐ ┌────▼────┐ ┌───▼──────────┐
│Tracer │ │ Events │ │ ftrace │ │ k/u/e probe │
│Plugins│ │ System │ │ Engine │ │ + BPF │
└───┬───┘ └────┬────┘ └────┬────┘ └───┬──────────┘
└──────────┴───────────┴──────────┘

┌────────────▼────────────┐
│ ring_buffer.c │
│ per-CPU Ring Buffer │
└────────────┬────────────┘

┌────────────▼────────────┐
│ 插桩: mcount/fentry │
│ tracepoint / kprobe │
│ uprobe / BPF / fprobe │
└─────────────────────────┘

数据流:插桩点触发 → ring_buffer_lock_reserve() 预留空间 → 写入 trace_entryring_buffer_unlock_commit() → 用户读 trace/trace_pipeprint_line / event format 格式化。


五、核心数据结构

5.1 trace_entry — 事件通用头

1
2
3
4
5
6
7
struct trace_entry {
unsigned short type; /* enum trace_type */
unsigned char flags;
unsigned char preempt_count;
int pid;
int padding;
};

5.2 trace_type — 内置类型

类型 含义
TRACE_FN 函数调用(function tracer)
TRACE_GRAPH_ENT/RET 函数图入口/返回
TRACE_CTX 上下文切换
TRACE_WAKE 唤醒
TRACE_PRINT trace_printk
TRACE_BLK 块 I/O
TRACE_HWLAT / TRACE_OSNOISE / TRACE_TIMERLAT 延迟/噪声

5.3 trace_array — 追踪实例

1
2
3
4
5
6
7
8
9
10
11
struct trace_array {
char *name;
struct array_buffer array_buffer; /* 主 ring buffer */
struct array_buffer max_buffer; /* 快照/最大延迟 buffer */
struct tracer *current_trace; /* 当前 tracer */
unsigned int trace_flags;
struct list_head events; /* 已注册 event */
struct dentry *dir; /* tracefs 目录 */
cpumask_var_t tracing_cpumask;
/* FUNCTION_TRACER: ftrace_ops, function_enabled */
};
  • 全局实例:top_trace_array() / global_trace_array
  • 子实例:/instances/<name>/ 独立 buffer 与 tracer

5.4 tracer — 插件虚函数表

1
2
3
4
5
6
7
8
9
10
11
12
struct tracer {
const char *name;
int (*init)(struct trace_array *tr);
void (*reset)(struct trace_array *tr);
void (*start)(struct trace_array *tr);
void (*stop)(struct trace_array *tr);
enum print_line_t (*print_line)(struct trace_iterator *iter);
int (*set_flag)(struct trace_array *tr, u32 old, u32 bit, int set);
struct tracer *next;
bool print_max;
bool allow_instances;
};

注册:register_tracer();用户选择:echo xxx > current_tracer


六、Ring Buffer 环形缓冲区

ring_buffer.c(~6234 行)是 tracing 的统一存储引擎。

6.1 设计要点

  • per-CPU buffer:写者只写本 CPU 页链表,无全局写锁
  • Reader page:读者专用页,读完后与 ring 中页 swap
  • 压缩时间戳:5-bit type_len + 27-bit delta;必要时 TIME_EXTEND / TIME_STAMP
  • 嵌套写入:中断/NMI 上下文可嵌套 reserve/commit
  • Splice:已读页可零拷贝传给用户态

6.2 页面模型

1
2
3
4
5
6
reader page          ring pages (per-CPU)
+--------+ +---+ → +---+ → +---+
| 读缓冲 | ←swap→ | | | | | |
+--------+ +---+ +---+ +---+
↑ │
└──────── writer 顺序写入 ──────────┘

6.3 主要 API

函数 功能
ring_buffer_alloc() 分配 per-CPU buffer
ring_buffer_lock_reserve() 预留 event 空间
ring_buffer_unlock_commit() 提交写入
ring_buffer_read() 读取下一 event
ring_buffer_overwrite() 覆盖模式(丢最旧)
ring_buffer_resize() 动态调整 buffer 大小

启动时 buffer 为最小尺寸(ring_buffer_expanded),首次启用 tracing 时扩展,避免空闲占内存。


七、ftrace 函数追踪引擎

7.1 编译期插桩(ARM64)

RK3588 使用 patchable function entries

  • 编译器 -mrecord-mcount-mfentry 在每个函数入口留下可 patch 点
  • __patchable_function_entries / mcount_loc 段记录所有位置
  • 架构代码:arch/arm64/kernel/ftrace.centry-ftrace.S

7.2 Dynamic Ftrace

ftrace.c(~8479 行)核心流程:

  1. Boot:所有插桩点 patch 为 AArch64 NOP(或 branch stub)
  2. Enable:NOP → bl ftrace_caller(或等价指令)
  3. Filterset_ftrace_filter / set_ftrace_notrace hash 表 per-ops 过滤
1
2
3
4
5
struct ftrace_ops *function_trace_op;
ftrace_func_t ftrace_trace_function;

int register_ftrace_function(struct ftrace_ops *ops);
int unregister_ftrace_function(struct ftrace_ops *ops);

ftrace_ops 字段:func 回调、flags(PID 过滤、SAVE_REGS、IPMODIFY 等)、func_hash filter。

7.3 Function Tracer 写入路径

1
2
3
4
5
6
7
some_func()
→ fentry/mcount stub
→ ftrace_caller
→ function_trace_call() [trace_functions.c]
→ ring_buffer_lock_reserve()
→ ftrace_entry { ip, parent_ip }
→ ring_buffer_unlock_commit()

7.4 Function Graph

trace_functions_graph.c + fgraph.c:在 entry 压栈返回地址,return 时弹栈计算 duration,输出缩进调用图。


八、Tracer 插件机制

8.1 已注册 Tracer

Tracer 源文件 功能
nop trace_nop.c 空 tracer,仅 tracepoint
function trace_functions.c 记录函数调用
function_graph trace_functions_graph.c 调用图 + 耗时
irqsoff / preemptoff / preemptirqsoff trace_irqsoff.c 中断/抢占关闭延迟
wakeup / wakeup_rt / wakeup_dl trace_sched_wakeup.c 唤醒延迟
hwlat trace_hwlat.c 硬件延迟(SMI 等)
osnoise / timerlat trace_osnoise.c OS 噪声 / 定时器延迟
branch trace_branch.c likely/unlikely 分支
blk blktrace.c 块 I/O
mmio trace_mmiotrace.c MMIO 访问

8.2 生命周期

1
2
3
4
5
6
7
register_tracer()  [模块 init]
→ echo xxx > current_tracer
→ tracer->init(tr) → tracer->start(tr)
→ ftrace_startup / 注册 tracepoint
... 运行中 print_line 格式化 ...
→ echo nop > current_tracer
→ tracer->stop(tr) → tracer->reset(tr)

8.3 Max Trace / Snapshot

支持 CONFIG_TRACER_MAX_TRACE 的 tracer(irqsoff、wakeup 等):

  • 延迟超过 tracing_max_latency 时 swap 到 max_buffer
  • snapshot 文件手动触发快照
  • trace 读 live 数据;max buffer 保留 worst-case 记录

九、Trace Event 事件系统

9.1 事件定义

静态 tracepoint(各子系统 include/trace/events/*.h):

1
2
3
4
5
6
7
TRACE_EVENT(sched_switch,
TP_PROTO(...),
TP_ARGS(...),
TP_STRUCT__entry(...),
TP_fast_assign(...),
TP_printk(...)
);

内置 ftrace entrytrace_entries.h):

1
FTRACE_ENTRY_REG(function, ftrace_entry, TRACE_FN, ...);

9.2 注册与管理(trace_events.c)

1
2
3
4
5
trace_event_reg()
→ 加入 ftrace_events 链表
→ tracefs: events/<system>/<event>/{enable,format,filter,id}
echo 1 > events/sched/sched_switch/enable
→ tracepoint 注册回调 → 写入 ring buffer

9.3 Filter(trace_events_filter.c)

1
echo 'pid == 1234 && comm == "myapp"' > events/.../filter

表达式编译为 predicate 字节码,写入前求值。

9.4 Trigger(trace_events_trigger.c)

Trigger 动作
snapshot 触发 buffer 快照
stacktrace 记录栈
traceon / traceoff 开关 tracing
enable_event / disable_event 控制其他 event
hist 直方图聚合

9.5 Histogram(trace_events_hist.c + tracing_map.c)

  • 按字段 hash 聚合(pid、comm、latency 桶)
  • onmatch / onmax 跨 event 延迟追踪
  • trace_events_synth.c 合成新 event 类型

9.6 常见 Event 子系统

子系统 典型事件
sched sched_switch, sched_wakeup, sched_waking
irq irq_handler_entry/exit
power cpu_idle, cpu_frequency
block block_rq_issue, block_rq_complete
kmem kmalloc, kfree
syscalls sys_enter/exit_*(trace_syscalls.c

十、动态探针、fprobe 与 BPF

10.1 动态 Event 框架(trace_dynevent.c)

1
2
3
/sys/kernel/debug/tracing/dynamic_events
/sys/kernel/debug/tracing/kprobe_events
/sys/kernel/debug/tracing/uprobe_events
类型 文件 说明
kprobe / kretprobe trace_kprobe.c 内核函数/指令探针
uprobe / uretprobe trace_uprobe.c 用户态探针
eprobe trace_eprobe.c eBPF-based probe
synthetic trace_events_synth.c 运行时合成 event

公共解析:trace_probe.c(参数 fetch、format 生成)。

kprobe 示例

1
2
echo 'p:myprobe do_sys_openat2 dfd=%di filename=%si flags=%dx' >> kprobe_events
echo 1 > events/kprobes/myprobe/enable

10.2 rethook + fprobe

rethook.cCONFIG_RETHOOK):通用函数返回 hook 基础设施,维护 per-task 返回 hook 链表,供 kretprobe/fprobe 复用。

fprobe.cCONFIG_FPROBE):基于 ftrace_ops + rethook 的轻量探针:

  • 支持 entry/exit handler
  • 可一次 probe 多个函数
  • 比 kprobe 开销更低,但仅限函数 entry/exit
1
2
/* fprobe 回调链 */
fprobe_handler() → entry_handler() → rethook_hook() → [函数执行] → exit_handler()

10.3 BPF 集成(bpf_trace.c)

BPF 程序可挂载:

类型 BPF_PROG_TYPE
tracepoint TRACING
kprobe/uprobe KPROBE
fentry/fexit 基于 ftrace
raw tracepoint RAW_TRACEPOINT

Helper:bpf_get_stackidbpf_probe_readbpf_printk 等。与 perf 事件桥接:trace_event_perf.c


十一、Latency 延迟追踪器

11.1 irqsoff / preemptoff(trace_irqsoff.c)

1
2
3
local_irq_disable() → trace_hardirqs_off()   /* 记录起始 */
... 临界区 ...
local_irq_enable() → trace_hardirqs_on() /* 超阈值则 swap max_buffer */

11.2 wakeup latency(trace_sched_wakeup.c)

1
2
try_to_wake_up() → trace_sched_waking()
schedule() → trace_sched_wakeup() /* 计算 wakeup→running 延迟 */

模式:wakeup(CFS)、wakeup_rtwakeup_dl

11.3 hwlat(trace_hwlat.c)

内核线程关中断 spin,检测 SMI/NMI 等 硬件引入的延迟空隙

11.4 osnoise / timerlat(trace_osnoise.c)

  • osnoise:统计 NMI/IRQ/SoftIRQ/线程/hw 各来源噪声
  • timerlat:测量 hrtimer 唤醒延迟,用于 PREEMPT_RT 调优

十二、追踪时钟(trace_clock.c)

时钟 API 特点
local trace_clock_local() 仅本 CPU,sched_clock(),最快
medium trace_clock() local_clock(),跨 CPU 有小 jitter
global trace_clock_global() 全局单调,有序列化开销
mono trace_clock_mono() 单调 boot time
jiffies trace_clock_jiffies() 以 jiffy 为单位

用户设置:echo global > trace_clock

RK3588 跨 CPU 分析建议用 globalmono,避免 big.LITTLE 间 local 时钟不一致。


十三、Runtime Verification(rv/)

rv/ 子目录(~1731 行)实现在线 形式化规范验证

1
2
3
Tracing 插桩 → Monitor 实例 ← Reference Model(规范)

Reaction(printk / panic / 自定义)
1
2
3
4
5
6
struct rv_monitor {
const char *name;
int (*enable)(void);
int (*disable)(void);
};
int rv_register_monitor(struct rv_monitor *monitor);

用户接口:/sys/kernel/debug/tracing/rv/available_monitors

内置 monitor:

Monitor 文件 说明
wip rv/monitors/wip/wip.c Work In Progress 状态
wwnr rv/monitors/wwnr/wwnr.c Wait/Wakeup Not Running

Reaction:reactor_panic.creactor_printk.crv_reactors.c)。


十四、tracefs 用户接口

挂载点:/sys/kernel/debug/tracing/(需 debugfs 挂载)。

14.1 常用控制文件

文件 功能
current_tracer 设置当前 tracer
available_tracers 列出可用 tracer
tracing_on 全局开关 0/1
trace 读追踪数据(读后清空)
trace_pipe 阻塞持续读
buffer_size_kb ring buffer 大小
trace_clock 时钟源
tracing_cpumask 限定 CPU
set_ftrace_filter / set_ftrace_notrace 函数 filter
snapshot 手动快照
tracing_max_latency 最大延迟阈值
events/ event 树
instances/ 独立实例

14.2 快速示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 函数图追踪(含耗时)
echo function_graph > current_tracer
echo funcgraph-abstime > trace_options
echo 1 > tracing_on
cat trace

# sched 事件 + nop tracer
echo 1 > events/sched/sched_switch/enable
echo nop > current_tracer
cat trace_pipe

# 中断关闭最大延迟
echo irqsoff > current_tracer
echo 0 > tracing_max_latency
cat trace

# kprobe
echo 'p:open do_sys_openat2 filename=%si' >> kprobe_events
echo 1 > events/kprobes/open/enable

14.3 启动时 tracing(trace_boot.c)

CONFIG_BOOTTIME_TRACING:通过内核命令行 ftrace= / trace_event= 在 boot 早期启用 tracing,便于启动路径分析。


十五、RK3588/ARM64 平台说明

15.1 架构支持总结

能力 ARM64/RK3588
FUNCTION_TRACER ✓(-mrecord-mcount / patchable entries)
DYNAMIC_FTRACE
DYNAMIC_FTRACE_WITH_REGS ✓(回调默认可获 pt_regs)
FUNCTION_GRAPH_TRACER ✓(default y)
KPROBE / UPROBE
HAVE_GENERIC_VDSO ✓(与 trace 独立,用户态 gettimeofday)

15.2 defconfig 与开销

Rockchip defconfig 显式启用 CONFIG_FUNCTION_TRACER=yDYNAMIC_FTRACE 默认 y,未启用 tracing 时接近零开销(NOP patch)。

15.3 常用调试场景

场景 推荐配置
驱动 probe/初始化 function_graph + set_ftrace_filter 限定驱动符号
调度延迟 wakeup tracer 或 events/sched/sched_wakeup/enable
CPU 频率/idle events/power/cpu_frequency/enable
中断延迟 irqsoff tracer
I/O 性能 events/block/enableblk tracer
多媒体 pipeline function_graph + tracepoint dma_fence
实时性评估 osnoise / timerlat

15.4 big.LITTLE 注意点

  • tracing_cpumask 限定 A76 或 A55 簇,减少交叉 CPU 噪声
  • trace_clockglobal/mono 保证跨核时间戳可比
  • 8 核建议 buffer_size_kb ≥ 4096,避免 function tracer 溢出
  • deep idle 时部分 little 核 clockevent 停,tracing 本身不依赖 periodic tick,但 hwlat/osnoise 测试需考虑 CPU 隔离

15.5 与 perf 的关系

  • perf 可复用 tracepoint(trace_event_perf.c
  • perf record -e sched:* 与 ftrace events 共享底层 tracepoint
  • BPF 程序可通过 bpf_trace.c 同时服务 tracing 与 perf

十六、完整追踪时序

内核函数ring_bufferftrace (ftrace.c)tracefs (trace.c)用户空间内核函数ring_bufferftrace (ftrace.c)tracefs (trace.c)用户空间per-CPU 无锁写入echo function > current_tracerecho 1 > tracing_onftrace_startup(ops)NOP → call ftrace_caller (dynamic patch)函数入口 mcount/fentrylock_reserve → ftrace_entry → commitcat trace_pipetrace_iterator 遍历print_line 格式化输出

十七、总结

要点 说明
Ring Buffer per-CPU 无锁存储,tracing 数据中枢
Dynamic ftrace 关闭时 NOP,启用时 patch,ARM64 patchable entries
Tracer 插件 struct tracer 扩展,覆盖函数/延迟/I/O
Trace Event tracepoint/kprobe/synthetic 统一框架 + filter/trigger/hist
fprobe/rethook 轻量 entry/exit 探针基础设施
tracefs 标准用户态控制面
RK3588 FUNCTION_TRACER + dynamic ftrace + graph;8 核 big.LITTLE 需注意 clock/buffer

kernel/trace 是内核开发、驱动调试、调度分析与实时性评估的核心工具链;与 perfBPFdebugfs 深度集成。


附录:源文件清单

文件 行数 分类
trace.c 10493 核心框架
ftrace.c 8479 ftrace 引擎
trace_events_hist.c 6757 直方图 trigger
ring_buffer.c 6234 环形缓冲区
trace_events.c 4112 事件框架
bpf_trace.c 2838 BPF 集成
trace_events_filter.c 2475 Event 过滤
trace_osnoise.c 2444 OS 噪声 / timerlat
trace_events_synth.c 2351 合成事件
trace_kprobe.c 2133 kprobe event
trace_events_trigger.c 1984 Event trigger
trace_events_user.c 1922 用户 event
blktrace.c 1922 块 I/O tracer
trace_uprobe.c 1675 uprobe event
trace_output.c 1583 输出格式化
trace_functions_graph.c 1367 函数图 tracer
trace_selftest.c 1287 自测
trace_probe.c 1234 探针公共代码
tracing_map.c 1139 无锁 hash map
trace_eprobe.c 1095 eprobe
trace_functions.c 974 function tracer
trace_hwlat.c 893 hwlat tracer
trace_sched_wakeup.c 820 wakeup tracer
trace_syscalls.c 808 syscall 事件
rv/rv.c 801 RV 框架
trace_irqsoff.c 752 irqsoff tracer
trace_boot.c 671 启动 tracing
fgraph.c 664 function graph 辅助
trace_stack.c 582 栈深度 tracer
trace_event_perf.c 529 perf 桥接
trace_dynevent.c 483 动态 event 控制
trace_branch.c 455 branch tracer
trace_seq.c 405 序列化输出
trace_printk.c 400 trace_printk
trace_stat.c 364 统计输出
trace_mmiotrace.c 362 MMIO tracer
rethook.c 346 返回 hook
fprobe.c 334 fprobe 探针
trace_events_inject.c 335 event 注入
pid_list.c 495 PID 过滤
trace_clock.c 158 追踪时钟
trace_nop.c 100 nop tracer
rv/rv_reactors.c 510 RV reaction
其他测试/辅助 ~3000 selftest、benchmark 等
合计(含 rv/) 83027 81 个 .c/.h 文件

行数统计:Linux 6.1,find kernel/trace -name '*.c' -o -name '*.h' | xargs wc -l

rk3588/kernel-6.1 kernel 内核模块架构详解

rk3588/kernel-6.1 kernel 内核模块架构详解

本文聚焦 rk3588/kernel-6.1/kernel 目录。该目录不是单一功能模块,而是 Linux 内核“通用核心层”聚合区:进程/调度/同步/中断/时间/模块装载/系统控制等都在这里落地实现。


1. kernel/ 在内核全局中的位置

可以把内核粗略分成:

  • init/:早期启动与 initcall 驱动初始化
  • mm/:内存管理
  • fs/:VFS 与文件系统
  • net/:网络协议栈
  • drivers/:设备驱动
  • kernel/跨子系统通用核心机制

kernel/Makefile 显示其既包含单文件核心(如 fork.oexit.osysctl.oworkqueue.o),也包含关键子目录(sched/locking/irq/rcu/time/module/printk/cgroup/ 等)。

从职责上看,kernel/ 更像“操作系统运行时公共层”。drivers/ 负责具体硬件,fs/ 负责文件系统抽象,net/ 负责协议栈,mm/ 负责内存;而 kernel/ 负责把这些模块串成一个可运行的系统:

  • 进程由 fork.c 创建后交给 sched/ 调度;
  • 驱动通过 irq/ 接收硬件中断,再通过 softirq.cworkqueue.c 延后处理;
  • 文件系统、网络、驱动都依赖 locking/rcu/ 做并发保护;
  • 模块驱动由 module/ 加载,参数由 params.c 解析,运行时策略由 sysctl.c 调整;
  • 故障时由 printk/panic.cwatchdog.ctrace/ 提供诊断能力。

所以阅读 kernel/ 不能只按目录理解,还要按“生命周期”理解:任务创建、调度运行、中断唤醒、同步保护、定时超时、退出回收、日志诊断,这些路径会跨越多个文件。


2. 构建与配置组织方式(Kbuild/Kconfig)

2.1 Kbuild 组织(kernel/Makefile

obj-yobj-$(CONFIG_*) 决定了模块装配:

  • 始终编译的核心fork.oexit.osignal.osys.opid.okthread.oworkqueue.ocred.oreboot.o
  • 按配置启用
    CONFIG_MODULES -> module/ + kmod.o
    CONFIG_AUDIT -> audit*.o
    CONFIG_KPROBES -> kprobes.o
    CONFIG_SECCOMP -> seccomp.o
    CONFIG_BPF -> bpf/
    CONFIG_CGROUPS -> cgroup/
  • 核心子目录装配sched/locking/power/printk/irq/rcu/time/

Kbuild 的作用不只是“把文件编译进去”。它还决定功能边界和依赖关系:

  • obj-y 表示内核核心必须存在的能力,例如进程、调度、信号、内核线程;
  • obj-$(CONFIG_xxx) 表示功能受配置控制,例如 BPF、audit、seccomp、module、cgroup;
  • 子目录形式通常代表较复杂的框架,例如 sched/irq/time/rcu/,这些目录内部还会根据配置选择不同实现;
  • 同一个 API 在不同配置下可能编译出不同路径,例如抢占模型、RT mutex、lockdep、NO_HZ、RCU 类型。

这解决了 Linux 需要同时适配服务器、桌面、手机、嵌入式 SoC 的问题:同一套源码通过 Kconfig/Kbuild 组合出不同内核能力。

2.2 Kconfig 入口特点

kernel/ 下常见为分片配置文件(而非单独 kernel/Kconfig):

  • Kconfig.preempt:内核抢占模型(NONE/VOLUNTARY/PREEMPT/RT)
  • Kconfig.hz:时钟中断频率(100/250/300/1000Hz)
  • Kconfig.locks:自旋锁/读写锁/排队锁等锁实现策略
  • Kconfig.freezer:freezer 开关(与休眠/cgroup freezer 相关)

这些配置会直接改变运行时行为:

  • CONFIG_PREEMPT_NONE 更偏吞吐,内核态长路径不主动抢占;
  • CONFIG_PREEMPT_VOLUNTARY 在显式点让出 CPU;
  • CONFIG_PREEMPT 降低交互延迟;
  • CONFIG_PREEMPT_RT 会改变大量锁和中断线程化语义;
  • CONFIG_HZ 影响 tick 周期,进一步影响调度粒度、timer 精度和功耗;
  • CONFIG_LOCKDEP 增加锁依赖检查,适合调试但有额外开销;
  • CONFIG_NO_HZ 减少空闲 CPU 周期 tick,有助于功耗和虚拟化场景。

因此分析 kernel/ 代码时必须结合 .config,否则同一处源码在实际运行中可能走完全不同的条件编译路径。


3. kernel/ 目录具备的功能全景

kernel/ 目录可以理解为 Linux 的“通用运行时核心”。它不直接对应某个外设,而是给全系统提供进程、调度、中断、同步、时间、模块、安全、调试、资源控制等基础能力。

功能类别 代表文件/目录 主要能力
进程生命周期 fork.cexit.cpid.c 创建进程/线程、分配 PID、退出回收、父子关系维护
调度 sched/ CFS/RT/DL/idle 调度类、上下文切换、负载均衡、CPU 亲和性
信号与 ptrace signal.cptrace.c 信号发送/递送/处理、调试器跟踪进程
凭证与权限 cred.ccapability.cgroups.c UID/GID、capability、进程凭证复制与提交
系统调用公共实现 sys.csys_ni.cexec_domain.c 通用 syscall 支撑、未实现 syscall 占位、执行域兼容
内核线程与异步执行 kthread.cworkqueue.casync.ctask_work.c 内核线程、工作队列、异步任务、返回用户态前回调
中断与下半部 irq/softirq.cirq_work.c IRQ 注册/分发、irqdomain、MSI、softirq、tasklet、irq_work
同步与并发 locking/rcu/ mutex/rwsem/spinlock/rtmutex/lockdep、RCU/SRCU 宽限期
时间与定时器 time/ timekeeping、clocksource、tick、hrtimer、POSIX timer、NTP
模块加载 module/kmod.cparams.c .ko 加载/卸载、ELF 重定位、符号解析、模块参数
日志与 panic printk/panic.creboot.c 内核日志、console 输出、panic、重启/关机
系统配置接口 sysctl.cutsname_sysctl.cksysfs.c /proc/sys、sysfs 内核信息、运行时参数调优
namespace nsproxy.cpid_namespace.cuser_namespace.cutsname.c PID/user/UTS namespace 及进程 namespace 关联
cgroup/资源控制 cgroup/ucount.cuser.c cgroup 层级、任务归属、用户资源计数与限制
安全与审计 seccomp.caudit*.c syscall 过滤、审计规则、审计事件生成
futex futex/ 用户态锁的内核等待/唤醒基础
tracing/观测 trace/tracepoint.cevents/ ftrace、tracepoint、kprobe/uprobe、perf events、ring buffer
BPF bpf/ eBPF 程序、map、helper、验证器与运行时支撑
DMA 映射 dma/ DMA mapping/coherent/CMA/SWIOTLB/IOMMU 抽象
电源管理 power/freezer.ccpu_pm.c suspend/hibernate、wakelock、freezer、CPU PM 通知
CPU/SMP/hotplug cpu.csmp.csmpboot.cstop_machine.c CPU 上下线、SMP call、stop_machine、启动辅助线程
崩溃与热重启 crash_core.ckexec*.ccrash_dump.c kexec、kdump、崩溃内核准备
动态补丁/优化 livepatch/jump_label.cstatic_call*.c livepatch、静态分支、static call 热路径优化
调试检测 watchdog.chung_task.cdebug/kcov.okcsan/gcov/ lockup/hung task 检测、KGDB/KDB、覆盖率、并发竞争检测

4. 核心子模块逐项详解

4.1 进程创建与退出:fork.c / exit.c / pid.c

要解决的问题

Linux 必须把“一个程序正在运行”抽象成可调度、可管理、可回收的任务。进程/线程生命周期模块解决四类问题:

  • 如何从当前任务复制出新任务;
  • 如何给任务分配 PID、父子关系、线程组关系;
  • 如何让新任务接入调度器、cgroup、namespace、审计、perf 等系统;
  • 如何在任务退出时按顺序释放所有资源,并通知父进程 wait()

实现的功能

fork.c 实现 fork()vfork()clone()clone3()kernel_thread() 等入口,核心是 kernel_clone()copy_process()
exit.c 实现 exit()exit_group()wait4()waitid() 等,核心是 do_exit()do_wait()
pid.c 负责 PID 分配、查找、引用计数和 PID namespace 下的 PID 映射。

创建路径如何实现

进程创建主链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
用户态 clone/fork/vfork
-> SYSCALL_DEFINE*()
-> kernel_clone()
-> copy_process()
-> dup_task_struct()
-> copy_creds()
-> copy_namespaces()
-> copy_files()
-> copy_fs()
-> copy_sighand()
-> copy_signal()
-> copy_mm()
-> copy_thread()
-> cgroup_can_fork() / cgroup_post_fork()
-> sched_fork()
-> wake_up_new_task()
-> 新任务进入调度队列

copy_process() 不是简单复制 task_struct。它按资源语义决定“共享还是复制”:

  • CLONE_VM 决定是否共享地址空间;
  • CLONE_FILES 决定是否共享文件描述符表;
  • CLONE_FS 决定是否共享根目录、当前目录、umask;
  • CLONE_SIGHAND / CLONE_THREAD 决定信号处理和线程组关系;
  • namespace、cred、cgroup、perf、seccomp、futex、io_uring 都会参与校验或初始化。

它解决的问题是:让 fork() 看起来像“复制进程”,但在实现上尽量共享资源、延迟复制,并维持内核对象引用计数正确。

退出路径如何实现

退出主链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
用户态 exit/exit_group 或 fatal signal
-> do_exit(code)
-> exit_signals()
-> io_uring_files_cancel()
-> acct_update_integrals()
-> perf_event_exit_task()
-> exit_mm()
-> exit_sem()
-> exit_files()
-> exit_fs()
-> exit_task_namespaces()
-> cgroup_exit()
-> exit_notify()
-> do_task_dead()
-> schedule()

do_exit() 的核心是“有序拆除”。顺序非常重要:例如地址空间、文件表、信号、cgroup、perf、RCU 回收和父进程通知之间存在依赖。如果顺序错误,可能出现 use-after-free、父进程无法 wait、僵尸进程无法回收等问题。

用途与典型场景

  • 用户程序创建进程、线程;
  • 内核创建 kthread;
  • 容器运行时创建带 namespace/cgroup 的任务;
  • shell 等待子进程退出;
  • ps/top 观察进程状态;
  • OOM、fatal signal、panic 前任务清理。

4.2 调度核心:sched/

要解决的问题

系统中 runnable 任务通常远多于 CPU 数量。调度器要决定:

  • 哪个任务现在运行;
  • 任务何时被抢占;
  • 唤醒任务应该放到哪个 CPU;
  • RT/DL/CFS/idle 等不同策略如何共存;
  • 多核和 RK3588 大小核平台上如何平衡性能、功耗和实时性。

实现的功能

kernel/sched/ 包含:

  • core.c:调度主框架、上下文切换、唤醒、调度系统调用;
  • fair.c:CFS,普通进程调度;
  • rt.c:实时调度 SCHED_FIFO / SCHED_RR
  • deadline.cSCHED_DEADLINE
  • idle.c:idle 任务;
  • stop_task.c:stop machine 等最高优先级任务;
  • topology.c:CPU 拓扑、调度域;
  • cpufreq_schedutil.c:schedutil 频率调节;
  • pelt.c:PELT 负载跟踪;
  • psi.c:Pressure Stall Information。

核心实现机制

每个 CPU 有一个 runqueue:

1
DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);

任务切换主链路:

1
2
3
4
5
6
7
8
9
schedule()
-> __schedule()
-> rq_lock()
-> pick_next_task()
-> stop -> deadline -> rt -> fair -> idle
-> context_switch()
-> switch_mm_irqs_off()
-> switch_to()
-> rq_unlock()

唤醒主链路:

1
2
3
4
5
try_to_wake_up()
-> select_task_rq()
-> ttwu_queue()
-> activate_task()
-> check_preempt_curr()

CFS 的核心思想是用 vruntime 近似公平运行时间。RT 调度按优先级抢占。Deadline 使用运行时间、周期、截止期描述任务需求。

解决的问题

  • 防止一个任务长期独占 CPU;
  • 让交互任务获得较低延迟;
  • 让实时任务按优先级抢占普通任务;
  • 在多核间迁移任务以避免某些 CPU 过载;
  • 为 cpufreq 提供负载信号;
  • 支撑 cgroup CPU 控制和 PSI 压力统计。

RK3588 关注点

RK3588 是大小核 SoC,调度器还要结合 CPU capacity、能效模型、cpufreq、thermal pressure。普通 CFS 任务不仅要“公平”,还要尽量放到合适性能/能耗的 CPU 上。性能抖动、卡顿、功耗异常通常都需要同时看 sched/time/cpufreq 和设备中断负载。


4.3 并发同步:locking/

要解决的问题

内核同时运行在多 CPU、硬中断、软中断、内核线程和用户进程上下文中。共享数据如果没有同步,会产生竞态;同步过度又会死锁或拖慢系统。

locking/ 解决:

  • 原子上下文不能睡眠时如何保护数据;
  • 进程上下文可睡眠时如何等待资源;
  • 多读少写场景如何降低开销;
  • 如何发现死锁和错误加锁顺序;
  • PREEMPT_RT 下如何让锁更适合实时性。

实现的功能

  • mutex.c:可睡眠互斥锁;
  • rwsem.c:读写信号量;
  • semaphore.c:传统信号量;
  • spinlock.cqspinlock.c:自旋锁和排队自旋锁;
  • qrwlock.c:排队读写锁;
  • rtmutex_api.c:带优先级继承的实时互斥锁;
  • lockdep.c:锁依赖图和死锁检测;
  • osq_lock.c:乐观自旋队列。

实现机制

mutex 的基本流程:

1
2
3
4
5
mutex_lock()
-> 快速路径尝试占有 owner
-> 失败则加入 wait_list
-> schedule() 睡眠
-> unlock 时唤醒等待者

spinlock 的基本流程:

1
2
3
4
spin_lock()
-> 禁止抢占,必要时关中断
-> 原子操作竞争 lock word
-> 持锁者释放后继续

lockdep 的思路是记录“锁类”和“获取顺序”,构造依赖图。如果发现可能形成环,就报告潜在死锁。

用途

  • rq->lock 保护调度队列;
  • tasklist_lock 保护进程树;
  • files_struct、inode、dentry、网络队列等大量对象依赖锁;
  • RT 内核中许多 spinlock 会转换成可睡眠 rtmutex 语义。

解决的问题

它让内核能在并发环境下保持数据一致,同时用 lockdep 在开发阶段尽早发现 ABBA 死锁、睡眠上下文错误、IRQ 上下文锁错误。


4.4 延迟回收与读多写少并发:rcu/

要解决的问题

很多内核数据结构读多写少,例如任务列表、路由表、文件描述符表、dentry cache。读路径如果每次都加锁,会严重影响性能。

RCU 解决的问题是:读者几乎无锁,写者更新后等待所有旧读者离开,再释放旧对象

实现的功能

  • update.c:RCU 通用 API;
  • tree.c:Tree RCU 主实现;
  • srcutree.c / srcutiny.c:可睡眠 RCU;
  • sync.c:同步等待;
  • rcu_segcblist.c:回调分段管理;
  • rcutorture.crcuscale.c:压力测试。

实现机制

典型更新:

1
2
3
4
5
6
7
8
9
10
读者:
rcu_read_lock()
p = rcu_dereference(ptr)
使用 p
rcu_read_unlock()

写者:
old = ptr
rcu_assign_pointer(ptr, new)
call_rcu(old, free_callback)

RCU 需要判断一个 grace period:所有在旧指针发布前已经进入 RCU 读侧临界区的 CPU/任务都退出后,旧对象才可释放。

用途

  • 网络路由查找;
  • 进程 PID 查找;
  • 文件系统路径缓存;
  • cgroup、namespace、模块列表;
  • BPF map 和 tracing 数据结构。

解决的问题

RCU 把读路径从“竞争锁”变成“轻量标记 + 内存屏障”,极大降低多核读密集路径开销。代价是写路径更复杂,且必须严格遵守 RCU 指针访问规则。


4.5 中断核心与下半部:irq/ / softirq.c / irq_work.c

要解决的问题

硬件中断来自不同控制器、不同硬件编号、不同触发方式。驱动希望只看到统一的 Linux IRQ 编号和统一注册接口。

中断核心解决:

  • 硬件 IRQ 到 Linux IRQ 的映射;
  • 中断处理函数注册与释放;
  • 中断屏蔽、确认、EOI、重触发;
  • threaded IRQ;
  • MSI/MSI-X;
  • 中断亲和性;
  • 高耗时工作下放到下半部。

实现的功能

kernel/irq/

  • irqdesc.cirq_desc 分配和管理;
  • manage.crequest_threaded_irq()free_irq()__setup_irq()
  • handle.chandle_irq_event() 等事件分发;
  • chip.cirq_chip 通用操作封装;
  • irqdomain.c:硬件中断号到 Linux IRQ 映射;
  • msi.c:MSI/MSI-X domain;
  • affinity.c:中断亲和性。

softirq.c

  • softirq 向量;
  • tasklet;
  • ksoftirqd
  • __do_softirq()

注册和分发链路

驱动注册:

1
2
3
4
5
6
request_threaded_irq()
-> __setup_irq()
-> 分配 irqaction
-> 挂到 irq_desc
-> 如有 thread_fn 则创建 IRQ thread
-> 启用中断线

硬件触发后:

1
2
3
4
5
6
arch/GIC 入口
-> generic_handle_domain_irq()
-> irq_desc handler
-> handle_irq_event()
-> 驱动 primary handler
-> 如返回 IRQ_WAKE_THREAD,唤醒 threaded handler

softirq 路径:

1
2
3
4
5
raise_softirq()
-> 标记当前 CPU pending
-> irq_exit 或 ksoftirqd
-> __do_softirq()
-> 调用 softirq_vec[nr].action

用途与解决的问题

  • 设备驱动中断处理;
  • 网络收包 NET_RX_SOFTIRQ
  • 定时器 TIMER_SOFTIRQ
  • RCU 回调;
  • block 层完成通知;
  • threaded IRQ 避免硬中断中做耗时操作。

中断框架把硬件差异隐藏在 irq_chip / irq_domain 后面,让驱动用统一 API 工作。


4.6 时间与定时器:time/

要解决的问题

内核必须同时处理:

  • 当前时间是多少;
  • 定时器什么时候到期;
  • 调度 tick 如何产生;
  • 高精度定时如何保证;
  • NTP 如何校准时间;
  • CPU idle/nohz 时如何减少 tick。

实现的功能

kernel/time/ 中:

  • timekeeping.c:维护墙钟、monotonic、boottime;
  • clocksource.c:选择和管理时钟源;
  • clockevents.c:管理定时事件设备;
  • jiffies.c:传统 tick 计数;
  • timer.c:低精度 timer wheel;
  • hrtimer.c:高精度定时器;
  • tick-sched.c:NO_HZ、调度 tick;
  • posix-timers.c:POSIX timer;
  • ntp.c:NTP 校准;
  • namespace.c:time namespace。

核心实现机制

时间更新:

1
2
3
4
clocksource 提供 cycle
-> timekeeping 计算 ns
-> update_wall_time()
-> 更新 xtime / monotonic / raw time

高精度定时:

1
2
3
4
5
hrtimer_start()
-> 插入 per-CPU rb-tree
-> 设置 clockevent 下一次到期
-> 中断到期
-> 执行回调或唤醒任务

调度 tick:

1
2
3
4
tick_sched_timer()
-> 更新 jiffies/timekeeping
-> scheduler_tick()
-> 触发时间片、负载统计、RCU 检查

用途

  • schedule_timeout()
  • socket 超时;
  • block IO 超时;
  • hrtimer 驱动;
  • 用户态 clock_gettime()
  • POSIX timer;
  • 调度器 PELT 和 CPU 使用统计。

时间子系统解决的是“内核所有等待、超时、统计和调度节拍的共同时间基准”。


4.7 日志、panic 与重启:printk/ / panic.c / reboot.c

要解决的问题

内核出现异常时,用户态可能不可用,文件系统可能不可写,甚至中断和锁都可能异常。日志系统必须尽量可靠地记录信息,并输出到 console。

实现的功能

printk/

  • printk.cprintk()vprintk_emit()、console 输出;
  • printk_ringbuffer.c:无锁/低锁日志环形缓冲;
  • printk_safe.c:特殊上下文安全打印;
  • sysctl.c:printk loglevel 等运行时控制。

panic.c

  • panic()
  • oops/panic 控制;
  • panic notifier;
  • kmsg dump;
  • panic timeout 和重启。

reboot.c

  • kernel_restart()
  • kernel_power_off()
  • reboot notifier;
  • 系统关机/重启路径。

实现机制

1
2
3
4
5
printk()
-> vprintk_emit()
-> 写入 printk ringbuffer
-> 根据 loglevel 和 console 状态输出
-> 唤醒 printk kthread 或直接 console_unlock()

panic 路径:

1
2
3
4
5
6
panic()
-> 禁止/限制其他 CPU 干扰
-> 打印 panic 信息和栈
-> 调用 panic notifier
-> kmsg_dump()
-> 等待或重启

用途

  • 驱动调试;
  • boot log;
  • crash 分析;
  • watchdog/hung task 输出;
  • 串口 early console;
  • 生产环境故障定位。

4.8 工作队列、内核线程与异步执行:workqueue.c / kthread.c / async.c / task_work.c

要解决的问题

很多内核工作不能在当前上下文直接执行:

  • 中断上下文不能睡眠;
  • 持锁路径不能做耗时操作;
  • 启动阶段硬件探测可以并行;
  • 某些工作需要绑定 CPU 或允许 freezer。

工作队列如何实现

workqueue.c 提供通用异步执行机制。源码注释明确说明:work item 在进程上下文执行,worker pool 自动管理。每 CPU 通常有 normal/highpri 两类 pool,unbound workqueue 使用动态 worker pool。

调用链:

1
2
3
4
5
6
7
8
9
INIT_WORK()
-> queue_work()
-> __queue_work()
-> 选择 pool_workqueue
-> work 加入 pool 链表
-> wake_up_worker()
-> worker_thread()
-> process_one_work()
-> work->func()

它解决的问题是:驱动和子系统可以把“稍后执行、可睡眠、可能耗时”的任务交给统一 worker,而不是每个模块都创建自己的线程。

内核线程如何实现

kthread.c 提供:

  • kthread_create()
  • kthread_run()
  • kthread_should_stop()
  • kthread_stop()
  • park/unpark;
  • CPU 绑定。

典型模式:

1
2
3
4
5
6
7
8
9
10
kthread_run(threadfn, data, name)
-> 创建 task
-> wake_up_process()
threadfn:
while (!kthread_should_stop())
do_work()
kthread_stop()
-> 设置 stop 标志
-> 唤醒线程
-> 等待退出

async.c 如何提高启动速度

async.c 的目标是减少启动时间。它把一些独立硬件延迟和发现操作异步执行,同时用 cookie 保证外部可见顺序:

1
2
3
4
5
6
7
async_schedule()
-> 分配 async_entry 和 cookie
-> queue_work()
-> async_run_entry_fn()
-> func(data, cookie)
-> 从 pending list 删除
-> wake_up 等待者

如果某个阶段必须等待之前异步任务完成,则调用 async_synchronize_full()async_synchronize_cookie()

task_work 的用途

task_work.c 允许把回调挂到某个 task 上,在该 task 返回用户态或退出前执行。io_uring、文件关闭延后处理等会用到它。


4.9 内核模块装载框架:module/ + kmod.c + params.c

要解决的问题

驱动不可能全部静态编进内核。模块框架解决:

  • .ko 如何加载到内核地址空间;
  • 符号如何解析;
  • ELF relocation 如何处理;
  • 模块参数如何解析;
  • init/exit 如何调用;
  • 模块是否可卸载;
  • 依赖、引用计数、sysfs/proc 如何维护;
  • 签名、vermagic、license 如何检查。

加载路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
finit_module/init_module syscall
-> load_module()
-> copy module from user
-> module_sig_check()
-> ELF header/section 检查
-> layout_and_allocate()
-> simplify_symbols()
-> apply_relocations()
-> post_relocation()
-> complete_formation()
-> mod_sysfs_setup()
-> do_init_module()
-> 执行 module init
-> 成功后释放 init section

卸载路径:

1
2
3
4
5
6
7
delete_module syscall
-> 查找 module
-> 检查引用计数
-> 调用 module exit
-> 从模块列表移除
-> RCU 同步
-> 释放 module memory

kmod.c 的用途

kmod.c 处理内核请求用户态加载模块的场景,例如设备热插拔后触发:

1
2
3
request_module("xxx")
-> call_usermodehelper()
-> /sbin/modprobe xxx

它解决的是“内核发现需要某个驱动时,自动让用户态 modprobe 加载”。

params.c 的用途

模块参数和内核启动参数都需要解析。params.c 提供通用参数解析、类型转换、权限和 sysfs 暴露支持。


4.10 系统控制与参数接口:sysctl.c / ksysfs.c / resource.c

要解决的问题

内核运行后仍需要调节参数,例如 panic 策略、调度调试、pid 上限、printk 级别、perf 权限等。不能每次都重新编译内核,所以需要统一运行时配置入口。

sysctl 如何实现

sysctl.c 提供 /proc/sys 树:

1
2
3
4
5
6
7
register_sysctl()
-> 注册 ctl_table
用户读写 /proc/sys/xxx
-> proc_sys_call_handler()
-> proc_dointvec/proc_dostring/proc_doulongvec_minmax...
-> 校验范围
-> 修改内核变量

常见 handler:

  • proc_dointvec():整数;
  • proc_dointvec_minmax():带范围整数;
  • proc_dostring():字符串;
  • proc_doulongvec_minmax():unsigned long;
  • proc_do_static_key():控制 static key。

其他接口

  • ksysfs.c:在 sysfs 暴露内核信息;
  • resource.c:管理 I/O memory、I/O port 等资源树;
  • ucount.c:用户级资源计数;
  • range.c:范围管理辅助。

用途

  • /proc/sys/kernel/*
  • /proc/iomem
  • 调试运行时行为;
  • 给发行版和产品配置内核策略。

4.11 cgroup 与资源控制:cgroup/

要解决的问题

多进程、多容器场景中,需要按组管理资源和统计压力。cgroup 解决:

  • 任务如何归属某个资源组;
  • controller 如何挂接;
  • fork/exit/migrate 时资源关系如何更新;
  • 统计如何聚合;
  • 用户态如何通过 cgroupfs 管理。

实现机制

核心概念:

  • cgroup:层级节点;
  • css_set:任务关联的一组 subsystem state;
  • cgroup_subsys_state:具体 controller 状态;
  • cgroup_file:cgroupfs 文件;
  • rstat:层级统计刷新。

任务 fork 时:

1
2
3
4
copy_process()
-> cgroup_can_fork()
-> cgroup_css_set_fork()
-> cgroup_post_fork()

任务迁移时:

1
2
3
4
5
写 cgroup.procs
-> 找到目标 cgroup
-> 校验权限/controller
-> 更新 task css_set
-> 触发 controller migrate 回调

用途

  • 容器资源隔离;
  • CPU/memory/io/pids 控制;
  • PSI 压力统计;
  • systemd slice/service 管理。

4.12 安全、凭证与审计:cred.c / capability.c / seccomp.c / audit*.c

凭证模型解决什么问题

内核不能只看数值 uid/gid,还要支持 capability、keyring、安全模块等。cred.c 把任务权限封装成不可随意原地修改的 struct cred

典型修改流程:

1
2
3
4
5
prepare_creds()
-> 复制当前 cred
-> 修改 uid/gid/capability
-> security hooks 检查
-> commit_creds()

这样解决了权限对象并发访问问题:读者可以稳定读取旧 cred,写者提交新 cred。

capability 的用途

capability.c 把 root 权限拆成多个能力位,例如:

  • CAP_SYS_ADMIN
  • CAP_NET_ADMIN
  • CAP_SYS_MODULE
  • CAP_SYS_PTRACE

它解决的问题是避免所有特权操作都依赖 uid 0。

seccomp 如何实现 syscall 过滤

seccomp.c 支持:

  • strict mode;
  • filter mode;
  • BPF filter;
  • user notification;
  • 违规时 kill、trap、errno、trace、log 等动作。

典型用途:

1
2
3
4
容器/沙箱
-> 加载 seccomp BPF
-> 每次 syscall 进入时检查
-> 不允许的 syscall 被拒绝或杀死进程

audit 的用途

audit*.c 记录安全相关事件,包括 syscall、文件访问、规则匹配、进程身份等。它主要服务合规、安全审计和入侵追踪。


4.13 系统调用公共层:sys.c / sys_ni.c / exec_domain.c

要解决的问题

系统调用入口通常在架构目录中完成寄存器保存和 syscall number 分发,但大量 syscall 的通用逻辑与架构无关,需要放在 kernel/。这部分解决“用户态请求如何转换成内核通用服务”的问题。

实现的功能

  • sys.c:实现大量通用 syscall,例如 setpriority()getpriority()sysinfo()reboot() 辅助、进程组/session 相关接口等;
  • sys_ni.c:为未实现或未启用的 syscall 提供 sys_ni_syscall() 占位,返回 -ENOSYS
  • exec_domain.c:保留执行域兼容相关框架;
  • umh.c:用户态 helper 执行基础,被 kmod.c 等使用。

实现机制

典型 syscall 路径:

1
2
3
4
5
6
7
用户态 svc/syscall 指令
-> arch syscall entry
-> syscall table 查表
-> SYSCALL_DEFINE*() 实现
-> copy_from_user/copy_to_user 访问用户态参数
-> 调用内核通用子系统
-> 返回 errno 或结果

sys_ni.c 的意义是让 syscall table 在配置裁剪后仍有明确占位,用户态调用未启用能力时得到稳定错误,而不是跳到未知地址。

用途

这部分是用户态进入内核的公共服务层。分析应用行为、strace 结果、seccomp 规则、权限检查时,经常需要从 SYSCALL_DEFINE* 找到 kernel/ 或其他子系统的实际实现。


4.14 namespace 与隔离:nsproxy.c / pid_namespace.c / user_namespace.c / utsname.c

要解决的问题

容器需要让不同进程看到不同的 PID 空间、主机名、用户权限映射、挂载视图、网络栈等。namespace 解决“同一个内核中提供多套隔离视图”的问题。

实现的功能

  • nsproxy.c:把一个任务关联到一组 namespace;
  • pid_namespace.c:PID namespace 生命周期、PID 映射、init 进程语义;
  • user_namespace.c:用户 namespace、uid/gid 映射、capability 隔离;
  • utsname.c / utsname_sysctl.c:hostname/domainname namespace;
  • 其他 namespace 分布在对应子系统,例如 mount namespace 在 fs/,network namespace 在 net/

实现机制

任务创建和 namespace 关系:

1
2
3
4
clone/unshare/setns
-> copy_namespaces()
-> 根据 CLONE_NEW* 决定复用还是创建 namespace
-> nsproxy 指向新的 namespace 集合

PID namespace 的关键点是同一个 struct pid 可以在不同层级 namespace 中有不同数字。容器内看到的 PID 1,宿主机上可能是另一个 PID。PID namespace 的 init 进程退出时,该 namespace 内任务需要被清理。

User namespace 的关键点是 capability 作用域被限制在 namespace 内。容器内 uid 0 不等价于宿主机全局 root,除非映射和权限允许。

用途

  • Docker/containerd/systemd-nspawn 等容器;
  • Android 应用/服务隔离中的部分机制;
  • 测试环境隔离;
  • 安全沙箱。

namespace 不是单独完成隔离的,它通常与 cgroup、seccomp、capability、LSM、mount/network 子系统共同构成容器安全边界。


5. 其他重要功能模块

5.1 信号、ptrace 与进程控制:signal.c / ptrace.c

要解决的问题

普通函数调用是同步的,但操作系统需要支持异步事件:终端按 Ctrl-C、父进程杀子进程、定时器超时、非法访问内存、调试器暂停任务等。信号机制解决“异步通知进程并在安全时机交给用户态处理”的问题。

signal.c 实现进程间异步通知机制:

  • kill/tkill/tgkill/rt_sigqueueinfo 等系统调用;
  • 普通信号与实时信号排队;
  • 信号屏蔽、挂起、递送与用户态 handler 返回;
  • fatal signal 触发进程退出。

实现机制

信号发送大致路径:

1
2
3
4
5
6
kill/tgkill/内核异常
-> 找到目标 task 或线程组
-> 检查权限
-> 构造 siginfo
-> 挂入 pending 队列
-> signal_wake_up()

信号递送不是立即强行打断任意内核代码,而是在任务准备返回用户态、或从可中断睡眠中醒来时处理:

1
2
3
4
5
6
返回用户态前
-> arch_do_signal_or_restart()
-> get_signal()
-> 选择未屏蔽信号
-> fatal: do_group_exit()
-> handler: 构造用户态 signal frame

这样解决了“异步事件”和“内核临界区安全”之间的矛盾。

ptrace.c 支撑调试器控制被调试进程:

  • PTRACE_ATTACH / PTRACE_SEIZE
  • 读写寄存器、内存与信号注入;
  • syscall enter/exit 跟踪;
  • seccompsignalwait 紧密耦合。

这部分决定了 gdb、strace、crash dump、容器运行时调试等能力。

ptrace 的关键在于把被调试任务置于可观察、可控制的 stop 状态,并允许调试器读取寄存器、修改内存、拦截 syscall。它和信号、wait、seccomp 都有关:调试器看到的很多事件最终都通过 signal/wait 语义通知。

5.2 futex 用户态锁支撑:futex/

要解决的问题

如果每次 pthread mutex 加锁都进入内核,用户态同步会非常慢。futex 的目标是:无竞争时完全用户态,只有竞争时进入内核睡眠/唤醒

用户态锁在无竞争时用原子指令完成;发生竞争时才进入内核:

1
2
3
4
5
用户态原子操作失败
-> futex_wait()
-> 任务睡眠到 hash bucket 队列
-> futex_wake()
-> 唤醒等待者

它支撑 pthread mutex、condition variable、Java/Go/Rust 运行时锁等大量用户态同步机制。
因此 futex 是 Linux 用户态并发性能的关键基础。

实现机制

futex 以用户态地址为 key,把等待者挂到内核 hash bucket:

1
2
3
4
5
6
7
8
9
10
11
futex_wait(uaddr, expected)
-> 从用户地址读取当前值
-> 如果值不等于 expected,直接返回
-> 根据 uaddr 构造 futex_key
-> 入队到 hash bucket
-> schedule() 睡眠

futex_wake(uaddr, nr)
-> 根据 uaddr 找到同一 hash bucket
-> 取出最多 nr 个等待者
-> wake_up_q()

这解决了经典 lost wakeup 问题:futex_wait() 入睡前必须再次确认用户态值仍符合预期,避免 unlock 已经发生但线程却睡下去。

高级能力

futex/ 还包含:

  • PI futex:优先级继承,解决实时任务优先级反转;
  • requeue:条件变量 broadcast 时把等待者从一个 futex 转移到另一个 futex;
  • robust list:进程死掉时释放其持有的用户态锁;
  • timeout:支持带超时等待。

5.3 perf events 与 tracing:events/ / trace/ / tracepoint.c

要解决的问题

内核需要可观测性:性能瓶颈在哪里、调度延迟来自哪里、中断是否频繁、函数调用路径如何、某个事件参数是什么。perf 和 tracing 解决“低开销、运行时可开启的内核观测”问题。

kernel/events/ 是 perf event 核心:

  • CPU PMU 事件;
  • software events;
  • tracepoint 事件;
  • perf ring buffer;
  • task/cgroup 维度统计;
  • 与 BPF、ftrace、scheduler、irq 等联动。

kernel/trace/ 是 ftrace/tracefs 主体:

  • function tracer;
  • function graph tracer;
  • irqsoff/preemptoff/sched wakeup tracer;
  • kprobe/uprobe/eprobe event;
  • synthetic event 和 histogram trigger;
  • ring buffer 和 trace_pipe;
  • runtime verification(trace/rv/)。

tracepoint.c 提供静态 tracepoint 注册与调用机制。
驱动、调度器、网络、块层等都通过 tracepoint 暴露可观测事件。

perf 实现机制

perf 把一个观测点抽象成 struct perf_event

1
2
3
4
5
6
7
perf_event_open()
-> perf_event_alloc()
-> 选择 PMU
-> event 初始化
-> 安装到 task/cpu perf_event_context
-> mmap ring buffer
-> enable 后开始计数或采样

采样数据通过 perf ring buffer 传给用户态。对于硬件 PMU,底层由架构 PMU 驱动配置计数器;对于 tracepoint/software event,则由内核事件点主动输出样本。

ftrace/tracefs 实现机制

trace/ 维护 tracing ring buffer 和 tracefs 控制接口。典型路径:

1
2
3
4
5
6
7
echo function > current_tracer
-> 启用 function tracer
函数入口
-> ftrace trampoline
-> 写入 ring buffer
cat trace_pipe
-> 用户态读取实时事件

tracepoint 则是静态埋点:

1
2
3
4
TRACE_EVENT(sched_switch, ...)
-> 编译生成 tracepoint
-> 运行时注册 probe
-> 事件发生时调用 probe

用途

  • perf top/perf record
  • trace-cmd / ftrace
  • 调度延迟定位;
  • 中断风暴定位;
  • BPF 程序挂载 tracepoint/kprobe;
  • Android/RK 平台性能和功耗调优。

5.4 BPF 运行时:bpf/

要解决的问题

BPF 解决的是“在不改内核、不加载传统模块的情况下,把受限制、可验证的小程序挂到内核事件点”。它兼顾可扩展性和安全性。

kernel/bpf/ 提供 eBPF 在内核中的基础设施:

  • BPF map 生命周期;
  • BPF program 加载、验证、运行;
  • helper 调用;
  • BTF、local storage、token 等扩展能力;
  • 与 tracepoint、kprobe、cgroup、network、perf event 联动。

BPF 不是单独的调试工具,而是内核可编程扩展框架。
它让用户态可以在受验证的前提下,把小段逻辑挂到内核事件点上。

实现机制

典型加载路径:

1
2
3
4
5
6
7
8
9
10
11
12
bpf(BPF_PROG_LOAD)
-> 复制用户态 insn
-> verifier 检查控制流、类型、安全边界
-> JIT 或解释执行准备
-> 返回 prog fd

bpf(BPF_MAP_CREATE)
-> 创建 map
-> 返回 map fd

attach
-> prog 挂到 tracepoint/kprobe/cgroup/socket/perf event

verifier 是核心:它证明程序不会越界访问、不会无限循环、不会随意调用内核函数、不会泄露内核指针。
BPF map 是用户态和 BPF 程序之间、不同 BPF 程序之间共享状态的主要方式。

用途

  • 网络过滤与转发;
  • tracing 和性能分析;
  • 安全策略;
  • cgroup hook;
  • 线上故障诊断。

5.5 DMA mapping 框架:dma/

要解决的问题

设备不能直接使用 CPU 虚拟地址。不同平台还可能有 IOMMU、cache 一致性限制、设备 DMA 地址宽度限制、CMA 连续内存限制。DMA mapping 框架把这些差异封装成统一 API。

kernel/dma/ 实现通用 DMA API 的架构无关部分:

  • streaming DMA mapping;
  • coherent DMA allocation;
  • CMA contiguous allocation;
  • SWIOTLB bounce buffer;
  • IOMMU DMA ops;
  • DMA debug;
  • DMA pool。

驱动调用的 dma_map_single()dma_alloc_coherent()dma_map_sg() 等接口,最终会落到这里与架构层/设备 IOMMU 配合。
对 RK3588 这类 ARM64 SoC,DMA mask、IOMMU、CMA、cache 一致性都与该框架相关。

实现机制

coherent DMA:

1
2
3
4
5
dma_alloc_coherent(dev, size, &dma_handle, gfp)
-> 根据 dev 的 dma ops 选择 direct/IOMMU/CMA 路径
-> 分配 CPU 可访问内存
-> 建立设备可访问 DMA 地址
-> 返回 cpu_addr + dma_addr_t

streaming DMA:

1
2
3
4
5
6
7
dma_map_single(dev, cpu_buf, len, dir)
-> cache sync
-> IOMMU 映射或 direct 物理地址转换
-> 返回 dma_addr_t
设备 DMA 完成
-> dma_unmap_single()
-> cache sync / 释放 IOVA

解决的问题

  • 统一 PCIe、USB、网卡、存储、显示等驱动 DMA 编程;
  • 避免驱动手写 virt_to_phys()
  • 支持 IOMMU 隔离;
  • 支持 32-bit 设备访问高内存时的 bounce;
  • 统一 debug DMA API 检查 map/unmap 泄漏。

5.6 电源管理与 freezer:power/ / freezer.c / cpu_pm.c

要解决的问题

移动和嵌入式系统需要低功耗。系统 suspend/hibernate 时,必须协调用户进程、内核线程、驱动、文件系统、CPU、设备电源状态,避免正在运行的任务破坏休眠镜像或硬件状态。

kernel/power/ 负责 suspend/hibernate 相关流程:

  • 系统 suspend;
  • hibernation 镜像保存/恢复;
  • wakelock;
  • 用户态 suspend 接口;
  • 测试与调试。

freezer.c 用于冻结可冻结任务,避免 suspend/hibernate 时用户态或内核线程继续修改状态。
cpu_pm.c 提供 CPU 低功耗状态切换的 notifier 框架,供架构和驱动在 CPU suspend/resume 前后处理硬件状态。

suspend 大致流程

1
2
3
4
5
6
7
用户写 /sys/power/state
-> pm_suspend()
-> freeze_processes()
-> device_suspend()
-> platform suspend / CPU suspend
-> resume 后 device_resume()
-> thaw_processes()

freezer 的作用是把可冻结任务停在安全点。内核线程如果支持冻结,需要周期性检查 freezer 请求。

用途

  • Android suspend/resume;
  • 车机/工业设备低功耗;
  • hibernation;
  • 驱动 runtime PM 和系统 PM 协同调试。

5.7 CPU、SMP 与 hotplug:cpu.c / smp.c / smpboot.c

要解决的问题

多核系统需要 CPU 上下线、跨 CPU 函数调用、per-CPU 线程、CPU 拓扑管理。RK3588 这类 big.LITTLE 平台还涉及大小核调度和功耗控制。

这部分管理 CPU 生命周期和跨 CPU 协作:

  • CPU online/offline;
  • CPU hotplug state machine;
  • SMP call function;
  • stop_machine;
  • per-CPU smpboot thread;
  • CPU 亲和性和调度拓扑联动。

在 RK3588 这类 big.LITTLE ARM64 平台上,CPU hotplug、调度拓扑、cpufreq、EAS、PM 都会互相影响。

实现机制

CPU hotplug 不是简单设置一个标志,而是状态机:

1
2
3
4
5
6
7
8
9
10
cpu_up()
-> cpuhp state machine
-> arch bringup CPU
-> 初始化 per-CPU 资源
-> 通知调度、RCU、timer、irq 子系统

cpu_down()
-> 迁移任务和中断
-> 停止 per-CPU timer/work
-> arch shutdown CPU

SMP call 用于让一个 CPU 请求另一个 CPU 执行函数:

1
2
3
4
smp_call_function_single()
-> 放入 call_single_data
-> 发送 IPI
-> 目标 CPU 中断中执行回调

stop_machine() 则用于极端场景:让大部分 CPU 停在受控状态,只执行指定回调,常用于 text patching、CPU hotplug、模块/内核修改敏感路径。

5.8 kexec、crash 与 reboot:kexec*.c / crash*.c / reboot.c

要解决的问题

系统重启、崩溃转储和无固件重启都需要内核级支持。普通 reboot 依赖平台固件或电源控制;kexec 则允许当前内核直接跳到另一个内核。

相关能力:

  • 正常重启、关机、poweroff;
  • kexec 直接跳转到新内核;
  • crash kernel / kdump 捕获崩溃现场;
  • panic 后保存 vmcore 所需信息。

这些功能主要用于生产环境故障恢复和内核崩溃分析。

实现机制

kexec:

1
2
3
4
5
6
7
8
kexec_load/kexec_file_load
-> 校验新内核镜像
-> 分配/准备段
-> 保存跳转信息
reboot 或 panic
-> machine_kexec()
-> 关闭中断/设备
-> 跳转到新内核入口

kdump:

1
2
3
4
5
预留 crashkernel 内存
-> panic
-> 跳转 crash kernel
-> crash kernel 读取旧内核内存
-> 生成 vmcore

这解决了生产环境“内核崩溃后如何保留现场”的问题。

5.9 动态补丁与热路径优化:livepatch/ / jump_label.c / static_call*.c

要解决的问题

内核既需要大量可选功能和动态开关,又不能让热路径充满昂贵分支和间接调用;同时生产系统有时需要不停机修复内核函数。

  • livepatch/:运行时替换内核函数实现,用于安全修复和在线补丁;
  • jump_label.c:静态分支,运行时把条件分支优化成近似 nop 或 jump;
  • static_call*.c:静态调用,把函数指针式调用优化成可动态修改的直接调用。

这些机制让内核在保持可配置、可观测的同时,尽量降低热路径分支和间接调用开销。

实现机制

static key / jump label:

1
2
3
4
5
代码中 static_branch_unlikely()
-> 默认编译为 nop 或 jump
运行时开关改变
-> text patching
-> 修改指令

static call:

1
2
3
4
定义 static_call()
-> 调用点可被 patch 成直接 call
运行时更新目标
-> 修改调用目标

livepatch:

1
2
3
4
加载 livepatch 模块
-> 注册替换函数
-> 检查任务一致性
-> ftrace/text patch 重定向旧函数到新函数

5.10 watchdog、hung task 与调试检测

要解决的问题

内核故障不一定立即 panic。常见问题是 CPU 长时间不响应、任务卡在 D 状态、锁竞争死锁、数据竞争、内存越界。调试检测模块用于尽早暴露问题。

代表文件:

  • watchdog.c / watchdog_hld.c:soft/hard lockup 检测;
  • hung_task.c:长时间 D 状态任务检测;
  • kcov.o:内核覆盖率采集;
  • kcsan/:并发数据竞争检测;
  • gcov/:代码覆盖率;
  • debug/:KGDB/KDB 等调试设施;
  • stacktrace.c / stackleak.c:栈回溯与栈清理。

这些不是业务功能,但对内核稳定性、测试覆盖和问题定位非常关键。

实现机制与用途

  • soft lockup:watchdog 线程或 timer 检测 CPU 是否长时间未调度;
  • hard lockup:通常依赖 NMI/perf event 检测 CPU 是否长时间不响应中断;
  • hung task:周期扫描 D 状态任务,超过阈值打印堆栈;
  • KCSAN:采样式数据竞争检测;
  • KCOV:为 fuzzing 提供覆盖率反馈;
  • stacktrace:为 lockdep、WARN、panic、perf 等提供调用栈。

这些能力解决“系统还活着但已经不正常”的诊断问题。


6. 典型跨模块主链路(理解“如何协同”)

6.1 进程创建链路

clone/fork
-> kernel_clone
-> copy_process
-> wake_up_new_task
-> sched 运行

关联:credcgroupnamespaceauditperfrcu

6.2 进程退出链路

exit/exit_group
-> do_exit
-> exit_mm/files/fs/task_work/...
-> cgroup_exit / perf_event_exit_task
-> exit_notify
-> do_task_dead -> schedule

6.3 中断到延后执行链路

硬中断
-> request_threaded_irq 注册的 handler
-> 快速路径仅做最小处理
-> queue_work / softirq / tasklet(视驱动)
-> worker 进程上下文执行重活

6.4 模块装载链路

finit_module
-> load_module(签名/ELF/重定位/参数/sysfs)
-> do_init_module
-> 模块功能上线


7. 调试与阅读建议(kernel/ 目录实战)

  1. 先看入口再看实现

    • syscall 入口:SYSCALL_DEFINE*
    • 调度入口:schedule()/__schedule()
    • 模块入口:init_module/finit_module/delete_module
  2. 配合配置文件理解行为差异

    • Kconfig.preempt 决定调度抢占语义
    • Kconfig.hz 影响 tick 频率与时延/功耗平衡
    • Kconfig.locks 影响锁实现路径
  3. 跨文件看生命周期

    • 进程:fork.c + exit.c + sched/core.c
    • 中断:irq/manage.c + irq/irqdomain.c
    • 定时:time/timekeeping.c + time/tick-sched.c
  4. 问题定位优先级

    • 卡顿/时延:先看 sched/ + time/ + locking/
    • 中断异常:先看 irq/ + 驱动 irq handler
    • 模块加载失败:看 module/main.c 错误分支与签名/版本检查

8. 总结

kernel/ 目录是 Linux 的“公共内核引擎层”,它不面向单一设备或协议,而是为全系统提供:

  • 任务生命周期(fork/exit/sched)
  • 并发原语(locking/rcu/workqueue)
  • 中断与时间基座(irq/time)
  • 运行期扩展与管控(module/sysctl/cgroup/seccomp/audit)
  • 可观测性、热更新与安全治理(trace/perf/BPF/livepatch/watchdog)

理解 kernel/,本质上是在理解“Linux 如何作为一个可并发、可扩展、可治理的操作系统内核运行”。

slam_toolbox 总览与架构

01 总览与架构

1.1 它解决什么问题

slam_toolbox 是 2D 激光 SLAM,给室内 AMR / AGV 用。输入是 sensor_msgs/LaserScan 和 TF 里的轮速里程计,输出是占用栅格 /mapmap → odom

相对“扫一圈存 pgm”多出来的能力:

  • 位姿图无损序列化,下次继续建图或做弹性定位
  • 同步 / 异步两种处理节奏
  • 定位模式:已有图 + 滚动窗口,不改底层地图
  • RViz 交互:手动挪节点、人工回环
  • 实验性 lifelong(按 IoU 删旧节点)、去中心化多机器人

匹配内核是工业界评价很高的 Karto 相关扫描匹配。作者 Steve Macenski,论文:Macenski & Jambrecic, JOSS 2021。

不是 3D LIO,也不是视觉 SLAM。RGB-D / 多传感器看 RTAB-Map。

1.2 三层结构

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────────────────┐
│ ROS 封装 include/ + src/ │
│ LifecycleNode、TF、服务、栅格化、RViz │
├─────────────────────────────────────────────┤
│ 模式层 async / sync / localization / … │
│ 只决定:何时、以哪种 Process* 送进 Mapper │
├─────────────────────────────────────────────┤
│ Karto lib/karto_sdk/ │
│ SequentialScanMatcher + MapperGraph │
│ 回环后调用 ScanSolver(Ceres 插件) │
└─────────────────────────────────────────────┘

代码量大致:ROS 封装约 4.7k 行,Karto Mapper.cpp 约 3.3k 行,Ceres 插件约 0.5k 行。真正的前端/回环在 Karto;ROS 层负责时间、坐标系和产品接口。

1.3 目录

1
2
3
4
5
6
7
8
9
10
slam_toolbox/
├── include/slam_toolbox/ 头文件,从 common 读起
├── src/ 公共层 + 各模式 + 工具
│ └── experimental/ lifelong、建图/定位切换
├── lib/karto_sdk/ Open Karto 改版
│ ├── include/karto_sdk/ Mapper.h / Karto.h / Math.h
│ └── src/ Mapper.cpp(核心)、Karto.cpp
├── solvers/ Ceres 插件(G2O/SPA/GTSAM 源码在但默认不编)
├── rviz_plugin/ 交互面板
├── launch/ config/ srv/ msg/

可执行文件(CMakeLists.txt):

可执行文件 用途
async_slam_toolbox_node AsynchronousSlamToolbox 在线建图(首选)
sync_slam_toolbox_node SynchronousSlamToolbox 离线/不丢帧
localization_slam_toolbox_node LocalizationSlamToolbox 已有图定位
lifelong_slam_toolbox_node LifelongSlamToolbox 实验性终身建图
map_and_localization_slam_toolbox_node MapAndLocalizationSlamToolbox 运行时切建图/定位
decentralized_multirobot_slam_toolbox_node 多机 交换 LocalizedScan
merge_maps_kinematic 工具 运动学拼图

*_node.cpp 只有 main:构造对应类,spinLifecycleNode 的 base interface。

1.4 类继承

1
2
3
4
5
6
7
8
9
10
11
rclcpp_lifecycle::LifecycleNode


SlamToolbox src/slam_toolbox_common.cpp
│ laserCallback() = 0
┌────┼──────────────┬────────────────────┐
▼ ▼ ▼ ▼
Async Sync Localization DecentralizedMR


MapAndLocalization experimental

SlamToolbox 持有:

  • SMapper:包一层 karto::Mapper
  • karto::Dataset:雷达模型 + 扫描对象(序列化用)
  • pluginlib::ClassLoader<karto::ScanSolver> → 默认 CeresSolver
  • 一组助手:GetPoseHelperLaserAssistantMapSaverLoopClosureAssistant

模式子类几乎只覆盖 laserCallback 和反序列化时的 ProcessType 限制。

1.5 线程模型

on_activate 里起两条 boost::thread(同步模式再加一条处理线程):

线程 函数 周期
TF publishTransformLoop transform_publish_period(默认 0.02–0.05 s)
可视化 publishVisualizations map_update_interval(建图配置里常 2–5 s)
同步处理 SynchronousSlamToolbox::run 100 Hz 出队

ROS 回调在 executor 线程。进 Karto 前要拿 smapper_mutex_map_to_odom_ 另有 map_to_odom_mutex_

on_deactivate 对线程 interrupt + join。循环里有 interruption_point()

1.6 和 Nav2 的边界

slam_toolbox 只负责:

  • map → odom(回环时这条边跳)
  • /map(占用栅格,QoS:KeepLast(1) + Transient Local + Reliable)
  • posePoseWithCovarianceStamped,map 系)

底盘必须自己发 odom → base_footprint。Nav2 消费地图和 TF,不进 Karto。

slam_toolbox 生命周期与公共层

02 生命周期与公共层

源码:include/slam_toolbox/slam_toolbox_common.hppsrc/slam_toolbox_common.cpp

2.1 为什么是 LifecycleNode

ros2 分支把节点改成 rclcpp_lifecycle::LifecycleNode,以便挂到 Nav2 的 lifecycle manager(bond)。online_async_launch.py 默认 autostart=true 时,launch 自己发 CONFIGUREACTIVATE

Humble 系统包 2.6.10 的 launch 仍是普通 Node,和这份源码不一致。

2.2 状态机

1
2
3
4
5
6
7
8
9
10
11
12
13
unconfigured
│ on_configure

inactive
│ on_activate

active ← 订阅 /scan、发 TF 和 /map
│ on_deactivate

inactive
│ on_cleanup

unconfigured

on_configure(约 L110)

  1. processor_type_ = PROCESSfirst_measurement_ = truestate_.reset()
  2. 新建 SMapperDataset
  3. setParams()setSolver()(pluginlib 加载 Ceres,Mapper::SetScanSolver
  4. 建 TF buffer / listener / broadcaster
  5. LaserAssistantGetPoseHelperScanHolderMapSaverLoopClosureAssistant
  6. loadPoseGraphByParams():若 YAML 给了 map_file_name 则立刻反序列化

此时还不订 /scan

on_activate(约 L157)

  1. setROSInterfaces():publisher、service、MessageFilterscan_topic_
  2. 各 LifecyclePublisher on_activate
  3. 注册 LoopClosureListener 到 Mapper(回环时发 event、请求发整图)
  4. reprocessing_transform_ 置单位阵
  5. 启动 TF 线程、可视化线程
  6. use_lifecycle_manager,建 bond

on_deactivate / on_cleanup

停线程、拆订阅和服务、拆 helper。再次 configure 是干净会话。

2.3 构造时的栈

序列化大图需要大栈。构造函数读只读参数 stack_size_to_use(默认 40e6),setrlimit(RLIMIT_STACK)。Windows 不可改。

2.4 关键成员(对照 hpp)

成员 类型 作用
smapper_ SMapper 持有 karto::Mapper
dataset_ karto::Dataset 雷达 + 扫描,写 .data
solver_ shared_ptr<ScanSolver> 默认 Ceres
lasers_ map<frame_id, LaserMetadata> 每个雷达 frame 一份模型
pose_helper_ GetPoseHelper odom ← base
laser_assistant_ LaserAssistant 扫描 + TF → Karto 雷达
map_to_odom_ tf2::Transform TF 线程要发的边
processor_type_ ProcessType 本次扫描走哪条 Process*
process_near_pose_ Pose2* 指定附近重定位的目标位姿
reprocessing_transform_ tf2::Transform 续建时新旧 odom 对齐
state_ PausedState NEW_MEASUREMENTS / PROCESSING / VISUALIZING_GRAPH
first_measurement_ bool 首帧或复位后强制处理

2.5 ProcessType(toolbox_types.hpp

枚举 谁设 Mapper 入口
PROCESS 默认建图 Process
PROCESS_FIRST_NODE 反序列化 START_AT_FIRST_NODE ProcessAtDock,一次后回到 PROCESS
PROCESS_NEAR_REGION START_AT_GIVEN_POSE/initialpose ProcessAgainstNodesNearBy
PROCESS_LOCALIZATION 定位节点 / LOCALIZE_AT_POSE ProcessLocalization

建图节点若收到 LOCALIZE_AT_POSE 会拒绝。定位节点若收到非 LOCALIZE_AT_POSE 也会拒绝。

2.6 暂停

PausedState 三路独立:

  • NEW_MEASUREMENTSshouldProcessScan 直接 false(服务 pause_new_measurements
  • PROCESSING:同步模式的 run() 不出队
  • VISUALIZING_GRAPH:不发交互图,省 RViz

reset 服务可在清空图后把 NEW_MEASUREMENTS 置暂停。

slam_toolbox 一帧数据流

03 一帧数据流

从雷达消息到 map→odom/map,这是读码的主路径。

3.1 总路径

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
/scan  LaserScan.header.frame_id = laser


tf2::MessageFilter<LaserScan>
目标坐标系 odom_frame_(默认 odom)
队列长度 scan_queue_size_(异步建议 1)
超时 transform_timeout_
│ 等不到变换:帧被 Filter 丢掉,进不了回调

laserCallback() 子类实现
│ 1) pose_helper_->getOdomPose(pose, stamp)
│ TF: 把 base_frame 恒等变换变到 odom
│ 失败打 "Failed to compute odom pose",return
│ 2) getLaser(scan)
│ 按 frame_id 缓存 LaserRangeFinder
│ 第一次:LaserAssistant 查 base←laser 外参
│ 3) shouldProcessScan(scan, pose)
│ 4) addScan / 入队

addScanImpl() common.cpp
getLocalizedRangeScan()
Mapper::Process* ()
成功:setTransformFromPoses + publishPose + dataset_->Add
失败:delete range_scan

├─ publishTransformLoop 周期发 map→odom
└─ publishVisualizations 周期 OccupancyGrid::CreateFromScans → /map

3.2 取里程计:GetPoseHelper

文件:include/slam_toolbox/get_pose_helper.hpp

1
2
3
4
base_ident.header.frame_id = base_frame_     // 默认 base_footprint
base_ident.header.stamp = scan.stamp
tf_->transform(base_ident, odom_frame_) // 得到 odom 系下的 base
Pose2(x, y, yaw)

没有 odom → base_footprint,整条链在这里断。 底盘必须发这条动态 TF。时间戳对不上(雷达用传感器钟、里程计用 now())也会抛 TransformException

3.3 门槛:shouldProcessScan(common.cpp 约 L813)

按顺序拒绝:

  1. 首帧first_measurement_):记下 pose/时间,放行,便于启动。
  2. 暂停 NEW_MEASUREMENTS
  3. 节流 scan_ctr % throttle_scans_ != 0
  4. 时间 stamp - last < minimum_time_interval_(默认常 0.5 s)。
  5. 前 5 帧scan_ctr < 5)丢掉,等稳定。注意:首帧已把 first_measurement_ 清掉,所以第 2–5 帧会被这条拦掉。
  6. 位移:默认 dist² < 0.8 * min_travel² 则丢(给匹配留 20% 余量)。check_min_dist_and_heading_precisely_=true 时,距离航向都不够才丢。

通过后更新 last_poselast_scan_time

Karto 内部还有第二次 HasMovedEnough。两道门槛叠加,车几乎不动时不会出新节点。

3.4 组装扫描:getLocalizedRangeScan

  1. scanToReadings:按 LaserMetadata.inverted 决定是否倒序距离
  2. reprocessing_transform_ * odom_pose:续建时把新 odom 拧到旧图坐标系
  3. new LocalizedRangeScan(laserName, readings)
  4. SetOdometricPose / SetCorrectedPose 先都设成变换后的里程计位姿
  5. SetTime 用扫描时间戳(秒)

3.5 addScanImpl 分派

1
2
3
4
PROCESS            → Mapper::Process
PROCESS_FIRST_NODE → ProcessAtDock,然后回到 PROCESS,并更新 reprocessing_transform_
PROCESS_NEAR_REGION→ 把 odom 位姿改成 process_near_pose_,ProcessAgainstNodesNearBy
其它 → FATAL exit

定位节点覆盖 addScan,走 PROCESS_LOCALIZATION / PROCESS_NEAR_REGION,成功后 dataset_->Add(定位窗口自己管内存)。

建图成功时:

  • 交互模式:scan_holder_->addScan 缓存原始 LaserScan 给 RViz
  • setTransformFromPoses(corrected, odom, stamp, update_reproc)
  • dataset_->Add(range_scan)
  • publishPosepublishNewNodeEvent

失败:delete range_scan。Karto 拒绝的扫描不会进图。

3.6 map→odomsetTransformFromPoses

已知:

  • corrected_pose:Karto 修正后的 base 在 map 里
  • odom_pose:当前 TF 的 base 在 odom 里

做法:

  1. 构造 base_to_map = Inverse(map_T_base_corrected)
  2. tf_->transform(base_to_map, odom_frame_) 得到 odom_T_map 的一部分
  3. map_to_odom_ = Inverse(odom_to_map)
  4. TF 线程发出 header.frame_id=map_frame_, child=odom_frame_

回环优化改的是各节点的 corrected pose,下一次 setTransformFromPoses 会让 map→odom 跳一下。odom→base 仍由底盘连续发,控制器看到的跳变在 map→odom

update_reprocessing_transform=true 时(dock / near region 刚对齐):

1
reprocessing_transform_ = odom_to_base_serialized * Inverse(odom_to_base_current)

之后新扫描的里程计都先乘这个齐,才能接到旧图。

TF 时间戳:restamp_tf_=false(默认)用 scan_stamp + transform_timeout_truenow() + timeout。超时加在 stamp 上是为了让下游 lookup 更容易成功。

3.7 /map 怎么来

publishVisualizationsmap_update_intervalupdateMap

  • 没有订阅者则直接 return(省 CPU)
  • SMapper::getOccupancyGrid(resolution_)OccupancyGrid::CreateFromScans(所有已处理扫描)
  • vis_utils::toNavMapnav_msgs/OccupancyGrid
  • 发布 /map/map_metadata

栅格不是增量维护的占据栅格滤波器,而是周期用全部扫描重绘。图很大时这个线程会变重。

参数:min_pass_through(至少几束穿过才标占用/空闲)、occupancy_threshold(击中/穿过比)。