英文

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

作者:

Mathieu Desnoyers

本文档解释了本地原子操作的目的,如何为任何给定的架构实现它们,并展示了如何正确使用它们。它还强调了当跨 CPU 读取这些局部变量时,如果内存写入顺序很重要,则必须采取的预防措施。

注意

请注意,不建议将基于 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 类型定义为不透明的 signed long,方法是在结构中嵌入 atomic_long_t。这样做是为了防止从这种类型转换为 long 失败。定义如下所示:

typedef struct { atomic_long_t a; } local_t;

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

  • 本地操作触及的变量必须是每个 CPU 变量。

  • 只有这些变量的 CPU 所有者才能写入它们。

  • 此 CPU 可以从任何上下文(进程、中断、软中断、NMI ...)中使用本地操作来更新其 local_t 变量。

  • 在进程上下文中使用本地操作时,必须禁用抢占(或中断),以确保在获取每个 CPU 变量和执行实际的本地操作之间,该进程不会迁移到不同的 CPU。

  • 在中断上下文中使用本地操作时,在主线内核上无需特别注意,因为它们将在本地 CPU 上运行,并且已禁用抢占。但是,我建议无论如何都明确禁用抢占,以确保它仍然可以在 -rt 内核上正常工作。

  • 读取本地 CPU 变量将提供该变量的当前副本。

  • 这些变量的读取可以从任何 CPU 完成,因为对齐的 “long” 变量的更新始终是原子的。由于写入 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));

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

这是一个示例模块,它使用 local.h 实现了一个基本的每个 CPU 计数器

/* test-local.c
 *
 * Sample module for local.h usage.
 */


#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;

/* IPI called on each CPU. */
static void test_each(void *info)
{
        /* Increment the counter from a non preemptible context */
        printk("Increment on cpu %d\n", smp_processor_id());
        local_inc(this_cpu_ptr(&counters));

        /* This is what incrementing the variable would look like within a
         * preemptible context (it disables preemption) :
         *
         * local_inc(&get_cpu_var(counters));
         * put_cpu_var(counters);
         */
}

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

        /* Increment the counters */
        on_each_cpu(test_each, NULL, 1);
        /* Read all the counters */
        printk("Counters read from CPU %d\n", smp_processor_id());
        for_each_online_cpu(cpu) {
                printk("Read : CPU %d, count %ld\n", cpu,
                        local_read(&per_cpu(counters, cpu)));
        }
        mod_timer(&test_timer, jiffies + 1000);
}

static int __init test_init(void)
{
        /* initialize the timer that will increment the counter */
        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("Local Atomic Ops");