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

作者:

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

如果你想使用远程 local_read 来在 CPU 之间同步对资源的访问,则必须分别在写入者和读取者 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");