this_cpu 操作 【ChatGPT】

发布时间 2023-12-09 20:34:58作者: 摩斯电码

作者 Christoph Lameter,2014年8月4日
作者 Pranith Kumar,2014年8月2日

this_cpu操作是一种优化访问与当前执行处理器相关的per cpu变量的方法。这是通过使用段寄存器(或者CPU专门存储特定处理器的per cpu区域开头的专用寄存器)来实现的。

this_cpu操作将per cpu变量的偏移量添加到处理器特定的per cpu基址,并将该操作编码到操作per cpu变量的指令中。

这意味着在计算偏移量和对数据进行操作之间没有原子性问题。因此,不需要禁用抢占或中断来确保在计算地址和对数据进行操作之间不更改处理器。

读-修改-写操作特别重要。通常处理器具有特殊的低延迟指令,可以在没有典型同步开销的情况下进行操作,但仍提供某种程度的松散原子性保证。例如,x86可以执行RMW(读取修改写)指令,如inc/dec/cmpxchg,而无需使用lock前缀和相关的延迟惩罚。

无lock前缀的变量访问不是同步的,但不需要同步,因为我们处理的是特定于当前执行处理器的per cpu数据。只有当前处理器应该访问该变量,因此在系统中不存在与其他处理器的并发问题。

请注意,远程处理器对per 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:段寄存器包含per cpu区域的基址。因此,可以简单地使用段覆盖来将per cpu相对地址重新定位到适当的处理器的per cpu区域。因此,通过段寄存器前缀,可以将重定位到per cpu基址的操作编码到指令中。

例如:

DEFINE_PER_CPU(int, x);
int z;

z = this_cpu_read(x);

结果是单条指令:

mov ax, gs:[x]

而不是执行per 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();

请注意,这些操作只能用于为特定处理器保留的per cpu数据。在周围代码中不禁用抢占的情况下,this_cpu_inc()只能保证正确递增一个per cpu计数器。但是,不能保证操作this_cpu指令执行之前或之后OS不会将进程移动。通常情况下,这意味着每个处理器的个别计数器值是无意义的。所有per cpu计数器的总和是唯一感兴趣的值。

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

特殊操作

y = this_cpu_ptr(&x)

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

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

per cpu变量和偏移量

per cpu变量具有到per cpu区域开头的偏移量。尽管在代码中看起来像地址,但它们没有地址。偏移量不能直接解引用。必须将偏移量添加到处理器的per cpu区域的基指针中,以形成有效地址。

因此,在per cpu操作的上下文之外使用x或&x是无效的,并且通常会被视为空指针解引用。

DEFINE_PER_CPU(int, x);

在per cpu操作的上下文中,上述意味着x是一个per cpu变量。大多数this_cpu操作都使用一个cpu变量。

int __percpu *p = &x;

&x和因此p是per cpu变量的偏移量。this_cpu_ptr()获取per cpu变量的偏移量,这使得看起来有点奇怪。

对per 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操作是中断安全的。某些体系结构不支持这些per cpu本地操作。在这种情况下,必须用禁用中断的代码替换操作,然后执行保证原子性的操作,最后重新启用中断。这样做是昂贵的。如果还有其他原因导致调度程序无法更改正在执行的处理器,那么就没有理由禁用中断。为此,提供了以下__this_cpu操作。

这些操作不能保证不会发生并发中断或抢占。如果per 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指令。

第二个操作首先添加两个偏移量,然后进行重定位。在我看来,第二种形式看起来更清晰,而且在使用()时更容易。第二种形式也与this_cpu_read()等的使用方式一致。

对per cpu数据的远程访问

per cpu数据结构设计为仅由一个CPU使用。如果按照预期使用变量,this_cpu_ops()将保证是“原子”的,因为没有其他CPU可以访问这些数据结构。

有一些特殊情况,您可能需要远程访问per 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区域。

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

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区域的本地缓存行,其性能以及唤醒时间将受到影响。