英语

内核并发清理器 (KCSAN)

内核并发清理器 (KCSAN) 是一种动态竞争检测器,它依赖于编译时插桩,并使用基于观察点的采样方法来检测竞争。KCSAN 的主要目的是检测数据竞争

用法

GCC 和 Clang 都支持 KCSAN。对于 GCC,我们需要 11 或更高版本,对于 Clang 也需要 11 或更高版本。

要启用 KCSAN,请使用以下配置内核

CONFIG_KCSAN = y

KCSAN 提供了几个其他配置选项来自定义行为(有关更多信息,请参阅lib/Kconfig.kcsan中的相应帮助文本)。

错误报告

典型的数据竞争报告如下所示

==================================================================
BUG: KCSAN: data-race in test_kernel_read / test_kernel_write

write to 0xffffffffc009a628 of 8 bytes by task 487 on cpu 0:
 test_kernel_write+0x1d/0x30
 access_thread+0x89/0xd0
 kthread+0x23e/0x260
 ret_from_fork+0x22/0x30

read to 0xffffffffc009a628 of 8 bytes by task 488 on cpu 6:
 test_kernel_read+0x10/0x20
 access_thread+0x89/0xd0
 kthread+0x23e/0x260
 ret_from_fork+0x22/0x30

value changed: 0x00000000000009a6 -> 0x00000000000009b2

Reported by Kernel Concurrency Sanitizer on:
CPU: 6 PID: 488 Comm: access_thread Not tainted 5.12.0-rc2+ #1
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
==================================================================

报告的头部提供了涉及竞争的函数的简短摘要。接下来是涉及数据竞争的 2 个线程的访问类型和堆栈跟踪。如果 KCSAN 还观察到值的更改,则分别在“value changed”行上显示观察到的旧值和新值。

另一种不太常见的数据竞争报告如下所示

==================================================================
BUG: KCSAN: data-race in test_kernel_rmw_array+0x71/0xd0

race at unknown origin, with read to 0xffffffffc009bdb0 of 8 bytes by task 515 on cpu 2:
 test_kernel_rmw_array+0x71/0xd0
 access_thread+0x89/0xd0
 kthread+0x23e/0x260
 ret_from_fork+0x22/0x30

value changed: 0x0000000000002328 -> 0x0000000000002329

Reported by Kernel Concurrency Sanitizer on:
CPU: 2 PID: 515 Comm: access_thread Not tainted 5.12.0-rc2+ #1
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
==================================================================

此报告是在无法确定其他竞争线程的情况下生成的,但由于被监视的内存位置的数据值已更改而推断出存在竞争。这些报告始终显示“value changed”行。此类报告的常见原因是竞争线程中缺少插桩,但也可能由于例如 DMA 访问而发生。只有在启用CONFIG_KCSAN_REPORT_RACE_UNKNOWN_ORIGIN=y时才会显示此类报告,默认情况下启用。

选择性分析

可能希望为特定的访问、函数、编译单元或整个子系统禁用数据竞争检测。对于静态黑名单,可以使用以下选项

  • KCSAN 理解data_race(expr)注释,它告诉 KCSAN 由于expr中的访问而导致的任何数据竞争都应被忽略,并且当遇到数据竞争时产生的结果行为被认为是安全的。有关更多信息,请参见LKMM 中的“标记共享内存访问”

  • data_race(...)类似,类型限定符__data_racy可用于记录由于访问变量而导致的所有数据竞争都是有意的,并且应被 KCSAN 忽略

    struct foo {
        ...
        int __data_racy stats_counter;
        ...
    };
    
  • 可以使用函数属性__no_kcsan来禁用对整个函数的数据竞争检测

    __no_kcsan
    void foo(void) {
        ...
    

    要动态限制生成报告的函数,请参阅DebugFS 接口黑名单/白名单功能。

  • 要禁用特定编译单元的数据竞争检测,请添加到Makefile

    KCSAN_SANITIZE_file.o := n
    
  • 要禁用Makefile中列出的所有编译单元的数据竞争检测,请添加到相应的Makefile

    KCSAN_SANITIZE := n
    

此外,可以告诉 KCSAN 根据偏好显示或隐藏整个类别的数据竞争。可以通过以下 Kconfig 选项更改这些设置

  • CONFIG_KCSAN_REPORT_VALUE_CHANGE_ONLY:如果启用,并且通过观察点观察到冲突的写入,但观察到内存位置的数据值保持不变,则不报告数据竞争。

  • CONFIG_KCSAN_ASSUME_PLAIN_WRITES_ATOMIC:默认情况下,假定最大字长的纯对齐写入是原子的。假定此类写入不受导致数据竞争的不安全编译器优化影响。该选项导致 KCSAN 不报告由于冲突而导致的数据竞争,其中唯一的纯访问是对齐的写入,最大为字长。

  • CONFIG_KCSAN_PERMISSIVE:启用额外的宽松规则,以忽略某些常见的数据竞争类别。与上述不同,这些规则更复杂,涉及值更改模式、访问类型和地址。此选项取决于CONFIG_KCSAN_REPORT_VALUE_CHANGE_ONLY=y。有关详细信息,请参见kernel/kcsan/permissive.h。建议仅关注来自特定子系统而不是整个内核的报告的测试人员和维护人员禁用此选项。

要使用尽可能严格的规则,请选择CONFIG_KCSAN_STRICT=y,这将 KCSAN 配置为尽可能密切地遵循 Linux 内核内存一致性模型 (LKMM)。

DebugFS 接口

文件/sys/kernel/debug/kcsan提供了以下接口

  • 读取/sys/kernel/debug/kcsan返回各种运行时统计信息。

  • onoff写入/sys/kernel/debug/kcsan允许分别打开或关闭 KCSAN。

  • !some_func_name写入/sys/kernel/debug/kcsan会将some_func_name添加到报告过滤器列表中,该列表(默认情况下)会将报告数据竞争列入黑名单,其中顶部堆栈帧之一是列表中的函数。

  • blacklistwhitelist写入/sys/kernel/debug/kcsan会更改报告过滤行为。例如,黑名单功能可用于消除频繁发生的数据竞争;白名单功能可以帮助重现和测试修复。

调整性能

影响 KCSAN 整体性能和错误检测能力的核心参数作为内核命令行参数公开,其默认值也可以通过相应的 Kconfig 选项更改。

  • kcsan.skip_watch (CONFIG_KCSAN_SKIP_WATCH):在设置另一个观察点之前,要跳过的每个 CPU 内存操作的数量。更频繁地设置观察点将导致观察到的竞争的可能性增加。此参数对整体系统性能和竞争检测能力具有最显着的影响。

  • kcsan.udelay_task (CONFIG_KCSAN_UDELAY_TASK):对于任务,设置观察点后要延迟执行的微秒数。较大的值会导致我们可能观察到竞争的窗口增加。

  • kcsan.udelay_interrupt (CONFIG_KCSAN_UDELAY_INTERRUPT):对于中断,设置观察点后要延迟执行的微秒数。中断具有更严格的延迟要求,它们的延迟通常应小于为任务选择的延迟。

它们可以在运行时通过/sys/module/kcsan/parameters/进行调整。

数据竞争

在执行中,如果两个内存访问冲突、它们在不同的线程中同时发生,并且至少其中一个是纯访问,则这两个内存访问形成数据竞争;如果两者都访问相同的内存位置,并且至少一个是写入,则它们冲突。有关更深入的讨论和定义,请参见LKMM 中的“纯访问和数据竞争”

与 Linux 内核内存一致性模型 (LKMM) 的关系

LKMM 定义了各种内存操作的传播和排序规则,这使开发人员能够推理并发代码。最终,这允许确定并发代码的可能执行,以及该代码是否没有数据竞争。

KCSAN 知道标记的原子操作 (READ_ONCEWRITE_ONCEatomic_*等)以及内存屏障所暗示的排序保证的子集。使用CONFIG_KCSAN_WEAK_MEMORY=y,KCSAN 模拟加载或存储缓冲,并且可以检测到缺少的smp_mb()smp_wmb()smp_rmb()smp_store_release()以及所有具有等效隐含屏障的atomic_*操作。

请注意,KCSAN 不会报告由于缺少内存排序而导致的所有数据竞争,特别是当需要内存屏障来禁止后续内存操作在屏障之前重新排序时。因此,开发人员应仔细考虑保持未检查的所需内存排序要求。

超出数据竞争的竞争检测

对于具有复杂并发设计的代码,竞争条件错误可能并不总是表现为数据竞争。如果并发执行的操作导致意外的系统行为,则会发生竞争条件。另一方面,数据竞争是在 C 语言级别定义的。以下宏可用于检查并发代码的属性,其中错误不会表现为数据竞争。

ASSERT_EXCLUSIVE_WRITER

ASSERT_EXCLUSIVE_WRITER (var)

断言没有并发写入var

参数

var

要断言的变量

描述

断言没有并发写入var;允许其他读取器。此断言可用于指定并发代码的属性,其中违规行为无法检测为正常的数据竞争。

例如,如果我们只有一个写入器,但有多个并发读取器,为了避免数据竞争,所有这些访问都必须标记;即使并发标记的写入与单个写入器竞争也是错误。不幸的是,由于被标记,它们不再是数据竞争。对于这种情况,我们可以使用以下宏

void writer(void) {
        spin_lock(&update_foo_lock);
        ASSERT_EXCLUSIVE_WRITER(shared_foo);
        WRITE_ONCE(shared_foo, ...);
        spin_unlock(&update_foo_lock);
}
void reader(void) {
        // update_foo_lock does not need to be held!
        ... = READ_ONCE(shared_foo);
}

注意

如果适用,ASSERT_EXCLUSIVE_WRITER_SCOPED()执行更彻底的检查,如果存在预期的没有并发写入的明确范围。

ASSERT_EXCLUSIVE_WRITER_SCOPED

ASSERT_EXCLUSIVE_WRITER_SCOPED (var)

断言在范围内没有并发写入var

参数

var

要断言的变量

描述

ASSERT_EXCLUSIVE_WRITER()的范围变体。

断言在引入它的范围的持续时间内没有并发写入var。与多个ASSERT_EXCLUSIVE_WRITER()相比,这提供了一种更好的方式来完全覆盖封闭范围,并增加了 KCSAN 检测竞争访问的可能性。

例如,它允许查找仅由于范围本身内的状态更改而发生的竞争条件错误

void writer(void) {
        spin_lock(&update_foo_lock);
        {
                ASSERT_EXCLUSIVE_WRITER_SCOPED(shared_foo);
                WRITE_ONCE(shared_foo, 42);
                ...
                // shared_foo should still be 42 here!
        }
        spin_unlock(&update_foo_lock);
}
void buggy(void) {
        if (READ_ONCE(shared_foo) == 42)
                WRITE_ONCE(shared_foo, 1); // bug!
}
ASSERT_EXCLUSIVE_ACCESS

ASSERT_EXCLUSIVE_ACCESS (var)

断言没有并发访问var

参数

var

要断言的变量

描述

断言没有并发访问var(没有读取器或写入器)。此断言可用于指定并发代码的属性,其中违规行为无法检测为正常的数据竞争。

例如,在确定没有对象的其他用户后,期望独占访问,但实际上并没有释放该对象。我们可以检查此属性是否实际保持如下

if (refcount_dec_and_test(&obj->refcnt)) {
        ASSERT_EXCLUSIVE_ACCESS(*obj);
        do_some_cleanup(obj);
        release_for_reuse(obj);
}
  1. 如果适用,ASSERT_EXCLUSIVE_ACCESS_SCOPED()执行更彻底的检查,如果存在预期的没有并发访问的明确范围。

  2. 对于释放对象的情况,KASAN更适合检测释放后使用错误。

注意

ASSERT_EXCLUSIVE_ACCESS_SCOPED

ASSERT_EXCLUSIVE_ACCESS_SCOPED (var)

断言在范围内没有并发访问var

参数

var

要断言的变量

描述

ASSERT_EXCLUSIVE_ACCESS()的范围变体。

断言在引入它的范围的整个持续时间内没有并发访问var(没有读取器或写入器)。与多个ASSERT_EXCLUSIVE_ACCESS()相比,这提供了一种更好的方式来完全覆盖封闭范围,并增加了 KCSAN 检测竞争访问的可能性。

ASSERT_EXCLUSIVE_BITS

ASSERT_EXCLUSIVE_BITS (var, mask)

断言没有并发写入var中位元的子集

参数

var

要断言的变量

mask

仅检查对mask中设置的位元的修改

描述

ASSERT_EXCLUSIVE_WRITER()的位元粒度变体。

断言没有并发写入var中位元的子集;允许并发读取器。与其他(字粒度)断言相比,此断言捕获了更详细的位级别属性。仅检查在mask中设置的位元的并发修改,同时忽略剩余的位元,即忽略对 ~mask 位元的并发写入(或读取)。

将此用于变量,其中某些位元不得并发修改,但期望其他位元并发修改。

例如,变量在初始化后,某些位元是只读的,但其他位元仍可能并发修改。读取器可能希望断言这是真实的,如下所示

ASSERT_EXCLUSIVE_BITS(flags, READ_ONLY_MASK);
foo = (READ_ONCE(flags) & READ_ONLY_MASK) >> READ_ONLY_SHIFT;
ASSERT_EXCLUSIVE_BITS(flags, READ_ONLY_MASK);
foo = (flags & READ_ONLY_MASK) >> READ_ONLY_SHIFT;

另一个可以使用它的示例是,仅当持有适当的锁时,才可以修改var的某些位元,但其他位元仍可以并发修改。写入器,其中其他位元可能会并发更改,可以使用如下断言

spin_lock(&foo_lock);
ASSERT_EXCLUSIVE_BITS(flags, FOO_MASK);
old_flags = flags;
new_flags = (old_flags & ~FOO_MASK) | (new_foo << FOO_SHIFT);
if (cmpxchg(&flags, old_flags, new_flags) != old_flags) { ... }
spin_unlock(&foo_lock);

注意

假定紧随ASSERT_EXCLUSIVE_BITS()之后的访问仅访问屏蔽的位元,并且 KCSAN 乐观地假定它是安全的,即使存在数据竞争,并且从 KCSAN 的角度来看,使用 READ_ONCE() 标记它是可选的。但是,我们告诫您,这样做仍然是明智的,因为我们无法在位操作方面推断所有编译器优化(在读取器和写入器端)。如果您确定不会出错,我们可以简单地将上述内容写成

实现细节

KCSAN 依赖于观察到两次访问同时发生。至关重要的是,我们希望 (a) 增加观察到竞争的机会(特别是对于很少表现出来的竞争),以及 (b) 能够实际观察到它们。我们可以通过注入各种延迟来实现 (a),并通过使用地址观察点(或断点)来实现 (b)。

如果我们故意停止内存访问,同时为它的地址设置了观察点,然后观察到观察点触发,则对同一地址的两次访问刚刚竞争。使用硬件观察点,这是DataCollider中采用的方法。与 DataCollider 不同,KCSAN 不使用硬件观察点,而是依赖于编译器插桩和“软观察点”。

在 KCSAN 中,观察点使用一种有效的编码来实现,该编码将访问类型、大小和地址存储在一个长整型中;使用“软观察点”的好处是可移植性和更大的灵活性。然后,KCSAN 依赖于编译器对纯访问进行插桩。对于每个插桩的纯访问

  1. 检查是否存在匹配的观察点;如果存在,并且至少一次访问是写入,则我们遇到了竞争访问。

  2. 定期地,如果没有匹配的观察点存在,则设置一个观察点并暂停一小段随机延迟。

  3. 还要在延迟之前检查数据值,并在延迟之后重新检查数据值;如果值不匹配,则我们推断出未知来源的竞争。

为了检测纯访问和标记访问之间的数据竞争,KCSAN 还会注释标记的访问,但仅用于检查是否存在观察点;即,KCSAN 永远不会在标记的访问上设置观察点。通过从不在标记的操作上设置观察点,如果对并发访问的变量的所有访问都已正确标记,则 KCSAN 永远不会触发观察点,因此永远不会报告访问。

模拟弱内存

KCSAN 检测由于缺少内存屏障而导致的数据竞争的方法是基于模拟访问重新排序(使用CONFIG_KCSAN_WEAK_MEMORY=y)。还选择为其设置观察点的每个纯内存访问,以便在其函数范围内模拟重新排序(最多 1 次进行中的访问)。

一旦选择了访问进行重新排序,就会针对直到函数范围结束的每个其他访问检查该访问。如果遇到适当的内存屏障,则将不再考虑该访问进行模拟重新排序。

当内存操作的结果应通过屏障排序时,KCSAN 可以检测到仅由于缺少屏障而发生的冲突的数据竞争。考虑以下示例

int x, flag;
void T1(void)
{
    x = 1;                  // data race!
    WRITE_ONCE(flag, 1);    // correct: smp_store_release(&flag, 1)
}
void T2(void)
{
    while (!READ_ONCE(flag));   // correct: smp_load_acquire(&flag)
    ... = x;                    // data race!
}

当启用弱内存建模时,KCSAN 可以考虑T1中的x进行模拟重新排序。在写入flag之后,再次检查x是否存在并发访问:因为T2能够在写入flag之后继续进行,因此检测到数据竞争。在适当的位置使用正确的屏障后,在正确释放flag之后,将不再考虑x进行重新排序,并且不会检测到数据竞争。

复杂性的有意权衡以及实际限制意味着只能检测到由于缺少内存屏障而导致的数据竞争的子集。使用当前可用的编译器支持,该实现仅限于模拟“缓冲”(延迟访问)的影响,因为运行时无法“预取”访问。还请记住,仅为纯访问设置观察点,并且 KCSAN 仅模拟重新排序的访问类型。这意味着不会模拟标记访问的重新排序。

上述情况的一个结果是,获取操作不需要屏障插桩(没有预取)。此外,引入地址或控制依赖关系的标记访问不需要特殊处理(不能重新排序标记的访问,不能预取稍后依赖的访问)。

关键属性

  1. 内存开销: 整体内存开销只有几个 MiB,具体取决于配置。当前实现使用一个小的长整型数组来编码观察点信息,这可以忽略不计。

  2. 性能开销: KCSAN 的运行时旨在最小化,使用一种有效的观察点编码,该编码不需要在快速路径中获取任何共享锁。对于在具有 8 个 CPU 的系统上的内核引导

    • 使用默认 KCSAN 配置时,速度降低 5.0 倍;

    • 仅来自运行时快速路径开销的速度降低 2.8 倍(设置非常大的KCSAN_SKIP_WATCH并取消设置KCSAN_SKIP_WATCH_RANDOMIZE)。

  3. 注释开销: 除了 KCSAN 运行时之外,需要最少的注释。因此,随着内核的发展,维护开销最小。

  4. 检测来自设备的竞争写入: 由于在设置观察点时检查数据值,因此也可以检测到来自设备的竞争写入。

  5. 内存排序: KCSAN 仅知道 LKMM 排序规则的子集;这可能会导致错过的数据竞争(假阴性)。

  6. 分析准确性: 对于观察到的执行,由于使用采样策略,分析是不健全的(可能存在假阴性),但旨在是完整的(没有假阳性)。

备选方案考虑

可以在内核线程清理器 (KTSAN)中找到内核的另一种数据竞争检测方法。KTSAN 是一种发生在之前的数据竞争检测器,它显式地建立了内存操作之间发生在之前的顺序,然后可以将其用于确定数据竞争中定义的数据竞争。

为了构建正确的发生在之前的关系,KTSAN 必须知道 LKMM 和同步原语的所有排序规则。不幸的是,任何遗漏都会导致大量假阳性,这在包括许多自定义同步机制的内核环境中尤其有害。为了跟踪发生在之前的关系,KTSAN 的实现需要每个内存位置的元数据(影子内存),对于每个页面,这对应于 4 个页面的影子内存,并且可以在大型系统上转换为数十 GiB 的开销。