为什么 Linux 内核中的 KCOV 代码中有 barrier()?

Why is there barrier() in KCOV code in Linux kernel?

在Linux KCOV代码中,为什么要放置这个barrier()

void notrace __sanitizer_cov_trace_pc(void)
{
    struct task_struct *t;
    enum kcov_mode mode;

    t = current;
    /*
     * We are interested in code coverage as a function of a syscall inputs,
     * so we ignore code executed in interrupts.
     */
    if (!t || in_interrupt())
        return;
    mode = READ_ONCE(t->kcov_mode);
    if (mode == KCOV_MODE_TRACE) {
        unsigned long *area;
        unsigned long pos;

        /*
         * There is some code that runs in interrupts but for which
         * in_interrupt() returns false (e.g. preempt_schedule_irq()).
         * READ_ONCE()/barrier() effectively provides load-acquire wrt
         * interrupts, there are paired barrier()/WRITE_ONCE() in
         * kcov_ioctl_locked().
         */
        barrier();
        area = t->kcov_area;
        /* The first word is number of subsequent PCs. */
        pos = READ_ONCE(area[0]) + 1;
        if (likely(pos < t->kcov_size)) {
            area[pos] = _RET_IP_;
            WRITE_ONCE(area[0], pos);
        }
    }
}

barrier() 调用阻止编译器重新排序指令。但是,这与这里的中断有什么关系?为什么语义正确性需要它?

如果没有 barrier(),编译器可以在 t->kcov_mode 之前自由访问 t->kcov_area。在实践中不太可能想要这样做,但这不是重点。没有某种障碍,C 规则允许编译器创建不执行我们想要的操作的 asm。 (C11 内存模型没有超出您明确施加的顺序保证;在 C11 中通过 stdatomic 或在 Linux / GNU C 中通过 barrier()smp_rb() 等障碍。)


如评论中所述,barrier() 正在创建一个 acquire-load wrt。代码 运行ning 在同一个内核上, 这就是你需要的所有中断。

    mode = READ_ONCE(t->kcov_mode);
    if (mode == KCOV_MODE_TRACE) {
        ...
        barrier();
        area = t->kcov_area;
        ...

我一般不熟悉 kcov,但看起来在 t->kcov_mode 中看到带有获取负载的特定值可以安全地读取 t->kcov_area。 (因为无论代码写入哪个对象,都会先写入 kcov_area,然后再对 kcov_mode 进行发布存储。)

https://preshing.com/20120913/acquire-and-release-semantics/ 概括说明 acq / rel 同步。


为什么不需要 smp_rb()(即使在弱排序的 ISA 上,获取排序也需要围栏指令来保证看到另一个核心完成的其他存储.)

中断处理程序 运行 在执行其他操作的同一核心上,就像信号处理程序中断线程和在其上下文中的 运行s 一样。 struct task_struct *t = current 表示我们正在查看的数据是单个任务的本地数据。这等效于 user-space 中单个线程中的某些内容。 (导致在不同内核上重新调度的内核抢占将使用任何必要的内存屏障,以在其他内核访问此任务一直使用的内存时保持单个线程的正确执行)。

用户-space C11 stdatomic 等同于此障碍是 atomic_signal_fence(memory_order_acquire)。信号栅栏只需要阻止编译时重新排序(如 Linux barrier()),不像 atomic_thread_fence 必须发出内存屏障 asm 指令。

无序的 CPU 会在内部重新排序,但是 OoO exec 的基本规则是一次保留指令 运行ning 的错觉,以便核心运行宁指令。这就是为什么 a = 1; b = a; 的 asm 等价物不需要内存屏障来正确加载刚刚存储的 1 的原因;硬件在程序顺序中保留了串行执行1 的错觉。 (通常通过让负载侦听存储缓冲区并将存储转发到尚未提交到 L1d 缓存的存储的负载。)

中断处理程序中的指令逻辑上 运行 在中断发生点之后(根据中断-return 地址)。因此我们只需要顺序正确的 asm 指令 (barrier()),硬件将使一切正常。

脚注 1:有一些明确并行的 ISA,如 IA-64 和 Mill,但它们提供了 asm 可以遵循的规则,以确保一条指令看到另一条较早指令的效果。与经典 MIPS 相同,我加载延迟槽和类似的东西。编译器会为已编译的 C 处理此问题。