ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux内存分配器全景解析:从伙伴系统到malloc的性能调优实战

Linux内存分配器全景解析:从伙伴系统到malloc的性能调优实战 搞Linux这些年让我最感慨的一句话就是内存分配器是那种“看起来哪都能堵住但大多数人只有出事了才想起来看”的组件。有次线上服务延迟曲线出现锯齿状抖动CPU、IO、数据库全查了一遍都正常最后用perf抓内核态调用栈发现大量时间花在alloc_pages和内存规整上根因是物理页碎片化太严重一个高阶内存分配请求触发了一次性的碎片整理某些请求线程被卡了几十毫秒。那次之后我才认真把Linux 内存管理里的内存分配器从原理到排查工具完整梳理了一遍今天这篇就当是给自己留的笔记也分享给所有被内存问题折腾过的人。这篇文章想讲清楚一条主线从物理页到用户态malloc整条链路上到底经过哪些分配器每一层设计思路是什么出了问题怎么定位调优。具体会拆成五段先建立全局的分层认知然后讲伙伴系统的页级分配与碎片问题接着讲 Slab/Slub 在内核对象分配里的角色随后跟踪一条用户态malloc请求如何吃到物理页最后给出一套面向实战的排查路径和内核参数调优经验。适合正在看内核源码的开发者、处理线上内存事故的运维和 SRE以及准备 Linux 内核方向面试的人。这中间我会穿插不少自己踩过的坑希望能帮你少走弯路。1. 内存分配器不是“给一块内存”那么简单先从问题边界说起1.1 一个把 malloc 当“取内存”的典型误解先解决一个最常见的误区很多新人在排查内存问题时会理所当然地认为malloc(1024 * 1024)返回成功那一刻这 1MB 物理内存就已经属于这个进程了。实际上完全不是这么回事。malloc是用户态的分配器glibc 的 ptmalloc、jemalloc、tcmalloc 等提供的接口。它做的第一件事是向内核“申请”一段虚拟地址空间这段虚拟空间在不同分配器里可能是通过brk系统调用扩展堆区也可能是通过mmap匿名映射来获得。无论哪种方式内核在返回时大概率没有真正分配物理页只是在你进程的地址空间里划了一片区域并在页表里做标记告诉 MMU“这块地址现在还没有内容”。物理内存在哪个节点、哪个 zone、哪个 page frame 上是在你真正访问这块内存、触发缺页异常page fault时才确定的。这一点是整个内存分配器体系的核心前提虚拟内存是“懒”的物理内存是“按需”的。理解了这条后面很多现象就顺了为什么top里 VSZ 和 RSS 能差出好几个数量级为什么一个进程申请 10GB 内存、只写其中几个页面系统内存却不大幅波动为什么内存碎片不是简单看总容量够不够就能下结论。1.2 从物理页到用户堆这条链路上有四类分配器顺着刚才的思路一条完整的内存分配链路会经过四层角色。我列了一个表方便对照理解层级典型实现分配粒度接口/机制解决的问题物理页分配器伙伴系统 Buddy System页通常 4KB以及 2 的幂次倍页块alloc_pages/free_pages管理物理页帧的分配与回收应对碎片内核对象分配器Slab / Slub / SLOB字节到对象大小kmalloc/kmem_cache_alloc在内核里分配小对象复用构造好的对象减少初始化开销用户态内存分配器glibc malloc、jemalloc、tcmalloc字节到块malloc/free管理进程堆降低系统调用频率减少锁竞争虚拟内存区域管理VMA 缺页异常虚拟地址区域brk/mmap/ page fault把合法虚拟地址与物理页通过页表关联起来我习惯把这四层类比成仓库的物流体系伙伴系统是总仓按整板出货页Slab/Slub 是总仓门口的“拆零区”仓库按板把货发出来拆零区拆成单件商品给内核里的各种对象用用户态内存分配器是商店的货架面对成千上万个顾客业务代码按需求量拿货而 VMA 和缺页异常是订单系统记录了每个货架上的东西什么时候真正需要从总仓补货。这个分层天然带来了一个好效果上层分配器只要不频繁打破抽象就能大量复用已经拿到的内存。比如 glibc 的 tcache、per-thread arena就是尽量让你在用户态完成分配和释放而不必每次都走一次系统调用去打扰内核。这也是为什么 jemalloc 在一些高并发场景下比 glibc 默认分配器表现好——它在用户态这个层面把“零售”效率做到了极致而底层内核页分配器动得越少整体开销越低。2. 伙伴系统页级分配的核心机制与碎片问题2.1 free_area、order 和伙伴合并分配器的“账本”怎么记伙伴系统是 Linux 物理页分配的地基几乎所有分配物理页面的路径最终都会汇聚到这里包括alloc_pages、__get_free_pages、page cache分配页、用户态缺页时的物理页申请。它管理的基本单位是页帧默认一页大小通常是 4KB。为了避免频繁地按单页拆分和合并内核把连续的空闲页按照大小为 2 的幂次分成不同的 order。每个 order 维护一个链表这个链表数组叫free_area。常见的架构里MAX_ORDER默认是 11也就是说 order 范围从 0 到 10order 10 表示 (2^{10}) 页也就是 4MB 的连续块在 4KB 页大小下。代码层面看这个概念大概长这样struct free_area { struct list_head free_list[MIGRATE_TYPES]; unsigned long nr_free; }; struct zone { // ... struct free_area free_area[MAX_ORDER]; // ... };这个结构里还有一层关键变量MIGRATE_TYPES。它把空闲页按“可移动性”分成不同链表UNMOVABLE、RECLAIMABLE、MOVABLE 等这一点后面讲碎片问题时会细说。“伙伴”这个词的含义是理解这个算法的关键两个物理地址相邻、并且合并起来正好构成一个更高 order 块的页块才叫一对伙伴。更严格地说伙伴之间的关系由物理页帧号PFN和二进制的位运算决定。比如一个 order 1 的块2 页它的伙伴是另一个 order 1 的块两者的起始 PFN 满足除了倒数第 2 位以外都相同的条件合并起来就是一个 order 2 的连续 4 页。这个“物理相邻 顺序匹配”的条件决定了整个伙伴系统的分配和合并逻辑。2.2 一次实际分配和释放的拆账过程假设某个驱动调用alloc_pages想申请一个 order 1 的块2 页。内核的处理逻辑是先去zone-free_area[1]的链表里找如果有空闲块直接摘下来返回。如果 order 1 链表为空就向上找 order 2、order 3……直到某个链表有块。假设 order 3 的链表里有一个 8 页的空闲块内核会把它拆成两个 order 2 的块其中一个挂回free_area[2]继续把这个 order 2 块拆成两个 order 1 块一个挂回free_area[1]另一个作为结果返回。拆块的每一步都在维持一个约束多出来的半块要被挂到对应的 order 链表里不能凭空丢失。释放路径正好相反。释放一个 order 1 块时内核会检查它的“伙伴”是否也是空闲的。如果是就把它俩合并成 order 2再往上检查 order 2 的伙伴是否空闲继续合并最多一路合并到 MAX_ORDER。这个机制的好处是算法简单、分配释放都是常数级时间坏处是它非常依赖“伙伴”恰好空闲这件事。假如一个 order 3 块被拆开分配了一半剩下的一半又被拆开消耗了一部分而被占用或拆散的部分长时间不释放外部连续内存就会越碎越厉害。哪怕空闲内存总量足够大也可能拿不出一块满足高阶请求的大连续块这就是所谓的外部碎片问题。2.3 碎片才是真正的敌人迁移类型与内存规整碎片问题是伙伴系统最臭名昭著的副作用。这不是玄学用/proc/buddyinfo一看便知。典型输出长这样$ cat /proc/buddyinfo Node 0, zone Normal 12345 6789 4567 2345 1234 567 234 123 45 22 3 Node 0, zone Movable 0 0 0 0 0 0 0 0 0 0 0列从左到右对应 order 0 到 order 10 的空闲块数量。如果一个系统里 order 0 和 order 1 的数量很多但 order 8、order 9、order 10 长期为 0说明连续大块内存已经很难拿到了。为了缓解这个问题内核做了两件很重要的事第一件是引入迁移类型。之前提到的MIGRATE_TYPES就是把空闲页分成 UNMOVABLE内核分配的关键页不能移动、RECLAIMABLE可以回收、MOVABLE用户进程匿名页、page cache 等可以通过页迁移手段移动三类。分配时尽量从对应类型的链表里按类型取页这样做的目的是让可移动页和不可移动页尽量“物以类聚”避免不可移动页像钉子一样插在中间切碎大块连续空间。第二件是内存规整compaction。当某个高阶分配请求失败时内核会扫描 MOVABLE 类型的页把分散的可移动页内容拷贝到别处然后释放它们原来的页帧从而腾出一段连续空间。这个过程可能发生在请求路径上也可能由 kcompactd 内核线程在后台慢慢做。我在实战里遇到过一个非常典型的场景KVM 宿主机试图预留 2MB 大页结果nr_hugepages设置之后系统始终只有一小部分大页成功剩下的报错说无法分配。查/proc/buddyinfo发现高阶连续块确实不足。当时没有选择重启而是先sync echo 3 /proc/sys/vm/drop_caches清掉回收型缓存再手动触发一次内存规整大页才陆续提前成功。这说明在生产环境里物理页碎片化是真实会咬人的问题尤其是对预分配大页、DPDK 内存池这类场景特别敏感。3. Slub 取代 Slab 之后内核对象分配是怎么工作的3.1 内核里为什么要在伙伴系统之上再“零售”内存伙伴系统的粒度是页但内核里最常见的需求并不是整页整页地要内存而是频繁创建和销毁小对象比如进程控制块task_struct、文件系统的目录项dentry、索引节点inode、网络协议栈里的sk_buff。如果一个驱动每次创建一个几十字节的结构体都要向伙伴系统拿一页、用完再还回去会带来两个明显的浪费一是页内剩余空间被白白浪费二是每次创建对象都要执行昂贵的构造与析构逻辑。Slab 分配器的思路就是在伙伴系统拿到的页上再做一层“零售”从伙伴系统申请一个或几个连续物理页作为一个 slab。把这个 slab 按对象大小分成等大的槽位。缓存里会维护空闲对象链表分配时取一个槽位释放时把槽位放回去。对象构造函数只在首次初始化时执行之后复用对象时就不需要重复构造。这段代码来自一个典型的用法static struct kmem_cache *my_object_cache; static int __init my_init(void) { my_object_cache kmem_cache_create( my_object, // 缓存名会在 /proc/slabinfo 里看到 sizeof(struct my_object), // 对象大小 0, // 对齐要求0 表示默认对齐 SLAB_HWCACHE_ALIGN, // 按硬件 cache line 对齐 NULL // 构造函数 ); if (!my_object_cache) return -ENOMEM; return 0; } static struct my_object *my_alloc(void) { return kmem_cache_alloc(my_object_cache, GFP_KERNEL); } static void my_free(struct my_object *obj) { kmem_cache_free(my_object_cache, obj); }在这个例子里kmem_cache_create创建的缓存会从伙伴系统分批拿页然后在内核里切分成对象槽位。kmem_cache_alloc就是从某个 slab 上取一个空闲槽位出来。这个名字my_object会在/proc/slabinfo里显示出来如果某个缓存异常增长一眼就能看到。3.2 SLAB / SLOB / SLUB三个分配器的演进和取舍Linux 里这套机制实际有三个实现SLAB、SLOB、SLUB通过内核编译选项CONFIG_SLAB、CONFIG_SLOB、CONFIG_SLUB控制。它们虽然干同样的事设计哲学差别很大。SLAB 是老牌实现设计精巧但代码复杂早期使用大量 per-CPU 队列还引入了 cache coloring 技术来优化硬件缓存行。这种精细带来了性能收益但也让代码难以维护在 NUMA 场景下锁竞争管理尤其复杂。SLOB 是极简实现把多个小对象的分配尽可能塞进同一页适合内存资源紧张、追求低内存开销的嵌入式设备。它的问题是分配速度慢、碎片容易存在不适合通用服务器。SLUB 是 2.6.23 之后就逐步成为主力的实现。它大幅砍掉了 SLAB 里的复杂元数据把每个 slab 页上的对象列表直接放在页本身里用更简单的 freelist 维护空闲对象减少了核心路径上的锁和 cache 写竞争而且调试元数据和 sysfs 支持都做得更直接。现在的发行版内核基本都是 SLUB。如果你在/boot/config-$(uname -r)里看到CONFIG_SLUBy不用惊讶这就是现代内核的默认答案。理解这个演进对读内核源码很有帮助你真去读/mm/slub.c会发现它的主体结构比/mm/slab.c好懂得多反过来网上很多老文章讲的 slab 概念比如 cache coloring、per-CPU 部分 slab在 SLUB 里已经大幅简化了照搬老概念反而会对不上号。3.3 驱动开发里怎么用 kmem_cache 管理对象实际内核开发中如果某个驱动或子系统要频繁分配大量同类型结构体最推荐的做法就是用kmem_cache_create创建专用缓存而不是直接kmalloc。原因很朴素专用缓存的对象大小固定槽位完全复用分配逻辑经过精心优化还天然自带对齐能力。我自己写内核模块时踩过一个很实在的坑起初图省事用kmalloc分配一个 200 字节左右的结构体上线后内存占用涨得很快。后来排查发现kmalloc走的是通用大小类缓存虽然表面上也能分到 256 字节的槽位但同一个通用缓存被各种子系统共享碎片和互相干扰难以避免。改成kmem_cache_create建专用缓存后不仅分配路径更可预测/proc/slabinfo里能直接看到这个缓存用了多少对象排查和监控都变得直观。使用时有几个细节值得注意align参数传 0 时使用架构默认对齐但如果对象里有atomic64_t或sector_t这类需要特殊对齐的成员最好显式传入__alignof__(struct my_object)。构造函数ctor传入后只会在新页加入缓存时调用一次不是每次kmem_cache_alloc都调用依赖“每次分配都是干净对象”的逻辑不要放在 ctor 里。释放对象时如果对象里有指向其他内存的指针需要自行决定是否清空kmem_cache_free不会帮你清理内容。这些看起来都是小细节但在高频率分配路径上一点对齐或构造函数设计不当都会演变成诡异的性能或者数据污染问题。3.4 通过 slabtop 和 kmemleak 追查“消失的内存”线上遇到内存占用莫名其妙升高free -h显示 used 很高但用top按进程排序又找不到哪个进程吃掉大头这种时候十有八九是内核态内存slab 缓存有很大嫌疑。第一步先看slabtop$ slabtop -o Active / Total Objects (% used) : 5438210 / 5467890 (99.4%) Active / Total Slabs (% used) : 127382 / 127382 (100.0%) Active / Total Caches (% used) : 137 / 179 (76.5%) OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME 1824060 1824033 99% 0.10K 4677 390 18708K vm_area_struct 950902 950823 99% 0.11K 2774 343 11096K anon_vma ...光看数字可能没感觉重要的是关注 NAME 列里是否有某个缓存的对象数异常攀升。比如某个版本的内核在特定业务形态下会导致dentry或filp缓存无限增长长期不回收最终把内存吃满。这时候要结合业务进程行为判断是正常的缓存增长还是泄漏。如果怀疑真正的内核内存泄漏可以用kmemleak。它依赖内核配置CONFIG_DEBUG_KMEMLEAK开启后按如下方式操作# 启用扫描 echo scan /sys/kernel/debug/kmemleak # 查看报告 cat /sys/kernel/debug/kmemleak报告里会列出疑似未被释放的指针和调用栈指向具体是哪个内核模块的哪条路径创建了对象。这个工具的价值在于能把问题从“内存没了”落到“是哪个调用路径没释放”缺点是需要开启内核调试选项、性能有一定损耗生产环境适合短时间临时开启来定位不适合长驻。4. 一条 malloc 请求是怎么一步步吃到物理页的4.1 glibc 的 brk 与 mmap分配虚拟地址的两种姿势现在我们把视角切回用户态看看一次普通的malloc调用背后内核经历了什么。glibc 的 ptmalloc 判断一块请求到底走哪条路核心逻辑是大小阈值小块分配走 brkbrk系统调用本质上是调整进程堆区mm-brk的边界相当于把虚拟地址空间的堆区往高地址推。glibc 会维护一个堆管理结构把这块连续虚拟地址切成各种大小的块用空闲链表管理。分配和释放都在用户态完成只有需要扩充堆时才会调用brk进内核。大块分配走 mmap超过M_MMAP_THRESHOLD默认 128KB动态可调的分配glibc 会使用mmap创建一段匿名映射然后返回映射区域的起始地址。大块通过munmap释放时可以立刻把虚拟地址区域和物理页全部归还给系统不会和堆里的其他小块互相纠缠。要分清一个点这两条路径在内核里都只是“虚拟地址空间操作”。brk只是移动一个边界值mmap只是创建vm_area_struct并设置 VMA 属性不会真的去伙伴系统拿物理页。真正的物理分配发生在后面的缺页异常处理中。4.2 缺页异常物理内存真正落地的瞬间当进程第一次写入malloc返回的地址时CPU 去查页表发现这个虚拟地址的 PTE 没有被映射于是触发缺页异常。内核陷入缺页处理路径主要逻辑可以简化为在mm-mmap红黑树里查找出错的虚拟地址落在哪个 VMA 上。校验这个 VMA 是否允许这种访问比如可写访问一个只读映射就会触发 SIGSEGV。调用handle_mm_fault进入页表层级处理和物理页分配。最终走到alloc_pages/__alloc_pages从当前进程所在 NUMA 节点的合适 zone 里拿到一个物理页帧。设置 PTE把虚拟地址映射到刚分配的物理页必要时刷新 TLB。这也是为什么 RSS驻留内存会随着进程实际访问内存而增长而 VSZ虚拟内存大小在一开始就包含了所有申请过的虚拟区域。如果你写一个程序申请 1GB 内存却只写了前几十个页面top里 RSS 几乎不会变化这就是按需分配的直接体现。顺带一提可写私有映射还有个陷阱fork 之后父子进程会共享同一批物理页谁先写谁触发缺页复制COW。所以高密度 fork 场景下一写内存可能引发大量缺页每个缺页都从伙伴系统取新页内存分配器会在短时间被轰得很厉害。4.3 overcommit 和 OOM Killer内核为什么要“撒谎”按需分配带来一个直接结果进程可以申请远超物理内存的虚拟内存而暂时不触发错误。这就是 overcommit 机制允许的“过度承诺”。/proc/sys/vm/overcommit_memory可以取三个值值含义适用场景0启发式过度承诺内核根据明显超出的申请做限制大多数通用服务器默认值1总是允许 overcommit不检查偏内存型计算、某些数据库实例配合大页2禁止明显超物理内存的过载按比例限制严格限制内存超额、避免被 OOM 波及的持久化系统当系统内存真的不够分配时内核会尝试回收 page cache、swap如果还不够就触发 OOM Killer选择一个进程杀掉以释放内存。很多刚接触的人会愤愤不平内核凭什么乱杀进程其实这正是“过度承诺”的代价。如果内核在malloc阶段就严格执行“物理内存必须够”的规则很多内存申请比如启动时预留、稀疏数组、fork 场景会直接失败根本无法正常工作。内核选择先答应申请再在真正揭不开锅时用 OOM 手段兜底换来的是更灵活的内存使用率。生产环境里如果业务进程内存管理不稳定经常把系统拖到 OOM 边缘可以临时把overcommit_memory设为 2让内核在申请阶段就严格拦截超限请求避免大势已去之后靠杀进程来止血。但要注意设为 2 之后很多自由使用虚拟内存的程序可能直接启动失败必须结合CommitLimit做评估。4.4 大页和 THP对分配器节奏的影响缺页路径每次分配一个 4KB 页在高频访问大内存时会频繁走页表遍历和 TLB miss。大页Huge Pages的思路是提前预留 2MB 或 1GB 的连续物理页把它们映射进页表。但大页对分配器提出了一个很硬的要求2MB 连续物理内存。这正好是伙伴系统高阶分配的苦主。普通的 4KB 页只要 order 0而 2MB 大页在 4KB 基准页下需要 order 9 的块。系统长时间运行后order 9 的空闲块经常早就被耗光导致大页预分配失败。这时候可以把大页放到开机早期分配那时内存还没碎或者靠 CMA 区域规避。THP透明大页则试图让系统自动享受大页的好处。它由khugepaged内核线程在后台把一段连续虚拟地址的 4KB 页表项提升为 2MB 映射不用应用介入。听起来很美好但在延迟敏感场景下THP 的扫描、合并不但可能带来额外的内存规整开销还会在 fork 后引发更复杂的 COW 处理。我的建议很直接数据库、消息中间件这类对延迟毛刺敏感的服务一般都要显式关闭 THP改为业务层自己评估是否需要显式大页echo never /sys/kernel/mm/transparent_hugepage/enabled echo never /sys/kernel/mm/transparent_hugepage/defrag顺带说一句很多流行的数据库容器镜像在入口脚本里就会做这件事背后正是这个原因。5. 线上内存问题的排查路径与调优参数5.1 用 /proc 文件系统给内存分配器“体检”真正排查内存分配问题时我习惯从这几个 /proc 节点入手。它们能最直观地展示内核视角下的内存现状。/proc/meminfo是全局概览重点关注MemFree、MemAvailable、SReclaimable、SUnreclaim、Dirty、Writeback。其中SReclaimable和SUnreclaim就是 slab 缓存的角色一个代表可回收的比如 dentry cache一个代表不可回收的比如正在使用的对象。如果SUnreclaim长期持续走高基本可以怀疑某个内核路径发生了泄漏。/proc/buddyinfo看空闲页块分布。判断标准很简单不管你系统多大如果 order 3 以上的空闲块长期为零而MemFree又显示好多内存“闲着”那多半是碎片导致的高阶分配困难并不是真的缺内存。/proc/pagetypeinfo可以看到不同迁移类型的空闲页数量。写申请大页、DPDK 复用分配连续内存之前值得先看一眼各 zone 各迁移类型的分布提前发现碎片风险。/proc/slabinfo是 slab 内部数据的总表。每一行对应一个缓存列里有 active_objs、num_objs、objsize、objperslab 等可以精确算出某个缓存当前占用多少内存。多数情况我直接用slabtop观察排序但如果要精确追某一个驱动分配的对象数量/proc/slabinfo会直接露出马脚。再配合vmstat里的cs、us、sy和si/so可以判断有没有频繁的内核态切换或者 swap 抖动。如果si/so持续非零说明内存真的紧张分配器开始依赖换页来“续命”这种局面下谈分配器优化意义不大应该先解决容量或业务内存占用的根因。5.2 一次由内存碎片引发的分配抖动完整复盘这里我完整复述一次真实的排查过程它比较能代表内存分配器问题在线上是如何逐步暴露出来的。现象是某搜索服务在晚高峰出现周期性的延迟尖刺从 2ms 飙到 200ms持续时间几十毫秒到上百毫秒。监控显示 CPU 总体使用率没有明显变化Java 应用 GC 也正常数据库无慢查询网络 IO 没有拥塞。第一步我用perf在抖动时间窗口抓了内核态热点perf record -g -a -- sleep 30 perf report --stdio看到两块东西很扎眼一块是compact_stall相关的函数栈另一块是alloc_pages_slowpath和直接内存回收direct reclaim的栈。这说明有些内存分配请求不是从快速路径直接拿到的而是走到了慢速路径甚至触发了内存规整。第二步查/proc/buddyinfo。当时内存总量还有不少富余但 order 5 以上的空闲块数量明显偏少。结合业务里某些线程池启动时会一次性申请较大的内存块基本确认是外部碎片问题内存总容量充足但连续的物理块已经凑不齐。第三步调整参数做缓解。我们没有直接重启而是先把vm.min_free_kbytes从默认值调大保证系统里始终留有水位更高的连续空闲页专门应对高阶分配sysctl -w vm.min_free_kbytes1048576这个参数的意思是让内核在回收内存时保留至少 1GB 的物理页作为紧急预留。调大之后接收高 order 的请求更容易直接命中空闲链表而不是走到内存规整。同时把 THP 的 defrag 开关从always改为madvise避免 khugepaged 在内存紧张时强行整理页表。上线观察两周延迟尖刺基本消失。这次调整的本质不是把碎片消除了而是给碎片问题“多留了一点冗余”让分配器不必在压力阶段再做昂贵的规整动作。真正一劳永逸的方案是修改业务的分配模型比如启动时就固定申请并复用内存但作为应急手段调参已经很有效。5.3 值得关注的内核参数以及哪些情况别乱调围绕内存分配器有几个内核参数是我在实战中反复用到的列出来给你做参考参数作用我的使用建议vm.min_free_kbytes保留最少空闲页量影响水位线和紧急分配高内存机器可适当调大到总内存的 1%~3%但太大浪费内存vm.vfs_cache_pressure控制 dentry/inode 缓存回收倾向默认 100 一般别动降低到 50 以下可让文件缓存更持久但量太大可能挤压分配vm.overcommit_memoryovercommit 模式稳定业务可设为 2 并配合 CommitLimit但需先测启动vm.zone_reclaim_modeNUMA 节点本地回收策略默认 0 通常最好某些 NUMA 场景设 1 能提升本地命中但要小心节点内存不足/sys/kernel/mm/transparent_hugepage/enabledTHP 开关延迟敏感服务建议 never/sys/kernel/mm/transparent_hugepage/defragTHP 的规整触发策略建议 madvise 或 never这几个参数里vm.min_free_kbytes是最容易见效也最容易误伤的一个。调太小内存回收线程会过于激进调太大你在free -h里会看到明明内存够用却一直有大量内存被“强制保留”白白浪费。另外要特别提醒不要同时反复调整多个参数。内存分配器是全局共享的参数改动影响的是所有进程的分配路径。生产环境里我见过因为把vm.vfs_cache_pressure调到 0 导致 page cache 永不回收最后整机内存被文件缓存占满、分配器不断触发直接回收的案例。调参前先明确这次要解决什么问题调完后观察vmstat、/proc/buddyinfo和业务延迟三个维度对照确认效果再考虑下一步。5.4 面试被问内存分配器时建议这样组织回答这个话题也经常出现在 Linux 工程师面试里。常见问题包括伙伴系统是怎么分配和回收的Slab 和 Slub 有什么区别malloc 和内核分配有什么关系为什么会有内存碎片被问到的时候我建议按“分层 机制 现象”的结构回答。先说明 Linux 物理内存分配有页级分配器和对象级分配器两层页级即伙伴系统管理顺序和 order对象级主要是 Slub提供对象缓存和普通kmalloc。然后讲一条malloc到物理页的路径虚拟地址申请、缺页异常、alloc_pages。最后结合实际现象补一句碎片问题的本质是伙伴关系断裂缓解手段包括迁移类型分类和内存规整。这样组织既不会浅尝辄止也不会因为过度抠细节把自己绕进去。面试官往往更在意你是不是真正理解整条链路的层次和取舍而不是背几个结构体名字。如果真要往深了学我最推荐的方式不是从头到尾读一遍源码而是带着问题去读先看/Documentation/vm/下的文档再通过perf和 tracepoint 跟踪一次真实分配路径比如用trace-cmd record -e kmem:kmalloc抓一次kmalloc事件看它从哪个缓存分配、调用栈长什么样。有了直观感受后再去/mm/page_alloc.c里顺着__alloc_pages往下读会清晰得多。我自己的体会是内存分配器这种涉及大量并发和 NUMA 细节的组件纯脑子推演很容易错工具配合源码才是最快的路径。
返回列表