this_cpu 操作

作者:

Christoph Lameter,2014 年 8 月 4 日

作者:

Pranith Kumar,2014 年 8 月 2 日

this_cpu 操作是一种优化访问与 当前 执行处理器关联的每 CPU 变量的方法。这是通过使用段寄存器(或 CPU 永久存储特定处理器每 CPU 区域起始地址的专用寄存器)来完成的。

this_cpu 操作将每 CPU 变量偏移量添加到特定于处理器的每 CPU 基址,并将该操作编码到对每 CPU 变量进行操作的指令中。

这意味着在偏移量的计算和数据操作之间不存在原子性问题。因此,无需禁用抢占或中断来确保处理器在地址计算和数据操作之间不被切换。

读-修改-写操作尤其重要。处理器通常有特殊的低延迟指令,可以在没有典型同步开销的情况下运行,但仍提供某种形式的宽松原子性保证。例如,x86 可以执行 RMW(读-修改-写)指令,如 inc/dec/cmpxchg,而无需 lock 前缀及相关的延迟开销。

没有 lock 前缀的变量访问不会被同步,但同步不是必需的,因为我们处理的是特定于当前执行处理器的每 CPU 数据。只有当前处理器才能访问该变量,因此系统中不存在与其他处理器的并发问题。

请注意,远程处理器对每 CPU 区域的访问是特殊情况,可能会影响本地 RMW 操作通过 this_cpu_* 的性能和/或正确性(远程写入操作)。

this_cpu 操作的主要用途是优化计数器操作。

定义了以下具有隐式抢占保护的 this_cpu() 操作。这些操作可以在不担心抢占和中断的情况下使用。

this_cpu_read(pcp)
this_cpu_write(pcp, val)
this_cpu_add(pcp, val)
this_cpu_and(pcp, val)
this_cpu_or(pcp, val)
this_cpu_add_return(pcp, val)
this_cpu_xchg(pcp, nval)
this_cpu_cmpxchg(pcp, oval, nval)
this_cpu_sub(pcp, val)
this_cpu_inc(pcp)
this_cpu_dec(pcp)
this_cpu_sub_return(pcp, val)
this_cpu_inc_return(pcp)
this_cpu_dec_return(pcp)

this_cpu 操作的内部工作原理

在 x86 上,fs: 或 gs: 段寄存器包含每 CPU 区域的基址。然后可以通过简单地使用段覆盖来将每 CPU 相对地址重定位到处理器的正确每 CPU 区域。因此,到每 CPU 基址的重定位通过段寄存器前缀编码在指令中。

例如

DEFINE_PER_CPU(int, x);
int z;

z = this_cpu_read(x);

导致一条指令

mov ax, gs:[x]

而不是每 CPU 操作中计算地址然后从该地址获取的序列。在 this_cpu_ops 之前,这样的序列还需要禁用/启用抢占,以防止内核在执行计算时将线程移动到不同的处理器。

考虑以下 this_cpu 操作

this_cpu_inc(x)

上述操作产生以下单条指令(没有 lock 前缀!)

inc gs:[x]

而不是在没有段寄存器时所需的以下操作

int *y;
int cpu;

cpu = get_cpu();
y = per_cpu_ptr(&x, cpu);
(*y)++;
put_cpu();

请注意,这些操作只能用于保留给特定处理器的每 CPU 数据。在周围代码不禁用抢占的情况下,this_cpu_inc() 只能保证其中一个每 CPU 计数器被正确递增。然而,不能保证操作系统不会在 this_cpu 指令执行之前或之后直接移动进程。通常这意味着每个处理器的各个计数器的值是无意义的。所有每 CPU 计数器的总和才是唯一有意义的值。

使用每 CPU 变量是出于性能原因。如果多个处理器并发地通过相同的代码路径,可以避免缓存行跳动。由于每个处理器都有自己的每 CPU 变量,因此不会发生并发的缓存行更新。这种优化所付出的代价是,当需要计数器的值时,必须将每 CPU 计数器相加。

特殊操作

y = this_cpu_ptr(&x)

获取每 CPU 变量的偏移量(&x !),并返回属于当前执行处理器的每 CPU 变量的地址。this_cpu_ptr 避免了常见的 get_cpu/put_cpu 序列所需的多个步骤。没有可用的处理器编号。相反,本地每 CPU 区域的偏移量简单地添加到每 CPU 偏移量中。

请注意,此操作只能在可以使用 smp_processor_id() 的代码段中使用,例如,已禁用抢占的地方。然后该指针用于在临界区中访问本地每 CPU 数据。当重新启用抢占时,此指针通常不再有用,因为它可能不再指向当前处理器的每 CPU 数据。

在可抢占代码中获取 per-CPU 指针的有意义的特殊情况由 raw_cpu_ptr() 处理,但此类用例需要处理两个不同 CPU 访问同一 per CPU 变量的情况,这很可能是第三个 CPU 的变量。这些用例通常是性能优化。例如,SRCU 将一对计数器实现为一对 per-CPU 变量,rcu_read_lock_nmisafe() 使用 raw_cpu_ptr() 获取指向某个 CPU 计数器的指针,并使用 atomic_inc_long() 来处理 raw_cpu_ptr() 和 atomic_inc_long() 之间的迁移。

每 CPU 变量和偏移量

每 CPU 变量具有指向每 CPU 区域起始的 偏移量。它们没有地址,尽管它们在代码中看起来像地址。偏移量不能直接解引用。偏移量必须添加到处理器的每 CPU 区域的基指针,才能形成有效地址。

因此,在每 CPU 操作上下文之外使用 x 或 &x 是无效的,通常会被视为 NULL 指针解引用。

DEFINE_PER_CPU(int, x);

在每 CPU 操作的上下文中,上述意味着 x 是一个每 CPU 变量。大多数 this_cpu 操作都接受一个 cpu 变量。

int __percpu *p = &x;

&x,因此 p,是每 CPU 变量的 偏移量。this_cpu_ptr() 接受每 CPU 变量的偏移量,这使得它看起来有点奇怪。

对每 CPU 结构字段的操作

假设我们有一个 percpu 结构

struct s {
        int n,m;
};

DEFINE_PER_CPU(struct s, p);

对这些字段的操作很简单

this_cpu_inc(p.m)

z = this_cpu_cmpxchg(p.m, 0, 1);

如果 struct s 有一个偏移量

struct s __percpu *ps = &p;

this_cpu_dec(ps->m);

z = this_cpu_inc_return(ps->n);

如果我们以后不使用 this_cpu 操作来操纵字段,指针的计算可能需要使用 this_cpu_ptr()

struct s *pp;

pp = this_cpu_ptr(&p);

pp->m--;

z = pp->n++;

this_cpu 操作的变体

this_cpu 操作是中断安全的。有些架构不支持这些每 CPU 本地操作。在这种情况下,操作必须替换为禁用中断的代码,然后执行保证原子性的操作,然后再重新启用中断。这样做代价很高。如果调度器无法更改我们正在执行的处理器有其他原因,则没有理由禁用中断。为此,提供了以下 __this_cpu 操作。

这些操作不保证防止并发中断或抢占。如果每 CPU 变量不在中断上下文中使用且调度器无法抢占,则它们是安全的。如果在操作进行中仍发生任何中断,并且中断也修改了变量,则无法保证 RMW 操作是安全的。

__this_cpu_read(pcp)
__this_cpu_write(pcp, val)
__this_cpu_add(pcp, val)
__this_cpu_and(pcp, val)
__this_cpu_or(pcp, val)
__this_cpu_add_return(pcp, val)
__this_cpu_xchg(pcp, nval)
__this_cpu_cmpxchg(pcp, oval, nval)
__this_cpu_sub(pcp, val)
__this_cpu_inc(pcp)
__this_cpu_dec(pcp)
__this_cpu_sub_return(pcp, val)
__this_cpu_inc_return(pcp)
__this_cpu_dec_return(pcp)

将递增 x,并且在不能通过地址重定位和同一条指令中的读-修改-写操作来实现原子性的平台上,不会回退到禁用中断的代码。

&this_cpu_ptr(pp)->n 与 this_cpu_ptr(&pp->n)

第一个操作获取偏移量并形成一个地址,然后添加 n 字段的偏移量。这可能导致编译器发出两条加法指令。

第二个操作首先将两个偏移量相加,然后进行重定位。在我看来,第二种形式看起来更简洁,并且更容易处理 ()。第二种形式也与 this_cpu_read() 和其友元函数的使用方式一致。

远程访问每 CPU 数据

每 CPU 数据结构旨在由一个 CPU 独占使用。如果按照预期使用变量,this_cpu_ops() 保证是“原子的”,因为没有其他 CPU 可以访问这些数据结构。

在某些特殊情况下,您可能需要远程访问每 CPU 数据结构。通常可以安全地进行远程读取访问,这经常用于汇总计数器。远程写入访问可能会有问题,因为 this_cpu 操作没有锁语义。远程写入可能会干扰 this_cpu RMW 操作。

强烈不鼓励对 percpu 数据结构进行远程写入访问,除非绝对必要。请考虑使用 IPI 唤醒远程 CPU 并执行对其 per CPU 区域的更新。

要远程访问 per-cpu 数据结构,通常使用 per_cpu_ptr() 函数

DEFINE_PER_CPU(struct data, datap);

struct data *p = per_cpu_ptr(&datap, cpu);

这明确表明我们正准备远程访问 percpu 区域。

您还可以执行以下操作将数据偏移量转换为地址

struct data *p = this_cpu_ptr(&datap);

但是,将通过 this_cpu_ptr 计算出的指针传递给其他 CPU 是不寻常的,应避免。

远程访问通常仅用于读取其他 CPU 的每 CPU 数据状态。由于 this_cpu 操作的宽松同步要求,写入访问可能会导致独特的问题。

一个说明写入操作相关问题示例如下:由于两个 per CPU 变量共享一个缓存行,但宽松同步仅应用于更新该缓存行的一个进程。

考虑以下示例

struct test {
        atomic_t a;
        int b;
};

DEFINE_PER_CPU(struct test, onecacheline);

人们担心,如果字段“a”从一个处理器远程更新,而本地处理器使用 this_cpu ops 更新字段 b,会发生什么。应注意避免同时访问同一缓存行内的数据。此外,可能需要昂贵的同步。在这种情况下,通常建议使用 IPI,而不是对另一个处理器的 per CPU 区域进行远程写入。

即使在远程写入很少的情况下,请记住远程写入会从最有可能访问它的处理器中逐出缓存行。如果处理器唤醒并发现丢失了每 CPU 区域的本地缓存行,其性能和唤醒时间将受到影响。