钟哩哩头像
Linux 缺页异常与大页内存(HugePages):透明大页(THP)在高性能计算中的双刃剑效应封面图

Linux 缺页异常与大页内存(HugePages):透明大页(THP)在高性能计算中的双刃剑效应

封面信息图

在现代服务器物理内存动辄数百 GB 乃至数 TB 的背景下,系统面临的最隐蔽性能杀手之一不是内存不够用,而是页表开销与 TLB 命中率崩溃。在传统的 x86_64 四级页表架构下,基准物理页尺寸为 4KB。如果一个进程持有了 64GB 活跃数据,内核就需要维护多达 16777216 个页表项(PTE),仅页表自身就需要占用 128MB 物理内存。更为致命的是,CPU 的转换后援缓冲区(TLB)容量极其宝贵,L1 D-TLB 通常仅有 64 到 128 个条目。海量的 4KB 随机内存寻址会导致 TLB 发生高频 Missing,每一次 Missing 都意味着 CPU 必须被迫在 L1/L2/L3 甚至物理内存中进行 4 次串行解引用遍历页表项,内存访问延迟瞬时飙升数倍。

为了解决 TLB 覆盖率瓶颈,Linux 引入了大页(HugePages,如 2MB 或 1GB 巨页)。一个 2MB 的大页条目即可替代 512 个传统的 4KB 页表项,TLB 缓存覆盖面积成倍扩展。为了降低开发者的适配门槛,Linux 内核进一步推出了“透明大页”(Transparent Huge Pages, THP),试图在应用程序完全无感的前提下,由内核自动将离散的 4KB 页聚合为 2MB 大页。然而,正是这个看似美好的自动化特性,在无数生产系统、内存数据库(Redis、RocksDB)以及端侧模型推理服务中,诱发了毁灭性的秒级毛刺与性能雪崩。

透明大页背后的死锁诱因:khugepaged 与直接内存紧缩

THP 之所以成为“双刃剑”,根源在于其内存聚合的物理现实与低延迟应用所要求的确定性之间存在无法调和的冲突:

  1. 直接内存紧缩(Direct Compaction)卡死用户线程:当应用程序通过 mmap 或 brk 申请内存,且 THP 设置为 always 时,内核在缺页异常(Page Fault)处理流程中会优先尝试直接分配连续的 2MB 物理物理页。如果内存子系统存在外部碎片,找不到连续的 2MB 空闲块,缺页异常上下文就会退化为“直接内存紧缩”。此时,分配线程被强制挂起,内核开始在整个 NUMA 节点上大面积扫描页帧、进行物理页数据拷贝搬移以拼凑连续空间。这个过程耗时往往长达几十毫秒甚至整整数秒,原本亚毫秒级的内存读写操作瞬间变成恶性长长尾延迟。
  2. khugepaged 后台线程的锁竞争(mmap_lock Contention):除了直接紧缩,内核还派生了 khugepaged 内核守护线程,持续在后台扫描各个进程的 VMA(虚拟内存区域),试图将已有的 512 个连续 4KB 页折叠收拢为一个 2MB 大页。在执行页折叠时,khugepaged 必须持有目标进程的 mm->mmap_lock 独占写锁。在高并发多线程服务中,一旦 mmap_lock 被写锁独占,所有的工作线程执行任何 malloc、free、mprotect 或触发缺页时全部进入阻塞休眠状态,整个进程发生“假死”。
  3. 内存写放大与写时复制(COW)灾难:在 Redis 等依赖 fork 进行 RDB 持久化或后台快照的系统中,一旦开启 THP,子进程和父进程共享内存页。当父进程仅仅修改了 4KB 区域中的 1 个字节时,由于硬件页表是以 2MB 颗粒度标记为只读的,内核被迫将整整 2MB 的物理大页完整复制一份。内存写放大比例暴增 512 倍,瞬间诱发系统物理内存耗尽触发 OOM 杀手。
[应用程序发起内存写入] ---> 触发缺页异常 (Page Fault)
                                |
                   [THP 配置为 always 模式?]
                    /                   \
            (是)  /                       \ (否 / madvise 模式)
                 v                         v
   [检查物理连续 2MB 页池]            [按需快速分配 4KB 页]
        /             \                    |
  (有空闲) /             \ (无连续大页)      +---> [立即返回执行]
        v                 v
[极速映射 2MB]      [退化为直接内存紧缩 (Direct Compaction)]
                          | (挂起用户工作线程数十至数百毫秒)
                          +---> [大面积迁移扫描页帧]
                          +---> [陷入严重尾延迟]

现代系统最佳架构:从 THP 暴力模式走向 madvise 精准受控

在严谨的系统工程中,彻底关闭全局透明大页,转为由应用程序在关键内存段显式声明需求,是兼顾吞吐与低延迟的唯一解。

1. 操作系统层面固化内核策略

将透明大页从全局强制(always)降级为受控(madvise),并彻底封堵直接内存紧缩:

# 仅对应用程序显式请求了 MADV_HUGEPAGE 的 VMA 分配大页
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

# 绝对禁止在缺页时执行直接内存紧缩,碎片不足直接降级为 4KB,不阻塞线程
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag

# 彻底禁用后台无感聚合
echo 0 > /sys/kernel/mm/transparent_hugepage/khugepaged/defrag
2. C23 标准下基于 madvise 与显式大页的内存管理实现

在关键的高性能环形任务队列、连续张量权重区或网络 DMA 缓冲区,通过 POSIX 内存对齐分配配合 madvise,精准获得大页 TLB 加速,同时杜绝常规数据结构的写放大问题。

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <errno.h>

constexpr size_t HUGEPAGE_2MB_SIZE = 2 * 1024 * 1024; // 2MB 大页边界

typedef struct {
    void *raw_ptr;
    size_t size;
    bool is_huge;
} PrecisionMemoryBlock;

// 申请精准对齐的大页内存区域
bool allocate_precision_huge_block(PrecisionMemoryBlock *block, size_t num_hugepages) {
    if (!block || num_hugepages == 0) return false;

    size_t total_size = num_hugepages * HUGEPAGE_2MB_SIZE;

    // 1. 使用 mmap 分配严格 2MB 对齐的匿名物理内存
    // MAP_ANONYMOUS 配合 PROT_READ | PROT_WRITE
    void *ptr = mmap(nullptr, total_size, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

    if (ptr == MAP_FAILED) {
        perror("mmap failed");
        return false;
    }

    // 2. 检查返回的虚拟地址是否自然对齐在 2MB 边界上
    if (((uintptr_t)ptr % HUGEPAGE_2MB_SIZE) != 0) {
        // 若未严格对齐,内核无法建立 2MB PMD 页表项
        munmap(ptr, total_size);
        fprintf(stderr, "[-] mmap 地址未对齐 2MB 边界: %p\n", ptr);
        return false;
    }

    // 3. 显式告知内核:仅针对该段内存启用透明大页映射
    if (madvise(ptr, total_size, MADV_HUGEPAGE) != 0) {
        perror("madvise MADV_HUGEPAGE failed");
        munmap(ptr, total_size);
        return false;
    }

    // 4. 预先触碰所有大页(Prefaulting),将缺页时机固定在初始化阶段
    uint8_t *byte_ptr = (uint8_t *)ptr;
    for (size_t i = 0; i < total_size; i += HUGEPAGE_2MB_SIZE) {
        byte_ptr[i] = 0; // 触发 PMD 大页故障分配
    }

    block->raw_ptr = ptr;
    block->size = total_size;
    block->is_huge = true;
    return true;
}

void free_precision_huge_block(PrecisionMemoryBlock *block) {
    if (block && block->raw_ptr) {
        munmap(block->raw_ptr, block->size);
        block->raw_ptr = nullptr;
        block->size = 0;
        block->is_huge = false;
    }
}

int main(void) {
    PrecisionMemoryBlock tensor_buffer = {};
    printf("[*] 正在为核心张量缓冲区申请 64MB 显式控制大页...\n");

    if (!allocate_precision_huge_block(&tensor_buffer, 32)) {
        fprintf(stderr, "[-] 大页内存分配失败!\n");
        return EXIT_FAILURE;
    }

    printf("[+] 大页内存分配就绪: 地址=%p, 尺寸=%zu 字节\n",
           tensor_buffer.raw_ptr, tensor_buffer.size);

    // 检查 /proc/self/smaps 查看 AnonHugePages 确认实际落盘大页
    printf("[*] 可执行: grep -A 20 -m 1 'AnonHugePages' /proc/%d/smaps 验证大页生效情况\n", getpid());

    free_precision_huge_block(&tensor_buffer);
    return EXIT_SUCCESS;
}

生产治理底线与监控指标

上线该方案后,系统监控平台应当设立专门的 eBPF 探针或指标采集器,紧盯 /proc/vmstat 中的两个核心指标:

  • compact_stall:系统由于直接紧缩导致进程停顿的累计次数。该值在高并发服务中必须绝对等于 0。
  • thp_fault_fallback:内核尝试分配 2MB 大页失败降级为 4KB 的次数。若该指标异常暴增,表明系统连续物理内存碎片极其严重,应提前规划内存节点回收或调整 min_free_kbytes。

大页是榨干现代硬件 TLB 潜能的绝佳利器,但只有将控制权收归工程师手中,摒弃黑盒化的全局全自动 THP,才能真正在获得高吞吐的同时,彻底粉碎几百毫秒不可控系统毛刺的梦魇。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/jiang_style/article/details/168454378

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--