kernel/kcsan 内核并发消毒器(KCSAN)机制与原理详解

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          # KCSAN 自身不被插桩
KCOV_INSTRUMENT := n
UBSAN_SANITIZE := n

CFLAGS_core.o := -fno-conserve-stack -fno-stack-protector
CFLAGS_REMOVE_core.o = $(CC_FLAGS_FTRACE) # 禁用 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
// 每 CPU 跳过计数器:大部分访问直接返回
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) // 跳过 N 次访问
return false;

return true; // 本次访问设置 watchpoint
}
参数 默认值 说明
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
watchpoint = find_watchpoint(addr, size, expect_write, &encoded);
if (watchpoint != NULL)
kcsan_found_watchpoint(...); // 慢路径:消费 watchpoint,记录竞争方
else if (should_watch(ctx, ptr, size, type))
kcsan_setup_watchpoint(...); // 慢路径:插入 watchpoint + 延迟
else
// 检查 reorder_access(弱内存)和 scoped_access
}

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) // = 3

跨页边界的大访问可能检查相邻 watchpoint 槽,减少漏检;极大访问仍可能错过。


四、Watchpoint 编码(encoding.h)

每个 watchpoint 编码为单个 atomic_long,无锁 CAS 更新:

1
2
3
4
5
6
7
// 编码格式(64-bit):
// [63] = is_write
// [62:ADDR_BITS] = size
// [ADDR_BITS-1:0] = addr 低位(masked)

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]; // watchpoint 数组
atomic_long_t kcsan_counters[KCSAN_COUNTER_COUNT]; // 统计计数器
static DEFINE_PER_CPU(struct kcsan_ctx, kcsan_cpu_ctx); // 中断上下文
// task 使用 task_struct.kcsan_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);

// 非 strict 模式警告
}

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 // Clang
// 或 GCC: --param tsan-distinguish-volatile=1

// 编译器在每个 load/store 插入:
__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() // volatile 访问
__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_); // 0 = read
}

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; // kcsan_disable_current() 嵌套计数
int disable_scoped;
int atomic_next; // 后续 N 个操作视为 atomic
int atomic_nest_count; // nestable atomic 区域深度
bool in_flat_atomic; // flat atomic 区域(如 seqlock 读侧)
unsigned long access_mask; // 位级 ASSERT_EXCLUSIVE_BITS
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(视为已同步):

  1. KCSAN_ACCESS_ATOMIC 标记的访问
  2. atomic_nest_count > 0(nestable atomic 区域内)
  3. in_flat_atomic == true(flat atomic 区域内)
  4. atomic_next > 0(显式标记的后续 atomic 操作)
  5. 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
// 同一对栈帧在 KCSAN_REPORT_ONCE_IN_MS (默认 3000ms) 内只报告一次
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);          // 断言无并发 write
ASSERT_EXCLUSIVE_ACCESS(var); // 断言无并发 read/write
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);
// ... critical section ...
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 内核 动态数据竞争检测 的完整实现:

  1. 采样 watchpoint — 非 shadow memory,开销可控,适合完整内核
  2. TSAN 插桩 — 复用编译器 -fsanitize=thread 在每个 load/store 插入回调
  3. 无锁 CAS watchpoint — 每个槽一个 atomic_long,编码地址/大小/读写
  4. 延迟放大 — udelay 扩大竞争观测窗口
  5. 值变化检测 — 区分真实竞争与 false sharing
  6. 原子区域 — nestable/flat atomic 标记已同步访问
  7. 弱内存模型 — reorder_access + barrier 钩子检测缺失 memory barrier
  8. ASSERT APIASSERT_EXCLUSIVE_* 约束检查
  9. 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 官方文档

文章互动

阅读 --

留言

0 条留言

正在加载留言…