kernel/kcsan 内核并发消毒器(KCSAN)机制与原理详解
源码路径:rk3588/kernel-6.1/kernel/kcsan/
内核版本:Linux 6.1(RK3588 平台)
平台:RK3588(4×Cortex-A76 + 4×Cortex-A55 big.LITTLE,ARM64)
该目录实现 KCSAN(Kernel Concurrency Sanitizer,内核并发消毒器) — Linux 内核的动态 数据竞争(Data Race) 检测框架。通过编译器插桩(基于 ThreadSanitizer 接口)在运行时以 watchpoint 采样 方式检测未同步的并发内存访问,并支持 ASSERT_EXCLUSIVE_* 等约束断言。
定位:KCSAN 是 调试/测试工具,默认不启用;与 KASAN 互斥,不可用于生产内核。
目录
一、源码目录结构
1.1 编译依赖(Makefile)
1 2 3 4 5 6 7 8 9 10
| KCSAN_SANITIZE := n KCOV_INSTRUMENT := n UBSAN_SANITIZE := n
CFLAGS_core.o := -fno-conserve-stack -fno-stack-protector CFLAGS_REMOVE_core.o = $(CC_FLAGS_FTRACE)
obj-y := core.o debugfs.o report.o obj-$(CONFIG_KCSAN_SELFTEST) += selftest.o obj-$(CONFIG_KCSAN_KUNIT_TEST) += kcsan_test.o
|
| 配置项 |
说明 |
CONFIG_KCSAN |
启用 KCSAN(依赖 HAVE_ARCH_KCSAN + HAVE_KCSAN_COMPILER + DEBUG_KERNEL,且 !KASAN) |
CONFIG_KCSAN_NUM_WATCHPOINTS |
watchpoint 槽数量(默认 64) |
CONFIG_KCSAN_EARLY_ENABLE |
启动早期即全局启用 |
CONFIG_KCSAN_STRICT |
严格模式(对齐 LKMM 规则) |
CONFIG_KCSAN_WEAK_MEMORY |
弱内存模型/缺失 barrier 检测 |
CONFIG_KCSAN_SELFTEST |
启动自检 |
CONFIG_KCSAN_VERBOSE |
报告含锁/IRQ 跟踪信息 |
1.2 源文件
| 文件 |
行数 |
功能 |
core.c |
~1371 |
核心运行时:watchpoint、check_access、TSAN 钩子 |
report.c |
~715 |
竞争报告生成、速率限制、栈回溯 |
debugfs.c |
~275 |
debugfs 统计/开关/过滤/微基准 |
encoding.h |
~102 |
watchpoint 地址/大小/读写编码 |
kcsan.h |
~142 |
内部 API、计数器定义 |
permissive.h |
~94 |
宽松模式忽略规则 |
selftest.c |
~272 |
启动自检 |
kcsan_test.c |
~1618 |
KUnit 集成测试 |
相关头文件:
| 文件 |
功能 |
include/linux/kcsan.h |
公共 API、struct kcsan_ctx |
include/linux/kcsan-checks.h |
ASSERT_EXCLUSIVE_* 宏 |
lib/Kconfig.kcsan |
全部 Kconfig 选项 |
二、整体架构
2.1 设计思想
KCSAN 采用 采样 watchpoint 而非追踪每一访问(对比用户态 TSAN 的 shadow memory):
1 2 3 4 5
| 每次内存访问(编译器插桩) → check_access() 快速路径 ├── 查找是否有冲突 watchpoint → 报告竞争 └── 采样:概率性设置 watchpoint + 延迟 → 其他线程访问同一地址 → 检测到竞争
|
优势:内核开销可控,可在完整内核上运行;代价:可能漏检(采样未覆盖的访问)。
2.2 数据流
1 2 3 4 5 6 7 8 9 10
| 编译阶段 运行阶段 ───────── ───────── -fsanitize=thread 每次 load/store → __tsan_readN/writeN() → check_access() → find_watchpoint (fast) → should_watch → setup_watchpoint (slow) → insert_watchpoint → udelay (扩大竞争窗口) → 值变化检测 → consume_watchpoint / report
|
2.3 与相关工具对比
| 工具 |
机制 |
检测目标 |
| KCSAN |
TSAN 插桩 + watchpoint 采样 |
数据竞争、缺失 barrier |
| KASAN |
影子内存 |
越界、use-after-free |
| KCSAN |
— |
与 KASAN 互斥(depends on !KASAN) |
| KCOV |
覆盖率 |
代码路径(模糊测试) |
| LOCKDEP |
锁依赖图 |
死锁、锁顺序 |
三、Watchpoint 采样算法
3.1 采样控制
1 2 3 4 5 6 7 8 9 10 11 12 13
| static DEFINE_PER_CPU(long, kcsan_skip);
static __always_inline bool should_watch(struct kcsan_ctx *ctx, ...) { if (is_atomic(ctx, ptr, size, type)) return false;
if (this_cpu_dec_return(kcsan_skip) >= 0) return false;
return true; }
|
| 参数 |
默认值 |
说明 |
KCSAN_SKIP_WATCH |
4000 |
每 CPU 跳过多少次访问后才采样一次 |
KCSAN_UDELAY_TASK |
80 μs |
任务上下文 watchpoint 延迟 |
KCSAN_UDELAY_INTERRUPT |
20 μs |
中断上下文 watchpoint 延迟 |
KCSAN_DELAY_RANDOMIZE |
y |
随机化延迟 |
KCSAN_SKIP_WATCH_RANDOMIZE |
y |
随机化 skip 计数 |
3.2 check_access 双路径
1 2 3 4 5 6 7 8 9 10 11 12
| static __always_inline void check_access(const volatile void *ptr, size_t size, int type, unsigned long ip) { watchpoint = find_watchpoint(addr, size, expect_write, &encoded); if (watchpoint != NULL) kcsan_found_watchpoint(...); else if (should_watch(ctx, ptr, size, type)) kcsan_setup_watchpoint(...); else }
|
3.3 setup_watchpoint 流程
1 2 3 4 5 6 7 8 9 10 11
| kcsan_setup_watchpoint(ptr, size, type, ip) 1. check_encodable() — 地址/大小是否可编码 2. insert_watchpoint() — CAS 插入 atomic_long 槽 3. old = read_instrumented_memory(ptr) — 记录当前值 4. delay_access(type) — udelay 扩大竞争窗口 5. new = read_instrumented_memory(ptr) — 重读检查值变化 6. consume_watchpoint() — 是否被其他线程命中? → 是:kcsan_report_known_origin() — 双方栈回溯报告 → 否但值变化:kcsan_report_unknown_origin() — 未知来源报告 7. remove_watchpoint() 8. set_reorder_access() — 弱内存模型重排序检测
|
3.4 相邻槽检查
1 2
| #define KCSAN_CHECK_ADJACENT 1 #define NUM_SLOTS (1 + 2*KCSAN_CHECK_ADJACENT)
|
跨页边界的大访问可能检查相邻 watchpoint 槽,减少漏检;极大访问仍可能错过。
四、Watchpoint 编码(encoding.h)
每个 watchpoint 编码为单个 atomic_long,无锁 CAS 更新:
1 2 3 4 5 6 7
|
encode_watchpoint(addr, size, is_write) watchpoint_slot(addr) = (addr / PAGE_SIZE) % CONFIG_KCSAN_NUM_WATCHPOINTS
|
| 常量 |
值 |
说明 |
INVALID_WATCHPOINT |
0 |
空槽 |
CONSUMED_WATCHPOINT |
1 |
已被竞争方消费 |
MAX_ENCODABLE_SIZE |
PAGE_SIZE * 3 |
最大可编码访问大小 |
SLOT_RANGE |
PAGE_SIZE |
槽映射粒度 |
地址按 (addr / PAGE_SIZE) % NUM_WATCHPOINTS 哈希到槽,不同地址可能碰撞(report.c 会过滤编码假阳性)。
五、核心运行时(core.c)
5.1 全局状态
1 2 3 4 5
| bool kcsan_enabled; static atomic_long_t watchpoints[NUM_WATCHPOINTS + 2]; atomic_long_t kcsan_counters[KCSAN_COUNTER_COUNT]; static DEFINE_PER_CPU(struct kcsan_ctx, kcsan_cpu_ctx);
|
5.2 统计计数器
| 计数器 |
说明 |
used_watchpoints |
当前使用中的 watchpoint |
setup_watchpoints |
累计设置次数 |
data_races |
检测到的数据竞争 |
assert_failures |
ASSERT 失败 |
no_capacity |
watchpoint 槽耗尽 |
report_races |
竞争报告竞争(一方已消费) |
races_unknown_origin |
未知来源竞争 |
unencodable_accesses |
无法编码的访问 |
encoding_false_positives |
编码碰撞假阳性 |
5.3 初始化
1 2 3 4 5 6 7 8 9 10
| void __init kcsan_init(void) { for_each_possible_cpu(cpu) per_cpu(kcsan_rand_state, cpu) = get_cycles();
if (kcsan_early_enable) WRITE_ONCE(kcsan_enabled, true);
}
|
5.4 自身保护
KCSAN 核心代码 禁止被插桩(KCSAN_SANITIZE := n),且禁用 ftrace/KCOV,避免递归和性能灾难。
六、编译器插桩接口
6.1 TSAN 钩子
KCSAN 复用编译器为 ThreadSanitizer 生成的插桩回调:
1 2 3 4 5 6 7 8 9 10
| CFLAGS_KCSAN := -fsanitize=thread -mllvm -tsan-distinguish-volatile=1
__tsan_read1/2/4/8/16(void *ptr) __tsan_write1/2/4/8/16(void *ptr) __tsan_read_writeN(void *ptr) __tsan_volatile_read/writeN() __tsan_read/write_range()
|
6.2 实现示例
1 2 3 4 5 6 7 8 9
| void __tsan_write4(void *ptr) { check_access(ptr, 4, KCSAN_ACCESS_WRITE, _RET_IP_); }
void __tsan_read4(void *ptr) { check_access(ptr, 4, 0, _RET_IP_); }
|
6.3 不插桩的情况
- 内联汇编(
asm)访问
- 标记为
__no_kcsan 的函数
- KCSAN 自身代码(Makefile 排除)
- 原子区域内的访问(
READ_ONCE/WRITE_ONCE 在 atomic 区域等)
七、上下文与原子区域
7.1 struct kcsan_ctx
1 2 3 4 5 6 7 8 9 10
| struct kcsan_ctx { int disable_count; int disable_scoped; int atomic_next; int atomic_nest_count; bool in_flat_atomic; unsigned long access_mask; struct list_head scoped_accesses; struct kcsan_scoped_access reorder_access; };
|
- 任务上下文:
current->kcsan_ctx
- 中断上下文:
per_cpu(kcsan_cpu_ctx)
7.2 原子区域 API
| API |
功能 |
kcsan_disable_current() / enable_current() |
临时禁用当前上下文 KCSAN |
kcsan_nestable_atomic_begin/end() |
嵌套原子区域(如 spin_lock 内) |
kcsan_flat_atomic_begin/end() |
非嵌套 flat 原子区域(如 seqlock 读) |
kcsan_atomic_next(n) |
标记后续 n 个操作为 atomic |
kcsan_set_access_mask(mask) |
位级访问掩码 |
7.3 is_atomic 判定
以下访问 不设置 watchpoint(视为已同步):
KCSAN_ACCESS_ATOMIC 标记的访问
atomic_nest_count > 0(nestable atomic 区域内)
in_flat_atomic == true(flat atomic 区域内)
atomic_next > 0(显式标记的后续 atomic 操作)
CONFIG_KCSAN_ASSUME_PLAIN_WRITES_ATOMIC:对齐 ≤ word 的 plain write
八、弱内存模型检测
8.1 重排序访问(CONFIG_KCSAN_WEAK_MEMORY)
模拟编译器/CPU 对内存访问的重排序,检测缺失 memory barrier:
1 2 3 4 5 6
| 普通 write 访问 → set_reorder_access() — 记录为"可能被重排序的访问" → 后续 barrier (__kcsan_mb/wmb/rmb/release) 清除 reorder → 若无 barrier,在函数退出或 scoped check 时 以 KCSAN_ACCESS_SCOPED 类型重新 check_access → 检测与其他线程的冲突
|
8.2 Barrier 钩子
1 2 3 4
| void __kcsan_mb(void) { reorder_access->size = 0; } void __kcsan_wmb(void) { 写屏障 } void __kcsan_rmb(void) { 读屏障 } void __kcsan_release(void) { release 语义 }
|
8.3 函数边界
1 2
| __tsan_func_entry() — 函数入口,初始化 reorder 作用域 __tsan_func_exit() — 函数退出,invalidate reorder_access
|
九、报告生成(report.c)
9.1 报告类型
| 类型 |
触发条件 |
| known origin |
watchpoint 被消费,双方栈回溯完整 |
| unknown origin |
延迟期间值变化但无 watchpoint 命中(如设备 DMA) |
| assert: race |
ASSERT_EXCLUSIVE_* 检测到冲突 |
9.2 报告格式示例
1 2 3 4 5 6 7 8 9 10 11 12
| BUG: KCSAN: data-race in foo_read / bar_write
write to 0xffffffffc0123456 of 4 bytes by task 1234 on cpu 2: foo_write+0x42/0x100 [module] ...
read to 0xffffffffc0123456 of 4 bytes by task 5678 on cpu 5: bar_read+0x18/0x80 [module] ...
Reported by Kernel Concurrency Sanitizer on: CPU: 2 PID: 1234 Comm: test Tainted: G ...
|
9.3 速率限制
1 2
| static bool rate_limit_report(unsigned long frame1, unsigned long frame2)
|
9.4 双线程协调
1 2 3 4 5
| 竞争方 A(命中 watchpoint): try_consume_watchpoint() → kcsan_report_set_info() — 写入 other_info
竞争方 B(watchpoint 设置者): consume_watchpoint 失败 → kcsan_report_known_origin() — 读取 other_info 打印完整报告
|
使用 report_lock 串行化报告,other_infos[] 数组大小与 watchpoint 数一致。
十、Debugfs 接口(debugfs.c)
路径:/sys/kernel/debug/kcsan/
| 文件 |
功能 |
enabled |
读写:全局启用/禁用 KCSAN |
stats |
各计数器统计 |
report_filterlist |
函数黑名单/白名单(按符号地址过滤报告) |
microbench |
写入迭代次数 → 微基准测试 fast-path |
10.1 模块参数
1 2 3 4 5 6
| kcsan.early_enable=1 kcsan.udelay_task=80 kcsan.udelay_interrupt=20 kcsan.skip_watch=4000 kcsan.interrupt_watcher=0 kcsan.weak_memory=1
|
十一、显式检查 API
定义于 include/linux/kcsan-checks.h:
11.1 数据竞争检查
1 2 3 4 5 6
| kcsan_check_read(ptr, size); kcsan_check_write(ptr, size);
kcsan_check_atomic_read/write/read_write(ptr, size);
|
11.2 排他性断言
1 2 3 4
| ASSERT_EXCLUSIVE_WRITER(var); ASSERT_EXCLUSIVE_ACCESS(var); ASSERT_EXCLUSIVE_WRITER_SCOPED(var); ASSERT_EXCLUSIVE_BITS(var, mask);
|
失败时报告 assert: race,计入 assert_failures 计数器。
11.3 Scoped Access
1 2 3 4
| struct kcsan_scoped_access sa; kcsan_begin_scoped_access(ptr, size, type, &sa);
kcsan_end_scoped_access(&sa);
|
十二、RK3588/ARM64 平台说明
12.1 架构支持
1 2
| # arch/arm64/Kconfig select HAVE_ARCH_KCSAN if EXPERT
|
- 需在
EXPERT 模式下才可选 KCSAN
- 编译器需支持
-fsanitize=thread(GCC ≥7 或 Clang ≥9)
- 与 KASAN 互斥,调试时二选一
12.2 启用方法
1 2 3 4 5 6
| make menuconfig: Kernel hacking → KCSAN: dynamic data race detector → Y (需要 DEBUG_KERNEL=y, 不能同时启用 KASAN)
# 内核命令行(可选) kcsan.early_enable=1
|
12.3 ARM64 注意事项
| 方面 |
说明 |
| 原子操作 |
ARM64 LL/SC 保证对齐 native atomic;plain write 在 strict 模式下仍可能被报告 |
| big.LITTLE |
watchpoint 全局共享,与运行在 A76/A55 无关 |
| 性能 |
采样模式开销远低于用户态 TSAN;仍显著增加内核体积和运行时间 |
| outline atomics |
Makefile 使用 -mno-outline-atomics 避免 core.c 中额外原子调用 |
| 生产环境 |
不应启用 — 仅 CI/开发内核使用 |
12.4 宽松模式(非 strict 默认)
RK3588 开发内核若未设 CONFIG_KCSAN_STRICT=y,默认启用多项宽松规则:
KCSAN_REPORT_VALUE_CHANGE_ONLY — 仅报告观察到值变化的竞争
KCSAN_ASSUME_PLAIN_WRITES_ATOMIC — 假设对齐 word write 原子
KCSAN_IGNORE_ATOMICS — 不插桩 marked atomic
KCSAN_PERMISSIVE — 忽略单 bit 变化等常见模式
启动时会打印:non-strict mode configured - use CONFIG_KCSAN_STRICT=y to see all data races
十三、完整检测时序
以 两个线程无锁并发写同一变量 为例:
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
| [编译期] int shared; // 未用 lock/atomic 保护 shared = 1; → 编译器插入 __tsan_write4(&shared)
[Thread A — CPU 2] __tsan_write4(&shared) → check_access(&shared, 4, WRITE, ip_A) → find_watchpoint: 无 → should_watch: true (skip 计数归零) → kcsan_setup_watchpoint() → insert_watchpoint(slot=shared的hash) → old = 1 → udelay(80μs) ← 竞争窗口 → new = 1 (尚未被 B 修改) → consume_watchpoint: 未被命中 → remove_watchpoint
[Thread B — CPU 5](在 A 的 udelay 期间) __tsan_write4(&shared) → check_access(&shared, 4, WRITE, ip_B) → find_watchpoint: 命中 A 的 watchpoint! → kcsan_found_watchpoint() → try_consume_watchpoint() = true → kcsan_report_set_info() ← B 记录栈信息
[Thread A — udelay 结束] → consume_watchpoint() = false (已被 B 消费) → kcsan_report_known_origin() → 打印完整报告:A(write) vs B(write) + 双方栈回溯 → data_races counter++
|
十四、总结
kernel/kcsan 是 Linux 内核 动态数据竞争检测 的完整实现:
- 采样 watchpoint — 非 shadow memory,开销可控,适合完整内核
- TSAN 插桩 — 复用编译器
-fsanitize=thread 在每个 load/store 插入回调
- 无锁 CAS watchpoint — 每个槽一个
atomic_long,编码地址/大小/读写
- 延迟放大 — udelay 扩大竞争观测窗口
- 值变化检测 — 区分真实竞争与 false sharing
- 原子区域 — nestable/flat atomic 标记已同步访问
- 弱内存模型 — reorder_access + barrier 钩子检测缺失 memory barrier
- ASSERT API —
ASSERT_EXCLUSIVE_* 约束检查
- Debugfs — 运行时开关、统计、过滤
RK3588 在 EXPERT 配置下可启用 KCSAN 用于驱动/内核并发 bug 调试,但应与 KASAN 分开构建,且不用于产品内核。
附录:源文件清单
| 文件 |
行数 |
分类 |
core.c |
~1371 |
核心运行时 |
report.c |
~715 |
报告生成 |
debugfs.c |
~275 |
debugfs 接口 |
kcsan_test.c |
~1618 |
KUnit 测试 |
selftest.c |
~272 |
启动自检 |
encoding.h |
~102 |
watchpoint 编码 |
kcsan.h |
~142 |
内部头文件 |
permissive.h |
~94 |
宽松规则 |
配置与文档:
| 文件 |
功能 |
lib/Kconfig.kcsan |
全部 Kconfig 选项 |
include/linux/kcsan-checks.h |
公共检查宏 |
Documentation/dev-tools/kcsan.rst |
官方文档 |
正在加载留言…