第 10 课 · 横切设施:锁、RCU、安全与观测怎么选?
本课环境
- 源码:
D:\Linux\rk3568_linux_sdk\kernel(Linux 5.10.160,RK3568 / ARM64)- 安全:
# CONFIG_SECURITY is not set(无 LSM )、CONFIG_DEFAULT_SECURITY_DAC=y、CONFIG_SECCOMP=y/SECCOMP_FILTER=y、CONFIG_KEYS=y、# CONFIG_AUDIT is not set- RCU:
CONFIG_TREE_RCU=y、CONFIG_TASKS_RCU_GENERIC=y、无CONFIG_PREEMPT_RCU(非抢占 RCU)- 观测:
FTRACE=y、FUNCTION_TRACER=y、DYNAMIC_FTRACE=y、FUNCTION_GRAPH_TRACER=y、TRACEPOINTS=y、EVENT_TRACING=y;FTRACE_SYSCALLS/KPROBES/PERF_EVENTS/BPF_SYSCALL/DEBUG_FS/KASAN全部未启用- 阅读:约 35--45 分钟 | 实验:10 分钟,不需要板子
- 配套速查表:横切设施速查表(锁选型 / RCU / 安全 / 观测)
承接上一课
前九课,每个子系统都在"偷偷使用"同一批东西:
| 课 | 用到了什么横切设施 |
|---|---|
| 01 启动 | initcall 的执行是靠 mutex 和完成量串起来的 |
| 02 调度 | 运行队列 rq 有 rq->lock;task_struct 用引用计数 |
| 03 内存 | SLUB 有 per-CPU freelist,减少锁竞争 |
| 04 虚拟内存 | mmap_lock 是读写信号量 |
| 05 中断 | softirq 预算、ksoftirqd、local_irq_save |
| 06 VFS | dcache 用 RCU-walk + rename_lock(seqlock) |
| 07 存储 | 页缓存 xarray 用 RCU 读 |
| 08 网络 | 路由表、neigh 表用 RCU |
| 09 设备模型 | device_add 有一整套锁 |
这一课一次性把它们讲透。 全课只回答三个问题:
① 该用哪把锁? ② RCU 到底在等什么? ③ 这块板子上哪些安全/观测能力真的存在?
本文速览
- 锁选型第一步不是"哪个快",而是"当前上下文能不能睡"
- spinlock 三个变体 :
_irqsave(防硬中断)、_bh(防软中断)、裸的(都不防) - RCU 的真相 :本机非抢占 RCU,
rcu_read_lock()展开后就是preempt_disable() - 宽限期只等"更新前已在读侧"的读者 ------更新后进来的读者不延长宽限期
CONFIG_LSM字符串存在 ≠ LSM 生效 :CONFIG_SECURITY=n时它只是一个无效字符串- 一个开关砍掉一族能力 :
CONFIG_DEBUG_KERNEL未设置 ⇒ LOCKDEP / KASAN / DEBUG_ATOMIC_SLEEP 全没有 - 本机唯一可用的观测手段是 ftrace + 事件追踪(没有 perf、没有 bpftrace、没有 kprobe_events)
- 实测时间线 :R1 在 t=3 离开不够,必须等 R2 在 t=5 离开,宽限期才结束
一、两分钟热身:上一课的三个问题
Q1:platform_match() 的匹配顺序?
driver_override→ OF compatible → ACPI → id_table → 驱动名。设备树的compatible是最优先的常规依据。
Q2:驱动 probe 返回 -EPROBE_DEFER 意味着什么?
依赖(clk/phy/regulator/pinctrl)尚未就绪,设备进入 pending 队列,provider 注册后自动重试,
late_initcall兜底。
Q3:为什么 sdmmc0 的驱动匹配表里只有 rockchip,rk3288-dw-mshc 也能驱动 RK3568?
因为 DTS 的第二个 compatible 就是回退项,驱动用它"认领"设备;真正的 SoC 差异在 probe 里用
of_device_is_compatible()分辨(dw_mmc-rockchip.c:354)。
二、同步原语:先问"能不能睡"
这是全课最重要的一条决策原则:
选锁的第一步不是"哪个更快",而是"当前上下文能不能睡眠"。
text
当前代码跑在什么上下文?
│
├── 硬中断 / 软中断 / 持锁路径(原子上下文)
│ │
│ └──▶ 不能睡 ⇒ 只能选:
│ atomic_t / refcount_t
│ spinlock_t(+ _irqsave / _bh 变体)
│ seqlock_t(读多写少、可重试)
│ RCU(读多写少、指针替换)
│
└── 进程上下文
│
└──▶ 能睡 ⇒ 可选:
mutex(长临界区)
rw_semaphore(读多写少)
completion / waitqueue(等事件)
semaphore(计数)
2.1 全家福对照
| 原语 | 能睡眠? | 适用 | 关键约束 |
|---|---|---|---|
atomic_t / refcount_t |
否 | 单字计数、引用计数 | 不能保护多字段不变量 |
spinlock_t |
否 | 极短临界区、中断/软中断共享 | 持锁不能睡眠 |
rwlock_t |
否 | 读多写少、原子上下文 | 写者可能饥饿;新代码多用 rwsem/RCU |
seqlock_t |
否 | 读多写少、读者可重试 | 读者可能读到撕裂数据,必须校验序列号 |
mutex |
是 | 进程上下文、较长临界区 | 中断里禁用 ;有优先级继承变体 rtmutex |
rw_semaphore |
是 | 进程上下文、读多写少 | down_read/up_read;写者可能饥饿 |
semaphore |
是 | 计数信号量 | 语义比 mutex 宽,容易滥用 |
completion / waitqueue |
是 | 等待事件/条件 | 不要用轮询代替等待队列 |
RCU |
读侧否,写侧可等 | 读多写少、指针替换 | 读侧不能睡眠;旧数据要等宽限期 |
2.2 三个变体,别用错
text
spin_lock() → 什么都不关
▲ 适用:确定没有中断/软中断会碰这份数据
│
spin_lock_bh() → 关软中断(bottom half)
▲ 适用:只有软中断(如网络收包)会竞争
│
spin_lock_irqsave() → 保存并关本地硬中断
适用:硬中断 handler 也可能访问同一数据
为什么必须区分? 看这个经典死锁:
text
线程 A(进程上下文) 中断 handler(同一 CPU)
───────────────────── ────────────────────────
spin_lock(&L); ← 拿到锁
│
│ ← 中断在这里到来!
│ spin_lock(&L); ← 等锁
│ 但锁在 A 手里,
│ 而 A 正被中断打断......
│ 中断永远等不到 ⇒ 死锁
spin_unlock(&L);
用 spin_lock_irqsave() 就不会:它在拿锁的同时关掉了本地中断,
中断 handler 根本没机会在这个窗口里运行。
📌
_bh与_irqsave的选择表格化记法:谁可能碰这份数据,就防谁。
只有软中断碰 →
_bh;硬中断也可能碰 →_irqsave;都不碰(或已经有别的机制保证)→ 裸spin_lock。拿不准时用
_irqsave------它代价略高但不会错。
2.3 锁的顺序比锁的类型更重要
text
✅ 全局统一顺序:先 L1 再 L2
线程 A: lock(L1) → lock(L2)
线程 B: lock(L1) → lock(L2)
❌ 顺序不一致 ⇒ ABBA 死锁
线程 A: lock(L1) → 等 L2
线程 B: lock(L2) → 等 L1
↑ 谁也走不了
lockdep 能在运行时发现顺序反转------但本机 CONFIG_DEBUG_KERNEL 未设置,LOCKDEP 不可用
(CONFIG_LOCKDEP_SUPPORT=y 只说明架构支持,不代表功能开启)。
所以在本机上,锁顺序只能靠代码审查和约定。
三、RCU:读多写少的"无锁"读侧
3.1 核心思想
读者不阻塞写者,写者不阻塞读者;写者发布新版本,等所有旧读者离开后再释放旧版本。
text
写者 读者们
│ │
│ ① 准备一个全新对象 │ 正在读 old
│ (不改 old!) │
│ │
├─② rcu_assign_pointer() ──▶│ 新读者立刻看到 new(原子发布)
│ (release 语义) │ 但老读者还拿着 old
│ │
├─③ synchronize_rcu() │
│ 等"更新前的读者"全部离开 │
│ ......(宽限期)...... │
│ ▼
├─④ 现在才 kfree(old) R1、R2 都走了
│
关键点在于第 ①:写者不修改 old,而是造一个新的。 这就是为什么"RCU 没有写者互斥"------
因为老读者看到的老对象内容从头到尾没变过。
3.2 四个 API
| 角色 | API | 位置 | 关键约束 |
|---|---|---|---|
| 读者进入 | rcu_read_lock() |
include/linux/rcupdate.h:647 |
临界区内不能睡眠 |
| 读指针 | rcu_dereference() |
rcupdate.h:566 |
保证读到的是完整发布过的指针 |
| 写者发布 | rcu_assign_pointer() |
rcupdate.h:424 |
release 语义,保证新对象初始化先于指针可见 |
| 写者等待(同步) | synchronize_rcu() |
kernel/rcu/tree.c:3672 |
会阻塞睡眠,等宽限期结束 |
| 写者等待(异步) | call_rcu(head, func) |
kernel/rcu/tree.c:3039 |
注册回调,宽限期结束后在软中断上下文执行 |
| 读者退出 | rcu_read_unlock() |
rcupdate.h:699 |
3.3 ⭐ 本机非抢占 RCU 的真相:读侧就是 preempt_disable()
这是本课最硬的一个"为什么"。看源码(include/linux/rcupdate.h:58--80,原文):
c
#else /* #ifdef CONFIG_PREEMPT_RCU */ /* ← 本机走这个分支 */
static inline void __rcu_read_lock(void)
{
preempt_disable(); /* :68 ← 全部内容就这一行 */
}
static inline void __rcu_read_unlock(void)
{
preempt_enable(); /* :73 */
rcu_read_unlock_strict(); /* :74 调试用 */
}
static inline int rcu_preempt_depth(void)
{
return 0; /* 非抢占 RCU 没有嵌套深度概念 */
}
所以在 RK3568 这块板子上:
text
rcu_read_lock() == preempt_disable() → 只增加一个抢占计数
rcu_read_unlock() == preempt_enable() → 只减少一个抢占计数
没有锁、没有原子操作、没有内存屏障、不会和其他 CPU 竞争。
这就是"RCU 读侧几乎零开销"的真正原因。
为什么这样就安全? 因为:
preempt_disable()保证这段代码不会被抢占切走;- 它是"读者正在读"的一个隐式标记 (内核能通过
preempt_count判断有没有人在读侧); - 写者等宽限期,本质上就是等所有 CPU 都报告"我已经不在那个时间点了"。
⚠️ 这也解释了 RCU 读侧的硬约束
读侧临界区里绝对不能睡眠 ------因为一旦睡眠,
preempt_disable的语义就被打破了,而内核的"宽限期检测"会因此卡住(甚至触发 RCU stall 报警)。
这也是为什么
rcu_read_lock()里不能调kmalloc(GFP_KERNEL)、不能mutex_lock、不能 I/O。
3.4 宽限期:只等"更新前已在读侧"的读者
这是 RCU 最反直觉、也最容易搞错的一点。实测时间线(assets/rcu_sim.py 真实输出):
text
=== RCU timeline ===
Writer will publish at t=2.0. Watch which readers are pre-existing
and which ones join after the publish.
t=0.0 R1: rcu_read_lock(), reads old pointer
t=1.0 R2: rcu_read_lock(), reads old pointer
t=2.0 writer: rcu_assign_pointer(new); pre-existing readers = ['R1', 'R2']
synchronize_rcu() waits; call_rcu(old, free_old) registered
new readers may join without extending the GP
t=2.5 R3: rcu_read_lock(), reads new pointer
t=3.0 R1: rcu_read_unlock() (quiescent state)
t=5.0 R2: rcu_read_unlock() (quiescent state)
t=5.0 grace period complete: all pre-existing readers passed a quiescent state
synchronize_rcu() returns; call_rcu callback frees old data
t=6.0 R3: rcu_read_unlock() (quiescent state)
R1: enter=0.0, exit=3.0, sees=old
R2: enter=1.0, exit=5.0, sees=old
R3: enter=2.5, exit=6.0, sees=new
grace period done=True, old data freed=True
把这条时间线画出来:
text
t=0 t=1 t=2 t=2.5 t=3 t=4 t=5 t=6
│ │ │ │ │ │ │ │
R1 ├──────┴──────┴──────┴──────┴──────┘ │ │ sees=old
R2 ├──────┴──────┴──────┴──────┴──────┴──────┘ sees=old
R3 ├──────┴──────┴──────┴──────┴──────┘ sees=new
▲
writer 发布 new(t=2)
│
└──▶ 宽限期从这里开始
等 R1、R2(**只等这两位**)
│
└──▶ t=5 R2 离开 ⇒ 宽限期结束
old 被释放
R1 在 t=3 离开**不够**------因为 R2 还在读 old!
R3 在 t=2.5 进入时已经读到 new,**不延长**宽限期。
一句话记住:宽限期只等"发布那一刻已经在读侧的人"。
💡 为什么 R3 不延长宽限期?
因为
R3在 t=2.5 才进入临界区,而此时指针已经指向 new 。它不可能持有 old 指针,所以它读多久都不影响 old 的安全性。
这个规则让 RCU 在"持续有读者进入"的场景下依然能结束宽限期------
否则一个高频读的系统就永远无法回收旧数据了。
3.5 对比:同样三个读者,用 mutex 会怎样
实测输出(同一份脚本的后半段):
text
=== mutex comparison (same reader intervals) ===
t=0.0 R1: mutex_lock(read) -> holds lock
t=1.0 R2: mutex_lock(read) -> blocks behind R1
t=2.0 writer: mutex_lock(write) -> blocks behind readers
t=2.5 R3: mutex_lock(read) -> blocks behind writer
t=3.0 R1: unlock; R2 gets lock
t=5.0 R2: unlock; writer gets lock and updates
t=5.0 writer: unlock; R3: gets lock and reads new data
RCU lets R3 read the new pointer at t=2.5 without blocking;
the writer only waits for the readers that existed at publish time.
并排对比最关键的那一格:
| 时刻 | RCU 下 R3 的行为 | mutex 下 R3 的行为 |
|---|---|---|
| t=2.5 | 立刻读到 new,不阻塞 | 被写者挡住,等到 t=5 |
| t=5.0 | 已读了 2.5 个时间单位 | 才开始读 |
| 对写者 | 完全不阻塞 | 写者要等所有读者 |
这就是 RCU 在"读多写少"场景的全部价值 :读者永远不排队。
代价是------旧数据必须等宽限期,不能立即释放。
3.6 RCU 不是"不用同步"
💡 最常见的误解
RCU 只保证"旧对象在你读的时候不会被释放" ;
它不保护对象内部的可变字段。
text✅ 适合 RCU 的数据 ❌ 不适合 RCU 的数据 ───────────────── ───────────────────── 只读的配置表 会被原地修改的计数器 路由表(整表替换) 链表里频繁插入/删除节点 策略表(整份替换) 需要保证多字段一致性的结构 neigh 邻居表共享数据要么只读 ,要么用其他机制保护------RCU 替代不了锁。
四、安全:DAC / capability / LSM / seccomp
安全设施是分层的,每层解决不同问题:
text
┌────────────────────────────────────────────────────────┐
│ 应用层:认证、加密、权限校验 │
├────────────────────────────────────────────────────────┤
│ seccomp filter 限制进程能调用哪些 syscall ← 本机有 ✅ │
├────────────────────────────────────────────────────────┤
│ LSM 框架 强制访问控制(SELinux/AppArmor...)← 本机无 ❌│
├────────────────────────────────────────────────────────┤
│ capability 细粒度特权(CAP_NET_ADMIN...) ← 本机有 ✅ │
├────────────────────────────────────────────────────────┤
│ DAC uid / gid / 权限位 ← 本机有 ✅ │
└────────────────────────────────────────────────────────┘
4.1 本机实测
| 能力 | .config 实际值 |
状态 |
|---|---|---|
| DAC | CONFIG_DEFAULT_SECURITY_DAC=y |
✅ 传统 uid/gid/权限位 |
| LSM 框架 | # CONFIG_SECURITY is not set |
❌ 没有 SELinux / AppArmor / Smack / Tomoyo / Yama / LoadPin |
| seccomp | CONFIG_SECCOMP=y + CONFIG_SECCOMP_FILTER=y |
✅ 进程可限制自己的 syscall |
| keys | CONFIG_KEYS=y |
✅ 内核密钥环 |
| audit | # CONFIG_AUDIT is not set |
❌ 没有审计子系统 |
| IMA / EVM | 未启用 | ❌ 没有完整性度量/验证 |
4.2 ⭐ 最坑人的一处:CONFIG_LSM 字符串的陷阱
本机 .config 里明明有这一行:
text
CONFIG_LSM="lockdown,yama,loadpin,safesetid,integrity,bpf"
看起来像是有 6 个 LSM 模块在跑,实际上一个都没有。
text
CONFIG_LSM="lockdown,yama,..." ← 只是一个"想启用哪些 LSM"的**配置字符串**
│
│ 但它生效的前提是 LSM 框架本身被编译
▼
CONFIG_SECURITY=y ──▶ security/security.c 被编译
└──▶ 才会去读 CONFIG_LSM 并按顺序挂钩子
│
└── 本机是 `# CONFIG_SECURITY is not set`
⇒ 整个 security/ 目录根本没编译
⇒ CONFIG_LSM 是一行**无效字符串**
怎么一眼验证? 看这几个都不存在就对了:
bash
ls /sys/kernel/security/ # 本机没有(securityfs 未编译)
cat /sys/kernel/security/lsm # 更没有这一行
dmesg | grep -i lsm # 不会有 LSM 初始化日志
⚠️ 排障启示
"权限失败"在这块板子上永远不会是 SELinux 造成的 ------
因为没有 SELinux。请查 DAC 权限位、capability、seccomp。
反过来,如果你从"标准 Android"移植过来,别指望 SELinux 策略会生效。
4.3 seccomp:本机可用,且很强
seccomp 的入口是 SYSCALL_DEFINE3(seccomp, ...)(kernel/seccomp.c:1939)。
text
进程自己调用 prctl(PR_SET_SECCOMP) / seccomp()
│
▼
装一个 BPF 程序(经典 BPF,不是 eBPF!)
│
▼
此后每次 syscall 都先过这个过滤器
│
├── 放行
├── 返回错误码(如 EPERM)
├── kill 整个进程
└── 转给 ptrace 的 tracer
📌 注意一个容易混淆的点
seccomp 用的 BPF 不等于 eBPF。
本机
# CONFIG_BPF_SYSCALL is not set(没有 eBPF、没有 bpftrace),但
CONFIG_SECCOMP_FILTER=y(seccomp 的 cBPF 过滤器照常用)。"没有 BPF" 这句话要说清楚是哪一个 BPF。
五、观测:printk、ftrace、tracepoints
内核可观测性同样受配置限制。本机的能力矩阵:
| 能力 | 配置 | 状态 |
|---|---|---|
| printk / dmesg | 核心功能 | ✅ |
| ftrace 函数追踪 | CONFIG_FTRACE=y、FUNCTION_TRACER=y、DYNAMIC_FTRACE=y |
✅ |
| 函数调用图 | CONFIG_FUNCTION_GRAPH_TRACER=y |
✅ |
| tracepoints / 事件追踪 | CONFIG_TRACEPOINTS=y、EVENT_TRACING=y |
✅ |
| syscall 追踪 | # CONFIG_FTRACE_SYSCALLS is not set |
❌ |
| kprobes / uprobes | # CONFIG_KPROBES is not set |
❌ 没有 kprobe_events |
| perf events | # CONFIG_PERF_EVENTS is not set |
❌ 没有 perf 命令 |
| eBPF | # CONFIG_BPF_SYSCALL is not set |
❌ 没有 bpftrace / BCC |
| debugfs | # CONFIG_DEBUG_FS is not set |
❌(tracefs 仍可用) |
| lockdep / KASAN | 未启用 | ❌ 死锁/内存错误靠代码审查 |
5.1 一个开关砍掉一整族能力
text
# CONFIG_DEBUG_KERNEL is not set
│
├──▶ LOCKDEP 不可用 (依赖 DEBUG_KERNEL)
├──▶ KASAN 不可用 (依赖 DEBUG_KERNEL)
├──▶ DEBUG_ATOMIC_SLEEP 不可用("持锁睡眠"检查,依赖 DEBUG_KERNEL)
└──▶ 大部分 DEBUG_* 全部不可用
⚠️
CONFIG_LOCKDEP_SUPPORT=y不代表 LOCKDEP 可用前者只是说"这个架构支持 lockdep"(几乎每个架构都是 y),
后者才是"启用 lockdep"。看 .config 时要看
CONFIG_LOCKDEP,不是_SUPPORT。这类"
_SUPPORT后缀"的坑在本机还有一个:CONFIG_STACKTRACE_SUPPORT=y。
5.2 ftrace:本机唯一的高性能观测手段
入口是 tracefs,通常挂在 /sys/kernel/tracing:
bash
mount -t tracefs none /sys/kernel/tracing # 没有 debugfs 也能挂 tracefs
ls /sys/kernel/tracing
| 文件 | 作用 |
|---|---|
available_tracers |
本机支持哪些 tracer(先看这个) |
current_tracer |
写入 function / function_graph / nop 切换 |
trace |
读追踪结果 |
trace_pipe |
流式读(阻塞等待新事件) |
set_ftrace_filter |
只追踪指定函数(性能关键) |
events/ |
所有 tracepoint 事件的树形目录 |
一个最小实验序列:
bash
# 1. 看有哪些 tracer 可用
cat /sys/kernel/tracing/available_tracers
# nop function function_graph ← 本机预期(没有 irqsoff/preemptoff,因为 IRQSOFF_TRACER 未启用)
# 2. 只追踪一个函数,避免日志爆炸
echo 'stmmac_xmit' > /sys/kernel/tracing/set_ftrace_filter
echo function > /sys/kernel/tracing/current_tracer
echo 1 > /sys/kernel/tracing/tracing_on
# ...在这里做一次网络发送...
echo 0 > /sys/kernel/tracing/tracing_on
cat /sys/kernel/tracing/trace | head -30
# 3. 换成函数图(能看调用深度和耗时)
echo nop > /sys/kernel/tracing/current_tracer
echo function_graph > /sys/kernel/tracing/current_tracer
cat /sys/kernel/tracing/trace | head -40
# 4. 事件追踪(结构化数据,比函数追踪好用)
echo 1 > /sys/kernel/tracing/events/sched/sched_switch/enable
cat /sys/kernel/tracing/trace | head -20
💡
set_ftrace_filter是本机最重要的技巧因为
CONFIG_FUNCTION_TRACER=y追踪的是全部内核函数 ------不设过滤器的话,一秒就能产生几十万行日志,把 ring buffer 冲掉。
先过滤,再开追踪。
⚠️ 上板第一件事不是跑命令,而是确认命令有意义
bashwhich perf; which bpftrace; ls /sys/kernel/debug # 本机:全部不存在 ls /sys/kernel/tracing # 本机:存在很多"我不会用 perf"其实是"这个内核没编 perf"。先看配置,再设计方案。
六、动手实验:观察 RCU 宽限期
assets/rcu_sim.py 用一条时间线模拟 R1/R2/R3 三个读者、一个写者,
以及 synchronize_rcu() / call_rcu()。它把"为什么旧数据不能立刻释放"变成可观察的事件序列。
任务 1:跑时间线
bash
python -I assets/rcu_sim.py
真实输出见 3.4 / 3.5 节。先自己预测,再对答案:
text
Q1:宽限期在哪个时刻结束?(t=3 还是 t=5?)
Q2:R3 会不会延长宽限期?
Q3:R1 在 t=3 离开时,为什么 old 还不能释放?
答案:
Q1:t=5 。R2 在 t=5 才离开。
Q2:不会 。R3 在 t=2.5 进入,此时指针已指向 new,它不可能持有 old。
Q3:因为 R2 还在读 old (R2 从 t=1 读到 t=5)。
宽限期要等所有"发布时已在读侧"的读者都离开,只走一个不够。
任务 2:看 mutex 版本的差别
同一份输出的后半段(3.5 节)已经展示了:mutex 下 R3 在 t=2.5 被写者挡住,
直到 t=5 才拿到锁。
这道题的真正价值在于让你想清楚:
text
如果你的场景是"读非常多、写非常少"(路由表、配置表、策略表)
→ RCU 让读者永不排队,收益巨大
→ 代价:写者要等宽限期,旧数据延迟释放
如果你的场景是"读写都频繁"或"需要保证多字段一致性"
→ RCU 不合适,老老实实用锁
任务 3:回到源码核对
bash
K=/d/Linux/rk3568_linux_sdk/kernel
# ① 读侧到底做了什么(本课最关键的一条)
grep -n "CONFIG_PREEMPT_RCU" -A 20 $K/include/linux/rcupdate.h | head -30
# ② 四个核心 API
grep -n "rcu_read_lock(void)" -A 8 $K/include/linux/rcupdate.h | head -20
grep -n "rcu_assign_pointer" -A 15 $K/include/linux/rcupdate.h | head -25
grep -n "void synchronize_rcu(void)" -A 20 $K/kernel/rcu/tree.c
grep -n "void call_rcu(struct rcu_head" -A 8 $K/kernel/rcu/tree.c
# ③ 确认本机是非抢占 RCU
grep -E "CONFIG_TREE_RCU|CONFIG_PREEMPT_RCU|CONFIG_PREEMPT_NONE" $K/.config
# ④ 本课新增:验证"一个开关砍掉一族能力"
grep -E "CONFIG_DEBUG_KERNEL|CONFIG_LOCKDEP|CONFIG_LOCKDEP_SUPPORT|CONFIG_KASAN" $K/.config
# ⑤ 本课新增:验证 CONFIG_LSM 陷阱
grep -E "^CONFIG_LSM=|^# CONFIG_SECURITY is not set" $K/.config
第 ① 条会打出 rcupdate.h:66--75 那两行 preempt_disable() / preempt_enable()------
这就是"RCU 读侧为什么几乎零开销"的源码证据。
任务 4(可选,需要板子)
bash
mount -t tracefs none /sys/kernel/tracing 2>/dev/null
ls /sys/kernel/tracing | head
cat /sys/kernel/tracing/available_tracers
# 只用事件追踪,轻量
echo 1 > /sys/kernel/tracing/events/sched/sched_switch/enable
cat /sys/kernel/tracing/trace | head -30
echo 0 > /sys/kernel/tracing/events/sched/sched_switch/enable
# 确认"哪些工具真的不存在"
which perf bpftrace 2>/dev/null; echo "---"
ls /sys/kernel/debug 2>/dev/null || echo "no debugfs"
cat /sys/kernel/security/lsm 2>/dev/null || echo "no LSM"
dmesg | tail -30
如果 tracefs 不可用,就退回 dmesg。
本机没有 perf/kprobes/BPF,不要期待 perf、bpftrace、kprobe_events 能工作。
实验检查点
- 我能按上下文(能否睡眠)选出合适的锁
- 我能解释
spin_lock/_bh/_irqsave三者的区别,并说出用错会怎么死锁 - 我能解释 RCU 宽限期只等"更新前已在读侧"的读者
- 我能说明本机
rcu_read_lock()展开后是什么(preempt_disable()) - 我能说清本机 LSM/audit 未启用,以及
CONFIG_LSM字符串为什么不生效 - 我能说清 ftrace 可用、perf/kprobes/BPF 不可用
- 我能解释为什么旧数据不能在更新后立即释放
七、检索练习
先不要翻回上文。凭记忆作答。
Q1. 在 hardirq 中保护共享数据,应优先使用?
A. mutex_lock | B. down_read | C. spin_lock_irqsave | D. wait_event
Q2. RCU 写者何时可以释放旧数据?
A. 宽限期结束,旧读者离开 | B. 更新指针后立即释放 | C. 所有 CPU 都重启后 | D. 下一次内存回收时
Q3. 哪种场景适合用 mutex 而不是 spinlock?
A. 中断上下文短临界区 | B. 进程上下文可睡眠长临界区 | C. 只读共享数据 | D. 单字节计数器
Q4. 本机安全配置的特点是?
A. SELinux + AppArmor 启用 | B. 只有 TOMOYO | C. 只有 IMA | D. LSM 未启用,有 DAC + seccomp/keys
Q5. 本机可用的主要观测手段是?
A. perf + kprobes + BPF | B. 只有 ftrace/事件追踪 | C. 只有 KASAN | D. 只有 lockdep
答案与解析
Q1 → C。 hardirq 不能睡眠,所以不能用 mutex/rwsem/wait_event。
spin_lock_irqsave会保存并关闭本地中断,避免中断重入死锁。Q2 → A。 更新后仍可能有读者持有旧指针;必须等这些"更新前的读者"都通过 quiescent state,
也就是宽限期结束。更新后才进入的读者看到新指针,不延长宽限期。
Q3 → B。 mutex 可以睡眠,适合进程上下文、临界区较长、可能调用阻塞函数的场景。
中断上下文只能用 spinlock/atomic/RCU;只读共享数据可用 RCU/seqlock。
Q4 → D。
CONFIG_SECURITY未设置,LSM 框架没有编译;SELinux/AppArmor 等不会生效。但 DAC、capability、seccomp filter 与 keys 仍然可用,audit/IMA 未启用。
注意
.config里的CONFIG_LSM="lockdown,yama,..."是一行无效字符串。Q5 → B。 本机
FTRACE/TRACEPOINTS/EVENT_TRACING开启,函数追踪、函数图、事件追踪可用;
PERF_EVENTS、KPROBES、BPF_SYSCALL、DEBUG_FS、LOCKDEP、KASAN 都未启用。
八、四个常见误区
| 误区 | 真相 |
|---|---|
| "RCU 读者不需要任何约束。" | 读侧不能睡眠,共享数据必须不可变或另行保护;旧数据要等宽限期。 |
| "spinlock 可以在任何上下文睡眠。" | 持 spinlock 睡眠会死锁/调度爆炸;中断上下文只能用原子锁。 |
| "SELinux 一定在。" | 本机 CONFIG_SECURITY 未启用,LSM 全部不生效(尽管 .config 里有 CONFIG_LSM 字符串)。 |
| "perf/bpftrace 一定能用。" | 本机没编 perf events、kprobes、BPF syscall、debugfs。 |
九、本课速查
9.1 一图流
text
┌─ 选锁:先问上下文 ────────────────────────────────────┐
│ 原子上下文(中断/软中断/持锁) │
│ → atomic / spinlock(_irqsave|_bh) / seqlock / RCU │
│ 进程上下文(可睡眠) │
│ → mutex / rwsem / completion / waitqueue │
│ 读多写少 + 指针替换 → RCU │
└───────────────────────────────────────────────────────┘
┌─ RCU 生命周期 ───────────────────────────────────────┐
│ 写者:造新对象 → rcu_assign_pointer() → 等宽限期 │
│ (synchronize_rcu 阻塞 / call_rcu 回调) │
│ 读者:rcu_read_lock() = preempt_disable()(本机) │
│ rcu_dereference() 取指针 → 用完 unlock │
│ 宽限期:只等"发布时已在读侧"的读者 │
│ 宽限期后:才能 kfree 旧对象 │
└───────────────────────────────────────────────────────┘
┌─ 安全分层(本机)────────────────────────────────────┐
│ seccomp ✅ > LSM ❌ > capability ✅ > DAC ✅ │
└───────────────────────────────────────────────────────┘
┌─ 观测(本机)────────────────────────────────────────┐
│ dmesg ✅ ftrace ✅ 事件追踪 ✅ │
│ perf ❌ kprobes ❌ BPF ❌ debugfs ❌ │
└───────────────────────────────────────────────────────┘
9.2 关键源码索引(逐条核对过)
| 位置 | 内容 |
|---|---|
include/linux/rcupdate.h:66--75 |
非抢占 RCU 的 __rcu_read_lock/unlock = preempt_disable/enable() |
include/linux/rcupdate.h:424 / 566 / 647 / 699 |
rcu_assign_pointer() / rcu_dereference() / rcu_read_lock() / rcu_read_unlock() |
kernel/rcu/tree.c:3039 / 3672 / 4531 |
call_rcu() / synchronize_rcu() / rcu_init() |
kernel/locking/mutex.c:296 |
mutex_lock() |
kernel/locking/rwsem.c:1531 |
down_read() |
kernel/seccomp.c:1939 |
SYSCALL_DEFINE3(seccomp, ...) |
kernel/trace/ftrace.c |
ftrace 核心(ftrace_trace_function 等) |
security/security.c |
LSM hook 分发(本机未编译) |
9.3 本机配置矩阵
| 项 | 值 | 结论 |
|---|---|---|
CONFIG_PREEMPT_NONE |
y | 非抢占内核 |
CONFIG_TREE_RCU |
y | tree RCU |
CONFIG_PREEMPT_RCU |
不存在 | ⇒ 读侧 = preempt_disable() |
CONFIG_SECURITY |
not set | 无 LSM |
CONFIG_LSM |
"lockdown,yama,loadpin,safesetid,integrity,bpf" |
无效字符串 |
CONFIG_SECCOMP / SECCOMP_FILTER |
y / y | seccomp 可用(cBPF) |
CONFIG_KEYS |
y | 密钥环可用 |
CONFIG_AUDIT |
not set | 无审计 |
CONFIG_DEBUG_KERNEL |
not set | ⇒ LOCKDEP / KASAN / DEBUG_ATOMIC_SLEEP 全无 |
CONFIG_LOCKDEP_SUPPORT |
y | 只是"架构支持",不是启用 |
CONFIG_FTRACE / FUNCTION_TRACER / FUNCTION_GRAPH_TRACER |
y / y / y | 函数追踪、函数图可用 |
CONFIG_TRACEPOINTS / EVENT_TRACING |
y / y | 事件追踪可用 |
CONFIG_PERF_EVENTS / KPROBES / BPF_SYSCALL / DEBUG_FS |
全 not set | perf/kprobe/eBPF/debugfs 全无 |
CONFIG_FTRACE_SYSCALLS |
not set | 不能用 ftrace 追 syscall |
9.4 "配置看一半会骗人"清单(本课收集)
| 你看到的 | 真实含义 |
|---|---|
CONFIG_LSM="lockdown,yama,..." |
只是一行配置字符串;CONFIG_SECURITY=n 时完全不生效 |
CONFIG_LOCKDEP_SUPPORT=y |
架构支持 ,不等于启用 (要看 CONFIG_LOCKDEP) |
CONFIG_STACKTRACE_SUPPORT=y |
同上,不等于有栈回溯 |
CONFIG_SECCOMP_FILTER=y 但 CONFIG_BPF_SYSCALL=n |
seccomp 的 cBPF 可用,eBPF/bpftrace 不可用 |
CONFIG_NET_SCHED=n 但仍有 pfifo_fast |
框架关了,内建默认 qdisc 仍在(第 08 课) |
CONFIG_SQUASHFS=y 但 Buildroot 没生成镜像 |
内核能读,不代表 rootfs 是它(第 07 课) |
结语
横切设施的选型,全部从上下文出发:
能否睡眠决定锁的类型;读多写少决定是否用 RCU/seqlock;配置决定安全与观测工具是否存在。
而 RCU 这一节,最该带走的是一个源码级的事实:
RCU 的核心不是"无锁",而是"读侧不阻塞写者、旧数据等宽限期再释放"。
在这块板子上,
rcu_read_lock()展开后就是preempt_disable()------一条抢占计数,没有锁、没有原子操作、没有跨核竞争。
这也正是它绝对不能在读侧睡眠的原因:一旦睡了,"我还在这段时间点"这个隐式声明就失效了。
还有一个必须记住的排障纪律:
在这块板子上,"权限失败"不会来自 SELinux,"性能瓶颈"不能用 perf 看,
"函数耗时"不能靠 kprobe。你手里真正能用的只有
dmesg+ftrace+ 事件追踪 +/proc+/sys。上板第一件事不是跑命令,而是确认命令有意义。
下一篇:第 11 课《ARM64 体系结构:EL、页表、启动与 PSCI》
前九课的所有子系统,都建立在"机器相关层"之上。
这一课补上最关键的地基:EL0--EL3 的分工、39 位 VA / 三级页表怎么算出来、
primary_entry 如何把主核带进 start_kernel、PSCI 如何通过 SMC 唤醒从核,
以及 RK3568 的 DTS 里到底写了哪些硬件事实。
我们会跑 arm64_probe.py,从 .config + System.map + DTS 三路交叉验证 VA 布局和关键符号地址。
本课所有行号均已在本机 D:\Linux\rk3568_linux_sdk\kernel(Linux 5.10.160)源码上逐条核对;实验输出为 assets/rcu_sim.py 的真实运行结果(含 RCU 与 mutex 两条时间线对比)。安全与观测能力取自 .config 实测,其中 CONFIG_LSM 字符串与 CONFIG_SECURITY 的矛盾、CONFIG_DEBUG_KERNEL 未设置导致 LOCKDEP/KASAN 全不可用,均已逐行确认。