K11 —— 物理页分配的全路径:alloc_pages 和伙伴系统

金句:每次调用 alloc_pages,都经历了一次从请求到物理页框的完整旅程。


alloc_pages 的函数签名

Bash
alloc_pages 是 Linux 分配物理页的核心函数:

struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);

参数:
  - gfp_mask:分配标志(GFP_KERNEL / GFP_ATOMIC 等)
  - order:分配 2^order 个页(order=01页,order=24页)

返回值:
  - 成功:返回指向 struct page 的指针
  - 失败:返回 NULL

gfp_mask 的关键标志:

  域(zone)选择:
    __GFP_DMA:只在 ZONE_DMA 分配
    __GFP_HIGHMEM:可以在 ZONE_HIGHMEM 分配
    (默认:ZONE_NORMAL + ZONE_HIGHMEM)

  行为(action):
    __GFP_WAIT:允许睡眠等待内存(慢路径)
    __GFP_HIGH:高优先级,紧急分配

  特殊:
    __GFP_ZERO:返回清零的页面
    __GFP_NOFAIL:不允许失败(持续重试)

GFP_KERNEL = (__GFP_WAIT | __GFP_IO | __GFP_FS)
  → 正常分配,可以等待,可以磁盘 I/O,可以文件系统

分配路径的完整流程

Bash
alloc_pages 的调用链:

alloc_pages(gfp_mask, order)
  → __alloc_pages(gfp_mask, order, 0, NULL)
      → get_page_from_freelist(gfp_mask, order, ...)
          → __alloc_pages_nodemask(gfp_mask, order, ...)
              → try_this_node(node, gfp_mask, order)
                  → buddy 系统分配

实际路径(有水线和回收):

alloc_pages()
  → check_new_pages() // 快速路径:检查当前 CPU 的缓存
  → if (NULL) __alloc_pages()
      → watermark 检查(pages_high / pages_low / pages_min)
      → if (watermark OK) buddy 分配(fast path)
      → if (watermark FAIL) {
          // 唤醒 kswapd
          // 尝试直接回收(direct reclaim)
          // 再试 buddy 分配
          // 还失败?返回 NULL 或触发 OOM
        }

Bash
Buddy System 分配的核心函数:

static inline struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
                        int preferred_nid, nodemask_t *nodemask)
{
    struct zone *zone;
    struct plist_head *queues;
    enum zone_type classzone_idx;

    // 1. 找到最高优先级的可用节点和 zone
    // 2. 检查水位线(watermark)
    // 3. 如果 OK,调用 rmqueue_pcplist(per-CPU 页框)
    // 4. 如果失败,调用 __rmqueue(真正从 buddy 分配)
    // 5. 如果 buddy 失败,尝试从备用节点分配

    // __rmqueue(真正分配):
    // for (order = requested; order <= MAX_ORDER; order++)
    //   在每个 order 的 free_list 上找可用块
    //   如果找到,split 进入更小的 order
    //   返回分配的页面
}

watermark(内存水位线)

Bash
三个关键水位线:

1. pages_high(高水位):
   - 内存充足,可以自由分配
   - kswapd 处于休眠状态

2. pages_low(低水位):
   - 开始唤醒 kswapd
   - 异步回收开始

3. pages_min(最低水位):
   - 必须立即回收(direct reclaim)
   - 如果回收失败,触发 OOM Killer

计算方式:
  - 最低水位(pages_min)= sqrt(物理内存) * 16 / sqrt(page_size) / 4
  - 高水位(pages_high)= pages_min + watermark_scale_factor * total_pages / 100
  - 低水位(pages_low)= pages_min + watermark_scale_factor * total_pages / 200

查看水位:
  $ cat /proc/zoneinfo
  Node 0, zone      Normal
    pages free      123456
    min      1234
    low      2345
    high     3456

高阶分配(allocate > 4KB)

Bash
order > 0 时的分配:

alloc_pages(GFP_KERNEL, 0);  // 分配 1 页(4KB)
alloc_pages(GFP_KERNEL, 2);  // 分配 4 页(16KB)

高阶分配的特点:
  1. 从 buddy system 的对应 order 链表分配
  2. 如果 order=N 链表为空,尝试 order=N+1
  3. 如果分配了 order=N+1 的块,拆成两个 order=N
  4. 一个分配,另一个放入 order=N 链表

例子:分配 128KB(order=5)

  free_list[5] 有块 → 直接分配

  free_list[5] 空:
    free_list[6] 有 256KB 块
    → 拆成两个 128KB
    → 一个分配,一个放入 free_list[5]

  free_list[6] 也空:
    → 继续向上找,直到 MAX_ORDER

  MAX_ORDER(11)仍然没有:
    → 触发回收或返回 NULL

分配失败和 OOM

Bash
分配失败的处理:

1. 快速路径失败(fast path):
   - watermark 不满足
   - 唤醒 kswapd
   - schedule(让出 CPU)

2. 直接回收(direct reclaim):
   - 调用 shrink_page_list()
   - 扫描 LRU,释放页面
   - 写回 dirty 页面到磁盘
   - swapout anonymous 页面

3. 仍然失败:
   - 如果是 GFP_ATOMIC(中断上下文):
     → BUG() 或 WARN(),无法等待

   - 如果是 GFP_KERNEL:
     → out_of_memory() 触发 OOM Killer
     → 杀掉最"坏"的进程
     → 释放其页面

查看分配失败日志:
  $ dmesg | grep -i "out of memory"
  [12345.678] Out of memory: Killed process 1234 (nginx)

总结

  • alloc_pages(gfp_mask, order):分配 2^order 个物理页,返回 struct page 指针
  • GFP_MASK:GFP_KERNEL(可等待+IO)/ GFP_ATOMIC(不可等待)/ __GFP_ZERO(清零)
  • watermark:pages_high(充足)/ pages_low(kswapd 唤醒)/ pages_min(强制回收/触发 OOM)
  • __rmqueue:遍历 free_list[order] → 如果空向上拆分 → 分配后返回
  • 分配失败:先 direct reclaim → 再失败触发 OOM Killer

下篇预告(K12):内存碎片化是怎么形成的?compaction 和内存规整——为什么分配大块内存会失败,以及怎么缓解。


关注公众号「AI不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年7月21日

作者

评论

发表评论

您的邮箱地址不会被公开。