本地原子操作的语义和行为 【ChatGPT】

发布时间 2023-12-09 19:59:40作者: 摩斯电码

这篇文档介绍了本地原子操作的语义和行为,以及如何在任何给定的架构中实现它们,并展示了它们如何被正确地使用。它还强调了在读取这些本地变量时必须采取的预防措施,特别是当内存写入的顺序很重要时。

注意
请注意,不建议在一般内核使用中使用基于 local_t 的操作。除非确实有特殊目的,请改用 this_cpu 操作。内核中大多数对 local_t 的使用已被 this_cpu 操作所取代。this_cpu 操作将重定位与 local_t 类似的语义结合在单个指令中,生成更紧凑和执行速度更快的代码。

本地原子操作的目的

本地原子操作旨在提供快速和高度可重入的每 CPU 计数器。它通过消除通常需要同步跨 CPU 的 LOCK 前缀和内存屏障来最小化标准原子操作的性能成本。

在许多情况下,拥有快速的每 CPU 原子计数器是很有意义的:它不需要禁用中断来保护中断处理程序,并且允许 NMI 处理程序中的一致计数器。这对于跟踪目的和各种性能监视计数器尤其有用。

本地原子操作仅保证相对于拥有数据的 CPU 的变量修改的原子性。因此,必须小心确保只有一个 CPU 写入 local_t 数据。这是通过使用每 CPU 数据并确保我们从一个抢占安全的上下文中修改它来完成的。但是,允许从任何 CPU 读取 local_t 数据:然后它将似乎相对于拥有者 CPU 的其他内存写入是无序的。

针对特定架构的实现

可以通过轻微修改标准原子操作来实现它:只需保留它们的 UP 变体。这通常意味着删除 LOCK 前缀(在 i386 和 x86_64 上)和任何 SMP 同步屏障。如果架构在 SMP 和 UP 之间没有不同的行为,则在您的架构的 local.h 中包含 asm-generic/local.h 就足够了。

local_t 类型被定义为一个不透明的有符号长整型,通过在结构体中嵌入 atomic_long_t 来实现。这样做是为了使得从这种类型到长整型的转换失败。定义如下:

typedef struct { atomic_long_t a; } local_t;

在使用本地原子操作时需要遵循的规则

  • 被本地操作触及的变量必须是每 CPU 变量。
  • 只有这些变量的 CPU 拥有者才能对其进行写入。
  • 这个 CPU 可以在任何上下文(进程、中断、软中断、NMI 等)中使用本地操作来更新它的 local_t 变量。
  • 在进程上下文中使用本地操作时必须禁用抢占(或中断),以确保进程在获取每 CPU 变量并执行实际的本地操作之间不会被迁移到不同的 CPU。
  • 在中断上下文中使用本地操作时,在主线内核上不需要特别注意,因为它们将在已经禁用抢占的本地 CPU 上运行。然而,我建议无论如何明确禁用抢占,以确保它在 -rt 内核上仍能正确工作。
  • 读取本地 CPU 变量将提供变量的当前副本。
  • 可以从任何 CPU 读取这些变量,因为对于“长整型”对齐的变量的更新始终是原子的。由于写入 CPU 没有进行内存同步,因此在读取其他 CPU 的变量时可能会读取到变量的过时副本。

如何使用本地原子操作

#include <linux/percpu.h>
#include <asm/local.h>

static DEFINE_PER_CPU(local_t, counters) = LOCAL_INIT(0);

计数

对有符号长整型的所有位进行计数。

在可抢占的上下文中,使用 get_cpu_var() 和 put_cpu_var() 包围本地原子操作:它确保在写访问每 CPU 变量时禁用了抢占。例如:

local_inc(&get_cpu_var(counters));
put_cpu_var(counters);

如果您已经在一个抢占安全的上下文中,可以使用 this_cpu_ptr() 代替:

local_inc(this_cpu_ptr(&counters));

读取计数器

可以从其他 CPU 读取这些本地计数器以对计数进行求和。请注意,跨 CPU 的 local_read 看到的数据必须被认为相对于拥有数据的 CPU 上发生的其他内存写入是无序的:

long sum = 0;
for_each_online_cpu(cpu)
        sum += local_read(&per_cpu(counters, cpu));

如果要使用远程 local_read 在 CPU 之间同步对资源的访问,必须在写入 CPU 和读取 CPU 上分别使用显式的 smp_wmb() 和 smp_rmb() 内存屏障。如果您将 local_t 变量用作缓冲区中写入的字节数的计数器,那么在缓冲区写入和计数器增加之间应该有一个 smp_wmb(),并且在计数器读取和缓冲区读取之间应该有一个 smp_rmb()。

以下是一个使用 local.h 实现基本每 CPU 计数器的示例模块:

/* test-local.c
 *
 * 用于 local.h 使用的示例模块。
 */

#include <asm/local.h>
#include <linux/module.h>
#include <linux/timer.h>

static DEFINE_PER_CPU(local_t, counters) = LOCAL_INIT(0);

static struct timer_list test_timer;

/* 在每个 CPU 上调用的 IPI。 */
static void test_each(void *info)
{
        /* 从非抢占上下文中增加计数器 */
        printk("在 CPU %d 上增加\n", smp_processor_id());
        local_inc(this_cpu_ptr(&counters));

        /* 在抢占安全的上下文中增加变量的操作如下(它禁用了抢占):
         *
         * local_inc(&get_cpu_var(counters));
         * put_cpu_var(counters);
         */
}

static void do_test_timer(unsigned long data)
{
        int cpu;

        /* 增加计数器 */
        on_each_cpu(test_each, NULL, 1);
        /* 读取所有计数器 */
        printk("从 CPU %d 读取计数器\n", smp_processor_id());
        for_each_online_cpu(cpu) {
                printk("读取:CPU %d,计数 %ld\n", cpu,
                        local_read(&per_cpu(counters, cpu)));
        }
        mod_timer(&test_timer, jiffies + 1000);
}

static int __init test_init(void)
{
        /* 初始化将增加计数器的定时器 */
        timer_setup(&test_timer, do_test_timer, 0);
        mod_timer(&test_timer, jiffies + 1);

        return 0;
}

static void __exit test_exit(void)
{
        timer_shutdown_sync(&test_timer);
}

module_init(test_init);
module_exit(test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Mathieu Desnoyers");
MODULE_DESCRIPTION("本地原子操作");