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 区域的访问是例外情况,并且可能会影响通过 this_cpu_* 进行的本地 RMW 操作的性能和/或正确性(远程写入操作)。

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 操作

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 偏移量。

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

每个 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 vs this_cpu_ptr(&pp->n)

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

第二个操作首先添加两个偏移量,然后进行重定位。IMHO,第二种形式看起来更简洁,并且更容易使用 ()。第二种形式也与 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的per cpu数据的状态。由于this_cpu操作的宽松同步要求,写入访问可能会导致独特的问题。

以下示例说明了写入操作的一些问题,由于两个per cpu变量共享一个缓存行,但宽松的同步仅应用于一个进程更新缓存行,因此会出现以下情况。

考虑以下示例

struct test {
        atomic_t a;
        int b;
};

DEFINE_PER_CPU(struct test, onecacheline);

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

即使远程写入很少发生,也请记住,远程写入会将缓存行从最有可能访问它的处理器中逐出。如果处理器唤醒并发现per cpu区域的本地缓存行丢失,则其性能以及唤醒时间将受到影响。