# Linux 内核内存管理深度实战:Buddy System 与 SLAB 分配器全解析 ## 引言 Linux 内核的内存管理是操作系统中最复杂且最核心的子系统之一。它不仅负责物理内存的分配与回收,还要在性能、碎片、延迟之间做出精妙的平衡。本文将从 Buddy System(伙伴系统)的底层原理出发,深入剖析 SLAB/SLUB/SLOB 分配器的设计哲学,最终给出在生产环境中诊断内存问题的完整实战方案。 ## 一、Linux 内核内存管理架构总览 ### 1.1 三级内存视图 Linux 内核看待内存有三个层次,自上而下: ``` ┌──────────────────────────────────────────┐ │ 用户空间 Virtual Address │ malloc / mmap ├──────────────────────────────────────────┤ │ 内核空间 VMALLOC Region │ vmalloc() ├──────────────────────────────────────────┤ │ 内核空间 LOWMEM (Direct Map) │ kmalloc() ← 物理连续 ├──────────────────────────────────────────┤ │ Buddy System (Physical Frame) │ 页框分配 └──────────────────────────────────────────┘ ``` - **物理层**:Buddy System 以页(Page,通常 4KB)为单位管理物理内存 - **对象层**:SLAB 分配器在页的基础上缓存内核对象,避免频繁申请释放页 - **虚拟层**:页表将内核/用户虚拟地址翻译为物理地址 ### 1.2 关键数据结构 ```c // 每个物理页的元数据 struct page { unsigned long flags; // PG_locked, PG_slab 等标志 atomic_t _count; // 引用计数 atomic_t _mapcount; // 映射到多少个页表 unsigned long private; // 可供各子系统自由使用 struct address_space *mapping; struct list_head lru; // LRU 链表节点 unsigned long index; struct { struct page *next; // 伙伴系统空闲链表 int pages; // 伙伴系统阶数 }; }; ``` ### 1.3 NUMA 架构下的内存组织 在 NUMA 系统中,内存被划分为多个 Node,每个 Node 包含若干 Zone: ``` Node 0 Node 1 ├── ZONE_DMA (0-16MB) ├── ZONE_DMA ├── ZONE_DMA32 (16M-4G) ├── ZONE_DMA32 ├── ZONE_NORMAL (映射区) ├── ZONE_NORMAL └── ZONE_HIGHMEM └── ZONE_HIGHMEM ``` 内核优先从当前 CPU 所在 Node 的 Zone 分配内存,减少跨 Node 访问延迟。 ## 二、Buddy System(伙伴系统)深度剖析 ### 2.1 核心思想 Buddy System 由 Knowlton 于 1965 年提出,Linux 内核自 1.0 版本即采用。其核心规则: 1. 将物理内存划分为连续的 2^n 页大小的块(n = 0 ~ MAX_ORDER-1) 2. 每个阶 n 维护一个独立的空闲块链表 free_area[n] 3. 分配时向上取整到最近的 2^n,若该阶无空闲则从更高阶拆分为两个"伙伴" 4. 释放时检查伙伴是否空闲,若空闲则合并为 2^(n+1) 块 ### 2.2 源码级分析:分配路径 ```c // mm/page_alloc.c struct page *alloc_pages(gfp_t gfp_mask, unsigned int order) { // 1. 从指定 migratetype 的空闲链表中查找 page = get_page_from_freelist(gfp_mask, order, zonelist); // 2. 若失败,从更高阶分割(expand) if (!page) page = rmqueue(zone, order, migratetype); return page; } static struct page * rmqueue(struct zone *zone, unsigned int order, int migratetype) { for (current_order = order; current_order < MAX_ORDER; ++current_order) { area = &(zone->free_area[current_order]); if (!list_empty(&area->free_list[migratetype])) goto found; } return NULL; // 所有高阶也无空闲 found: page = list_entry(area->free_list[migratetype].next, struct page, lru); list_del(&page->lru); // 从空闲链表移除 __mod_zone_page_state(zone, NR_FREE_PAGES, -(1UL << current_order)); expand(zone, page, order, current_order, migratetype); // 分割 return page; } ``` ### 2.3 伙伴的判定与合并 两个块互为"伙伴"的条件: ```c static inline int page_is_buddy(struct page *buddy, struct page *page, unsigned int order) { // 地址相邻,且大小相同,且在同一 zone if (page_zone_id(buddy) != page_zone_id(page)) return 0; // buddy 的起始地址与 page 的起始地址相差 exactly 2^(order+1) 页 // 即:buddy_pfn == page_pfn XOR (1 << order) return (page_pfn(buddy) ^ page_pfn(page)) == (1 << order); } // 合并时:两个阶为 order 的伙伴合并为一个阶为 order+1 的块 static inline struct page * __find_buddy_page(struct page *page, unsigned long page_idx, unsigned int order) { unsigned long buddy_idx = page_idx ^ (1 << order); return page + (buddy_idx - page_idx); } ``` ### 2.4 迁移类型(Movable)与反碎片 Linux 2.6.24 引入 MIGRATE 类型,将页分为: | 类型 | 说明 | 特点 | |------|------|------| | MIGRATE_UNMOVABLE | 内核数据、页表等不可移动 | 无法迁移,易造成碎片 | | MIGRATE_RECLAIMABLE | inode cache、dentry cache | 可回收 | | MIGRATE_MOVABLE | 用户页、可移动内核页 | 可迁移至其他块 | | MIGRATE_ISOLATE | 用于CMA/热插拔 | 隔离区 | | MIGRATE_CMA | 连续内存分配器 | 大页预留 | 通过将 MOVABLE 和 UNMOVABLE 分离开来分配,确保可迁移页可以聚集到一方,另一方留给不可移动对象,从根本上延缓外部碎片化。 ### 2.5 直接内存回收与碎片整理 当 Buddy System 无法满足分配请求时的处理流程: ``` 分配请求 │ ▼ alloc_pages ──→ 空闲页足够?──是──→ 直接分配 │ │ │ 否 │ ▼ │ __alloc_pages_slowpath │ │ │ ┌────────┼──────────┐ │ ▼ ▼ ▼ │ 页面回收 页面紧凑 OOM Killer │ (kswapd/ (page (杀死 │ direct migration) 进程) │ reclaim) │ ▼ 返回页面 ``` ## 三、SLAB/SLUB/SLOB 分配器详解 ### 3.1 为什么需要 SLAB Buddy System 以整页(4KB)为单位分配,但内核对象通常只有几百字节。频繁的小对象分配: - 严重浪费内存(内部碎片) - 频繁申请/释放导致性能下降 - 缺少 CPU Cache 热度保持 SLAB 分配器(Jeff Bonwick, 1994, Solaris)通过在页内缓存对象解决这些问题。 ### 3.2 SLAB 核心概念 ``` ┌─────────────────────────────────────────┐ │ SLAB Cache (如 dentry_cache) │ │ ┌──────────┐ ┌──────────┐ │ │ │ Full SLAB │ Partial SLAB│ Empty SLAB│ │ └──────────┘ └──────────┘ │ │ │ │ SLAB (一页或多页) │ │ ┌──────────────────────────────────┐ │ │ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │ │ │ │obj1│ │obj2│ │obj3│ │obj4│ ...│ │ │ │ └────┘ └────┘ └────┘ └────┘ │ │ │ │ bufctl 位图 (INUSE/FREE) │ │ │ └──────────────────────────────────┘ │ └─────────────────────────────────────────┘ ``` - **Cache**:一组相同大小的对象池(如 `dentry_cache`、`inode_cache`) - **SLAB**:一个或多个连续的物理页,被切分成若干等大小槽位 - **bufctl**:每个槽位的使用状态位图(传统 SLAB),或 SLUB 用 `freelist` 链表 ### 3.3 SLUB:现代的默认分配器 SLUB (Unqueued SLAB, Christoph Lameter, 2007) 是 Linux 默认的 SLAB 实现,解决了传统 SLAB 在 NUMA 和大规模多核系统中的扩展性问题。 ```c // SLUB 的核心结构 struct kmem_cache { struct kmem_cache_cpu __percpu *cpu_slab; // CPU 本地缓存 unsigned long flags; unsigned long min_partial; // 保持的最少 partial slab 数 size_t size; // 对象实际大小 size_t object_size; // 用户请求大小 unsigned int offset; // freelist 在对象内的偏移 struct kmem_cache_node *node[MAX_NUMNODES]; // 节点级 partial 链表 }; struct kmem_cache_cpu { void **freelist; // 本地可用对象链表 struct page *page; // 当前使用的 slab 页 struct page *partial; // 本地 partial slabs (CONFIG_SLUB_CPU_PARTIAL) }; ``` SLUB 的关键优化: | 优化点 | 说明 | |--------|------| | CPU Freelist | 无锁快速路径,直接从 CPU 本地链表分配 | | CPU Partial | 释放到 CPU 本地 partial,避免全局锁竞争 | | Debug | 红区(Red Zone)、对象毒化(Poison)、Use-after-free 检测 | | 合并 | 相似大小的 Cache 合并,减少对象种类 | ### 3.4 SLOB 分配器 SLOB(Simple List Of Blocks)是嵌入式系统的轻量级选择,特点: - 代码极小(约 1000 行) - 只维护 3 个大小等级的链表(2^n + small extras) - 内存碎片严重,不适用于大内存系统 - 仅在内核配置 `CONFIG_SLOB` 时使用 ### 3.5 kmalloc vs vmalloc 的选择 ```c // kmalloc: 物理连续,GFP 标志灵活,适合 DMA void *kmalloc(size_t size, gfp_t flags); // vmalloc: 虚拟连续(不一定物理连续),只需页数,适合大内存 void *vmalloc(unsigned long size); ``` 关键区别: | 特性 | kmalloc | vmalloc | |------|---------|---------| | 物理连续性 | ✅ 必须连续 | ❌ 仅虚拟连续 | | 最大大小 | ~4MB (依赖 MAX_ORDER) | 接近系统总内存 | | TLB 开销 | 低(连续页框) | 高(逐页映射) | | 延迟 | ~几十 ns | ~几百 ns | | DMA 可用 | ✅ | ❌ | | vmalloc 区域 | 不占用 | 占用 vmalloc region | **实战原则**:除非需要分配超出连续物理内存能力的大块数据,否则一律使用 `kmalloc`。 ## 四、内存分配 GFP 标志详解 ### 4.1 分类与常用组合 ```c // 区域修饰符 #define GFP_DMA __GFP_DMA // ZONE_DMA 分配 #define GFP_DMA32 __GFP_DMA32 // ZONE_DMA32 分配 #define GFP_NORMAL // 默认,优先 LOWMEM #define GFP_HIGH __GFP_HIGHMEM // 可分配 HIGHMEM // 行为标志 #define GFP_ATOMIC __GFP_HIGH // 绝不睡眠,可访问紧急储备 #define GFP_NOWAIT __GFP_NOWARN // 不回收,不睡眠 #define GFP_NOIO __GFP_IO // 不启动磁盘 I/O #define GFP_NOFS __GFP_FS // 不启动文件系统操作 #define GFP_KERNEL (__GFP_RECLAIM) // 正常内核分配,可睡眠 #define GFP_USER // 用户空间分配 #define GFP_NORETRY __GFP_NORETRY // 失败直接返回,不重试 ``` ### 4.2 实战选择原则 | 场景 | 推荐 GFP | 原因 | |------|----------|------| | 中断上下文 | GFP_ATOMIC | 不能睡眠 | | 持有自旋锁 | GFP_ATOMIC | 不能睡眠 | | 文件系统写回 | GFP_NOIO | 避免递归 I/O | | 普通内核路径 | GFP_KERNEL | 允许回收和睡眠 | | 用户空间 | GFP_USER | 允许直接回收 | | 分配超过 __MAX_ORDER | GFP_NOWAIT + 重试 | 避免 OOM | ## 五、页面回收与 Swap 机制 ### 5.1 LRU 链表机制 Linux LRU 采用双时钟算法,分为两组: - **Active 链表**:最近被访问过,短期内可能被再次使用 - **Inactive 链表**:长时间未被访问,候选回收 ``` 页面被访问 ──→ Active LRU (头部) │ ▼ (到达尾部,refault 计数) Inactive LRU (头部) │ ▼ (到达尾部) 回收 (写入 swap/丢弃) ``` ### 5.2 kswapd 与 Direct Reclaim ``` 空闲内存 < free_low 空闲内存 < free_min │ │ ▼ ▼ 轻微回收 Direct Reclaim (kswapd 异步) (分配者同步阻塞) │ ▼ 仍不足? │ ┌───────┴───────┐ ▼ ▼ 触发 compaction OOM Killer ``` ### 5.3 Swap 调优实战 ```bash # 查看当前 swap 使用 cat /proc/meminfo | grep Swap # 调整 swappiness(0-100) # 0:尽可能不换出(数据库推荐 1-10) # 60:默认 # 100:激进换出 sysctl vm.swappiness=10 # 观察回收行为 vmstat 1 # si: swap in, so: swap out # 如果 si/so 持续非零,说明内存压力严重 ``` ## 六、OOM Killer 机制与调优 ### 6.1 OOM 触发与评分 当所有内存分配尝试(包括 Direct Reclaim 和 Compaction)均失败且无可用内存时,OOM Killer 被调用。 OOM Killer 为每个进程计算 `oom_score`: ``` oom_score ≈ (进程占用内存 / 系统总内存) × 1000 ``` 评分越高,被 kill 概率越大。调整方式: ```bash # 查看当前进程的 OOM 评分 cat /proc/self/oom_score cat /proc/self/oom_score_adj # -1000 到 1000 # 保护关键进程(调整 -1000 表示绝不被 kill) echo -1000 > /proc/$(pidof mysqld)/oom_score_adj # 指定高优先级被 kill echo 1000 > /proc/$(pidof mem-hogger)/oom_score_adj ``` ### 6.2 OOM 事件日志分析 ```bash dmesg | grep -i "out of memory\|oom\|killed" # 或 journalctl -k | grep -i oom ``` 典型 OOM 日志包含: - 触发进程及 GFP 标志 - 各 Node/Zone 的空闲内存 - 被选中 kill 进程的 OOM score - 内存快照 ## 七、CMA (Contiguous Memory Allocator) 与大页 ### 7.1 CMA:可迁移的连续内存池 嵌入式设备(GPU、视频编解码)需要大块连续物理内存。CMA 的巧妙之处在于: - 预留一块物理内存区域(通过 DTB 指定) - 可由 MOVABLE 页临时借用 - 需要时通过 `migrate_pages` 迁移走借用者,归还连续块 ```bash # 查看 CMA 区域信息 cat /proc/meminfo | grep Cma # CmaTotal: 262144 kB # 总计预留 # CmaFree: 204800 kB # 当前可用连续 ``` ### 7.2 大页 (Huge Page / Transparent Huge Page) 标准页 4KB,大页 2MB(x86-64)或 1GB,可大幅减少 TLB 未命中: ```bash # 透明大页(THP)配置 cat /sys/kernel/mm/transparent_hugepage/enabled # always / madvise / never # 数据库通常建议关闭 THP(因为随机大页拆分延迟不可控) echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defrag # 静态大页(Hugetlbfs)配置 echo 20 > /proc/sys/vm/nr_hugepages # 同时挂载 hugetlbfs mount -t hugetlbfs none /mnt/hugetlb ``` ## 八、内存泄漏诊断与实战 ### 8.1 常用观测工具链 ```bash # 1. 获取系统内存总览 free -h cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable|Buffers|Cached|Slab" # 2. Buddy System 状态(判断外部碎片) cat /proc/buddyinfo # Node 0, zone Normal 10 8 4 2 1 1 0 0 0 0 0 # 阶数 0 1 2 3 4 5 6 7 8 9 10 # 如果高阶全为 0 而低阶有值,严重外部碎片 # 3. SLAB 分配器状态 / 发现持续增长的缓存 cat /proc/slabinfo | head -30 # 4. 每进程内存 top -p # 或 cat /proc//smaps | grep -E "^(Rss|Private|Size)" | awk '{sum+=$2} END {print sum/1024 " MB"}' # 5. vmstat 实时观察 vmstat 1 5 # r: 运行队列, b: 阻塞队列 # swpd: swap使用, free: 空闲内存 # si/so: swap in/out (关键指标!) # bi/bo: block I/O ``` ### 8.2 SLAB 缓存泄漏排查 ```bash # 安装 slabtop(实时监控) apt install procps slabtop -o # 找出持续增长的缓存(如 dentry、inode 等) watch -n 1 'cat /proc/slabinfo | head -50' # 使用 slab 追踪(需 CONFIG_SLUB_DEBUG) echo 1 > /sys/kernel/slab//trace # 通过 kmemleak 探测内核内存泄漏 echo scan > /sys/kernel/debug/kmemleak cat /sys/kernel/debug/kmemleak ``` ### 8.3 ftrace / perf 内存分析 ```bash # 追踪分配热点 perf record -e kmem:kmalloc -ag -p perf report # 或使用 bpftrace 实时统计(推荐) bpftrace -e 'kprobe:__kmalloc { @[comm, args->size] = count(); }' # 输出每秒各进程按大小段的分配频率 # 追踪页面回收速率 bpftrace -e 'kprobe:shrink_node { @[comm] = count(); }' ``` ### 8.4 OOM 前兆告警规则 ```yaml # Prometheus 告警配置示例 groups: - name: memory rules: - alert: HighMemoryUsage expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes > 0.9 for: 5m labels: severity: warning - alert: HighSlabUsage expr: node_memory_Slab_bytes / node_memory_MemTotal_bytes > 0.3 for: 10m labels: severity: warning annotations: summary: "Slab 占用超过 30%" - alert: SwapThrashing expr: rate(node_vmstat_pswpin[5m]) + rate(node_vmstat_pswpout[5m]) > 100 for: 2m labels: severity: critical ``` ## 九、生产环境调优实践与案例 ### 9.1 关键内核参数速查表 ```bash # ===== 页面回收策略 ===== # vm.swappiness: 值越低越倾向回收 page cache 而非 swap 匿名页 vm.swappiness = 10 # 数据库宿主机建议 1-10 vm.swappiness = 60 # 默认 # vm.dirty_ratio: 脏页达到总内存此比例时,进程被强制写回 vm.dirty_ratio = 40 # 默认 40 # vm.dirty_background_ratio: 后台 kthread 开始写回的阈值 vm.dirty_background_ratio = 10 # vm.min_free_kbytes: 保留的最小空闲内存(紧急分配储备) vm.min_free_kbytes = 262144 # 256MB,大内存机器按需增大 # ===== 内存超额分配策略 ===== # vm.overcommit_memory: 0=启发式,1=总是允许,2=严格拒绝 vm.overcommit_memory = 0 # 默认 # vm.overcommit_ratio: 可超额分配的比例(当 overcommit_memory=2 时) vm.overcommit_ratio = 50 # ===== 同恶性/碎片控制 ===== # 页面紧凑阈值(compact_memory 由外部触发) /proc/sys/vm/compact_memory = 1 # 透明大页 /sys/kernel/mm/transparent_hugepage/enabled = madvise /sys/kernel/mm/transparent_hugepage/defrag = defer+madvise ``` ### 9.2 案例:数据库服务的内存调优 场景:宿主机 64GB RAM,运行 PostgreSQL,且与其他服务共享。 ```bash # 1. 关闭透明大页(避免大页拆分导致延迟毛刺) echo never > /sys/kernel/mm/transparent_hugepage/enabled # 2. 设置 swappiness = 1,仍允许最后手段换出 sysctl vm.swappiness=1 # 3. 增大脏页阈值,减少 IO 抖动但增加崩溃丢失 sysctl vm.dirty_ratio=30 sysctl vm.dirty_background_ratio=5 # 4. 提升 min_free_kbytes,保证紧急情况有足够储备 sysctl vm.min_free_kbytes=2097152 # 2GB # 5. 监控 Slab 中的 dentry/inode 缓存,避免内存被缓存占满 # 主动缩小缓存(临时) echo 2 > /proc/sys/vm/drop_caches # 仅 pagecache echo 3 > /proc/sys/vm/drop_caches # pagecache + slab ``` ### 9.3 案例:容器化环境的 cgroup 内存限制 在容器中(Docker/K8s),内存限制通过 cgroup 实现: ```bash # 容器的内存子系统 cat /sys/fs/cgroup/memory/memory.limit_in_bytes # 硬限制(触发 OOM) cat /sys/fs/cgroup/memory/memory.soft_limit_in_bytes # 软限制(尽力满足) cat /sys/fs/cgroup/memory/memory.usage_in_bytes # 当前使用量 cat /sys/fs/cgroup/memory/memory.stat # 详细统计 # 容器内的 OOM 行为与宿主机不同: # 1. 容器限制内的 OOM 不会触发宿主机 OOM Killer # 2. 请求超过容器限制会在分配时直接失败(非 kill) ``` 关键指标:`total_cache`(page cache)、`total_rss`(匿名页+共享内存)、`total_slab_reclaimable`(可回收 slab)。 ## 十、新一代追踪技术:eBPF 内存分析 ### 10.1 BPF 工具链用于内存监控 ```bash # 使用 bcc 工具包 # 1. 观察虚拟内存分配路径 biolatency -m # 查看内存回收延迟 # 2. OOM 事件追踪 oomkill # BCC 工具,实时打印 OOM 事件 # 3. Page Fault 分析 ex top: ex LAT # 页面故障延迟 ex 或 # 使用 fault.sh ``` ### 10.2 bpftrace 实时内存热力图 ```bpftrace #!/usr/bin/env bpftrace // 按 IP 和大小统计 kmalloc 分配 kprobe:__kmalloc { @alloc_size_hist[comm, (uint64)args->size] = count(); } // 追踪 vmalloc 大分配(>8KB) kprobe:__vmalloc_node_range { $size = args->size; if ($size > 8192) { time("%H:%M:%S "); printf("vmalloc %s size=%d\n", comm, $size); } } ``` 通过 eBPF,可以在不重新编译内核、几乎零开销的情况下实时观测: - 各进程按大小段的分配频率 - 某段代码的累计分配量 - 页面回收的触发者和回收速率 - OOM 前完整的内存分配栈回溯 ## 总结 Linux 内核内存管理是一个分层的精密系统: 1. **Buddy System** 负责以页为单位的物理内存分配,通过伙伴合并/分裂、迁移类型、页面压缩来对抗碎片 2. **SLUB 分配器** 在页之上构建对象缓存,通过 CPU 本地 freelist 实现无锁快速路径 3. **页面回收与 Swap** 用双时钟 LRU 算法维护内存活性,kswapd 后台回收与直回回收协同工作 4. **CMA 与 Huge Page** 满足大块连续内存的特种需求 在生产实践中,要做好内存管理需要: - 持续监控 `/proc/buddyinfo`(碎片)和 `/proc/slabinfo`(内核对象泄漏) - 根据业务类型调整 `swappiness`、`dirty_ratio`、`min_free_kbytes` - 用 eBPF 工具进行分配路径的实时追踪 - 为关键进程设置 `oom_score_adj` 避免被误杀 理解这套机制,是做好高并发、低延迟服务运维的基础。
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
/* 跳过导航链接 (无障碍) */ position: absolute; top: -100px; left: 15px; z-index: 99999; padding: 8px 16px; background: #007bff; color: #fff; font-size: 14px; border-radius: 0 0 4px 4px; text-decoration: none; transition: top 0.2s; } top: 0; outline: 3px solid #0056b3; }