Warning
此文件的目的是为让中文读者更容易阅读和理解,而不是作为一个分支。 因此, 如果您对此文件有任何意见或更新,请先尝试更新原始英文文件。
Note
如果您发现本文档与原始文件有任何不同或者有翻译问题,请联系该文件的译者, 或者请求时奎亮的帮助:<alexs@kernel.org>。
- Original:
- 翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
本地原子操作的语义和行为¶
- 作者:
Mathieu Desnoyers
本文解释了本地原子操作的目的,如何为任何给定的架构实现这些操作,并说明了 如何正确使用这些操作。它还强调了在内存写入顺序很重要的情况下,跨CPU读取 这些本地变量时必须采取的预防措施。
Note
注意,基于 local_t
的操作不建议用于一般内核操作。请使用 this_cpu
操作来代替使用,除非真的有特殊目的。大多数内核中使用的 local_t
已
经被 this_cpu
操作所取代。 this_cpu
操作在一条指令中结合了重
定位和类似 local_t
的语义,产生了更紧凑和更快的执行代码。
本地原子操作的目的¶
本地原子操作的目的是提供快速和高度可重入的每CPU计数器。它们通过移除LOCK前 缀和通常需要在CPU间同步的内存屏障,将标准原子操作的性能成本降到最低。
在许多情况下,拥有快速的每CPU原子计数器是很有吸引力的:它不需要禁用中断来保护中 断处理程序,它允许在NMI(Non Maskable Interrupt)处理程序中使用连贯的计数器。 它对追踪目的和各种性能监测计数器特别有用。
本地原子操作只保证在拥有数据的CPU上的变量修改的原子性。因此,必须注意确保只
有一个CPU写到 local_t
的数据。这是通过使用每CPU的数据来实现的,并确
保我们在一个抢占式安全上下文中修改它。然而,从任何一个CPU读取 local_t
数据都是允许的:这样它就会显得与所有者CPU的其他内存写入顺序不一致。
针对特定架构的实现¶
这可以通过稍微修改标准的原子操作来实现:只有它们的UP变体必须被保留。这通常
意味着删除LOCK前缀(在i386和x86_64上)和任何SMP同步屏障。如果架构在SMP和
UP之间没有不同的行为,在你的架构的 local.h
中包括 asm-generic/local.h
就足够了。
通过在一个结构体中嵌入一个 atomic_long_t
, local_t
类型被定义为
一个不透明的 signed long
。这样做的目的是为了使从这个类型到
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);
计数器¶
计数是在一个signed long的所有位上进行的。
在可抢占的上下文中,围绕本地原子操作使用 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中读取,以求得计数的总和。请注意,local_read 所看到的跨CPU的数据必须被认为是相对于拥有该数据的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)
{
del_timer_sync(&test_timer);
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Mathieu Desnoyers");
MODULE_DESCRIPTION("Local Atomic Ops");