第 1-10 课: 横切设施:锁、RCU、安全与观测怎么选?

第 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 读侧几乎零开销"的真正原因。

为什么这样就安全? 因为:

  1. preempt_disable() 保证这段代码不会被抢占切走;
  2. 它是"读者正在读"的一个隐式标记 (内核能通过 preempt_count 判断有没有人在读侧);
  3. 写者等宽限期,本质上就是等所有 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 冲掉。

先过滤,再开追踪。
⚠️ 上板第一件事不是跑命令,而是确认命令有意义

bash 复制代码
which 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 全不可用,均已逐行确认。

相关推荐
臻一1 小时前
rk3568---ftp无法连接
linux·ftp·3568
自由彩虹2 小时前
靶场通关16--victim1
linux·服务器·网络·安全·web安全·网络安全
叼烟扛炮2 小时前
Linux 学习笔记(三十九):网络基石——TCP/IP 模型与 UDP 实战
linux·运维·网络·笔记·学习·tcp/ip
闲云野鹤在人间2 小时前
头秃修炼手册:StorageClass+StatefulSet+ETCD 全套渡劫笔记
linux·数据库·笔记·云原生·kubernetes·etcd
wuminyu2 小时前
JVM规范学习总结:class执行性能差异
java·linux·c语言·jvm·c++
EVEN-09132 小时前
Linux-线程控制
linux·jvm
刚入门的大一新生2 小时前
Linux-线程控制
linux·运维·服务器
C++ 老炮儿的技术栈2 小时前
Linux 动态库踩坑:明明重新编译了 .so,调用函数返回值却一直不变(libfjrobot.so 版本号异常排查实录)
linux·c语言·网络·c++·visual studio
你看我名字3 小时前
云计算工程师技能树:从 Linux 基础到云平台架构能力
linux·架构·云计算