背景

mempolicy 的 interleave 策略把页轮流散布到策略 nodemask 内的各个节点,weighted interleave 再按每个节点的权重决定散布比例。缺页 fault 路径上,interleave_nid() 与 weighted_interleave_nid() 每次 fault 都要遍历 nodemask 选出节点,批量分配器 alloc_pages_bulk_weighted_interleave() 则要遍历全局权重表 iw_table,算出每个节点各分多少页。

这些状态随时可能被并发修改:cpuset 变化触发 rebind 改写 nodemask,sysfs 写者整体更换权重表。为了让遍历期间数据不变,现状是先把状态拷一份再走循环:节点选择器把整个 nodemask(MAX_NUMNODES=1024 时为 128 字节)拷到栈上;批量分配器每次调用都 kmalloc 一块临时数组,在 RCU 读锁下拷入权重表。拷权重而不是持锁遍历,是因为页分配器可能睡眠,不能留在 RCU 读侧临界区里。写者换完表还要 synchronize_rcu() 等读者退出,才能释放旧表。

┌──────────────────────────────────────────┐
│       fault 路径选取节点或批量分配       │
└────────────────────┬─────────────────────┘
                     ▼
┌──────────────────────────────────────────┐  nodemask 128 字节,权重表另加一次
│     把 nodemask 与权重表拷到本地副本     │  kmalloc
└────────────────────┬─────────────────────┘
                     ▼
┌──────────────────────────────────────────┐
│ 遍历本地副本,写者换表后 synchronize_rcu │
│ 等读者退出                               │
└──────────────────────────────────────────┘

问题

  • 两个节点选择器每次 folio fault 都把 128 字节 nodemask 拷到栈上
  • 批量分配器每次调用为权重表多做一次 kmalloc 与 memcpy
  • 写者更换权重表后用 synchronize_rcu() 等读者退出,写路径被阻塞

方案

取消 fault 路径上的两次状态拷贝:权重表改用 SRCU pin 住就地遍历,nodemask 直接遍历并补边界检查。

批量分配路径进入 SRCU 读侧临界区后直接引用全局权重表,睡眠不受限,临时数组与拷表随之消失。旧表退役先走 call_srcu(),回调里再做 kfree_rcu(),睡眠中的读者与普通 RCU 读者都被覆盖,sysfs 写者换表后不再需要 synchronize_rcu() 原地等待。

节点选择器直接在 pol->nodes 上遍历。并发 rebind 可能缩小 mask,也可能把一次读撕开、让 mask 看起来是空的,两个选择器各有处理:

  • weighted_interleave_nid() 边累加权重边数出节点数,把目标查找限制在单趟以内,读出的权重总和为零或遍历扑空就回退 numa_node_id()
  • interleave_nid() 改用 next_node_in() 遍历,中途缩小的 mask 会绕回仍在策略内的节点,越界同样回退 numa_node_id()

回退 numa_node_id() 正是过去拷贝结果为空时的既有行为,语义没有变化。

批量分配器按权重总和推算每个节点分多少页,而总和与节点数是在 nodemask 遍历之外单独读出的,rebind 可能让推算结果超过请求总量,于是每个分配块都钳制到 page_array 剩余空间,读到空 mask 则返回已经分到的部分。cpuset cookie 在这里用不上:三个函数里有两个拿的是 VMA 策略,rebind 发生在 mmap_write_lock 下,不走 mems_allowed_seq 序列。

代价是 rebind 进行期间节点分布精度下降。原来的拷贝同样没有修正分布,它防的只是除零与越过请求边界,这两件事现在由边界检查接手。

                         ┌──────────────────────────────┐
                         │ fault 路径选取节点或批量分配 │
                         └──────────────┬───────────────┘
                 ┌──────────────────────┴──────────────────────┐
                 ▼                                             ▼
┌──────────────────────────────────┐     ┌────────────────────────────────────────────┐
│ 权重表用 SRCU pin 住就地遍历,写 │     │ nodemask 直接遍历,扑空回退 numa_node_id, │
│ 者换表不再原地等读者             │     │ 分配块钳制到请求剩余                       │
└──────────────────────────────────┘     └────────────────────────────────────────────┘

收益

patch 2/2 给出 MAX_NUMNODES=1024 下 -fstack-usage 实测的栈占用(字节,越小越好):

函数 Before After 下降
weighted_interleave_nid 184 56 69.6%
interleave_nid 168 32 81.0%
alloc_pages_bulk_mempolicy_noprof 360 136 62.2%

其余从代码逻辑推断的预期收益:

  • fault 路径的节点选取少一次带屏障的 128 字节 nodemask 拷贝
  • 批量分配每次调用省一次 kmalloc 与权重表 memcpy
  • sysfs 写者更新权重表后不再被 synchronize_rcu() 阻塞