关注了就能看到更多这么棒的文章哦~
Per-CPU memory for user space
By Jonathan Corbet
April 8, 2025
LSFMM+BPF
Gemini-1.5-flash translation
https://lwn.net/Articles/1016408/
内核广泛使用 per-CPU 数据,以此来避免处理器之间的争用并提高可扩展性。在用户空间中使用相同的技术则更难,因为我们几乎无法控制进程可能在哪个 CPU 上运行。尽管如此,Mathieu Desnoyers 仍在尝试;在 2025 年 Linux 存储、文件系统、内存管理和 BPF 峰会的内存管理专题讨论中,他提出了一个关于用户空间 per-CPU 内存如何工作的提议。
Desnoyers 首先表示,他的目标是帮助用户空间开发者更好地利用 restartable sequences (可重启序列),它通过在关键部分中断进程(如果进程在此时被迁移),从而促进对 per-CPU 数据的某些类型的访问。用户空间应用程序通常使用线程局部存储(thread-local storage)来处理这类代码,但如果运行的线程多于可用的 CPU 数量,这种方式就会变得效率低下。线程局部存储还必须静态定义,这使得它的灵活性较差,并且如果区域很大,还会减慢线程的创建速度。
因此,他希望提供真正的 per-CPU 数据作为替代方案。他说,一种 不 应该使用的方法是将 per-CPU 数据组织成一个数组,并以 CPU 编号作为索引。这种实现相对简单;代码可以直接从 sched_getcpu()
函数或从可重启序列的共享内存区域获取其当前的 CPU 编号。但是,如果这个数组是紧凑排列的,就会导致 CPU 之间的缓存行弹跳(cache-line bouncing),从而消除大部分(或全部)性能优势。如果数组条目按照缓存行对齐,那么条目之间可能会浪费大量的空间。
他说,内核的 per-CPU 分配器在每个 CPU 上映射一个地址空间范围,以提供对该 CPU 本地内存空间的访问;分配操作只返回一个地址,该地址可以在所有 CPU 上使用。他已经在 librseq 库中实现了一种类似的方法。该分配器创建一组内存池,每个 CPU 对应一个;然后,分配操作返回一个在每个 CPU 上都相同的偏移量。本质上,这是缓存行对齐数组方法的变体,但分配器会将分配的数据紧凑地排列在每个 CPU 的区域内,从而减少这些区域之间的内存浪费。它可以支持多个池,从而将不同的用户隔离开来。
内核的 memfd feature (内存文件描述符特性) 被用于创建 per-CPU 内存池。他说,这几乎是唯一允许他创建特性所需的、到同一区域的各种映射的方法。
不过,这种方法也存在一些潜在的问题。如果一个四线程进程运行在一个拥有 512 个 CPU 的系统上,会发生什么?为所有这些 CPU 分配和初始化内存将是一种浪费,因为大部分内存永远不会被使用。因此,该库首先在一个特殊的“初始化区域”中初始化一个 CPU 的内存,然后为每个 CPU 创建该区域的写时复制(copy-on-write)映射。任何 CPU 从其区域读取数据时,都会从该单一副本中读取;如果一个 CPU 写入其区域,该页面将被复制,并真正成为 CPU 本地的。
另一个需要关注的问题是进程 fork 之后会发生什么;per-CPU 区域将在 fork 之后被共享,这可能不是我们想要的结果。他正在考虑添加一个 memfd 标志 (MFD_PRIVATE ),该标志将使内存区域成为 per-process (每个进程私有);这样,fork 之后,子进程将获得该区域的一个单独副本。目前,他正在使用一个“不太方便的变通方法”,该方法由几个 madvise()
操作组成,用于检测和处理 fork。作为其中的一部分,该库映射一个特殊的“金丝雀页面”(canary page),该页面被设置为在 fork 发生时被清除;然后可以检查该页面的内容来检测 fork。
将来,他正在考虑添加在 per-CPU 内存池中分配可变大小元素的能力。在每个 CPU 区域之间放置保护页(guard pages)将防止一个 CPU 预取到另一个 CPU 区域中而引起的缓存行弹跳。他还考虑改进控制组(control-group)的 CPU 控制器,以允许设置最大并发限制,这将可以更严格地限制池中所需的条目数。
Desnoyers 在结束他的演讲时,再次提到了 MFD_PRIVATE
这个想法,他认为这是解决 fork 问题的最佳方法。他说,这个特性在其他情况下也会很有用。 MESH allocator 需要这种特性,Google 的动态分析工具也需要。David Hildenbrand 说, MFD_PRIVATE
可能是一个合理的补充,但他认为它的使用也应该隐含类似于使用 MADV_WIPEONFORK
时的行为,即在 fork 发生时将内存清零。Desnoyers 回答说,这种行为可能不是我们想要的;子进程仍然可以利用来自父进程的 per-CPU 数据,但会拥有自己的副本,以便进行任何更改。
Suren Baghdasaryan 评论了使用保护页来防止跨 CPU 预取的可能性,并指出这种行为是与架构相关的。他想知道 Desnoyers 是否考虑过这项工作如何与 cpusets 交互。Desnoyers 说 cpusets 和 per-CPU 内存可以协同工作,但存在一些挑战。由于他的库不会收到 CPU 热插拔通知,因此它必须为 CPU 拓扑中意外的变化做好准备。
Hildenbrand 问,进程如何确保 CPU 在访问 per-CPU 数据时不会在它们不知情的情况下发生变化;Desnoyers 回答说,通常使用可重启序列来做到这一点。我接着问,可重启序列是否是使用此内存的唯一安全方法;他说,还有其他选择,包括原子操作或 rseq locks (rseq 锁)。
会议到此结束。Desnoyers 没有发布本次会议的幻灯片,但 他在 2 月份 FOSDEM 大会上的幻灯片 涵盖了相同的要点。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~