Linux内核内存管理深度实战:从Buddy System到底层分配器的完整架构剖析

Linux内核的内存管理子系统是整个操作系统中最复杂、最核心的模块之一。它不仅负责物理内存的分配与回收,还构建了虚拟内存、页面缓存、交换空间等抽象层,为上层应用提供了高效、安全的内存访问机制。本文将从内核源码级别,逐一拆解Linux内存管理的核心子系统,并结合实际案例展示如何调优与排查内存相关问题。

一、物理内存管理:Buddy System(伙伴系统)

1.1 核心思想

伙伴系统是Linux物理内存分配的基础算法。它将所有物理页面划分为11个链表(order 0 到 order 10),每个链表管理大小为 2^n 个连续页面的内存块。order 0 管理单个页面(4KB),order 10 管理 2^10 = 1024 个页面(4MB)。

当一个请求需要分配 n 个页面时,系统首先查找 order 对应的链表。如果该链表为空,则从更高一级的链表中取出一个块,将其分裂为两个"伙伴"(buddy),一半用于分配,另一半放入低一级的链表中。释放时,如果对应伙伴也是空闲的,则合并成一个更大的块。

1.2 源码分析

在内核源码 mm/page_alloc.c 中,核心分配函数是 __alloc_pages_nodemask():

struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
                                     int preferred_nid, nodemask_t *nodemask)
{
    // 1. 快速路径:尝试从per-cpu缓存中直接分配
    page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
    if (likely(page))
        goto out;
    
    // 2. 慢速路径:唤醒kswapd,尝试内存回收
    page = __alloc_pages_slowpath(alloc_gfp, order, &ac);
    
out:
    return page;
}
分配策略分为快速路径和慢速路径: - **快速路径(Fast Path)**:直接从per-cpu页面缓存(pcp)或伙伴系统空闲链表中分配,不触发任何回收操作。 - **慢速路径(Slow Path)**:当快速路径失败时,唤醒kswapd内核线程进行页面回收,必要时直接进行内存回收甚至OOM。

1.3 GFP分配标志

`gfp_t` 是分配请求中最重要的控制参数,它决定了分配器的行为边界: | 标志 | 含义 | |------|------| | `GFP_KERNEL` | 标准内核分配,可能睡眠,适用于进程上下文 | | `GFP_ATOMIC` | 原子分配,不睡眠,适用于中断上下文或持有自旋锁时 | | `GFP_DMA` | 从DMA Zone分配,确保物理地址低于16MB | | `GFP_DMA32` | 从DMA32 Zone分配,物理地址低于4GB | | `__GFP_ZERO` | 分配的内存清零 | | `__GFP_HIGHMEM` | 允许从High Memory分配(32位系统) |

1.4 Zone分配器

物理内存按区域划分(Zone),不同架构的Zone定义不同: - **ZONE_DMA**:0-16MB,用于传统ISA设备DMA - **ZONE_DMA32**:16MB-4GB,用于32位DMA设备 - **ZONE_NORMAL**:直接映射到内核线性地址空间(x86_64上896MB) - **ZONE_HIGHMEM**:动态映射(仅32位系统) - **ZONE_DEVICE**:设备映射的持久内存 在x86_64架构中,由于线性地址空间极大(128TB),ZONE_HIGHMEM基本不再使用,所有物理内存都直接映射。

二、Slab分配器:内核对象的内存管理

2.1 为什么需要Slab

伙伴系统以页(4KB)为最小分配单位,但内核中小对象(task_struct、inode、dentry等)的分配通常只有几十到几百字节。如果直接使用伙伴系统,内部碎片会非常严重。 此外,频繁创建和销毁同一类型的对象时,初始化的开销也很大。Slab分配器通过对象缓存来解决这两个问题: 1. **减少内部碎片**:将一页划分为多个小对象槽位 2. **缓存热数据**:已释放的对象保持在缓存中,下次分配无需初始化 3. **硬件缓存友好**:对象按着色(coloring)偏移排列,避免缓存行冲突

2.2 Slab的演进

Linux内核经历了三代Slab分配器: 1. **Slab**(Linux 2.2):最初的实现,SLAB,但存在缓存链表管理复杂的问题 2. **Slub**(Linux 2.6.23):目前默认的分配器,设计更简洁,调试支持更好 3. **Slob**:用于极嵌入式场景,代码极小但碎片化严重 Slub是当前的默认分配器,其核心设计理念是尽可能简化数据结构。每个Slab对应一个或多个物理页面,页面内部被均匀划分为对象槽位。

2.3 Slab分配实战

// 创建自定义缓存
struct kmem_cache *my_cache = kmem_cache_create(
    "my_struct",         // 缓存名称
    sizeof(my_struct),   // 对象大小
    0,                   // 对齐
    SLAB_HWCACHE_ALIGN,  // 硬件缓存对齐
    NULL                 // 构造函数
);

// 分配对象
my_struct *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);

// 释放对象
kmem_cache_free(my_cache, obj);

// 销毁缓存
kmem_cache_destroy(my_cache);

2.4 kmalloc与vmalloc

对于不频繁的小内存分配,内核提供了两个通用接口: **kmalloc**: - 基于Slab,返回的物理连续的内存 - 大小限制:Slub最大约8KB-16KM(取决于配置) - 返回的虚拟地址和物理地址都有线性映射 - 适用于需要DMA的场景 **vmalloc**: - 分配虚拟连续但物理不一定连续的内存 - 大小几乎无限制 - 分配开销大(需要修改页表) - 不适用于DMA场景
// kmalloc示例
void *ptr = kmalloc(1024, GFP_KERNEL);

// vmalloc示例
void *vptr = vmalloc(1024 * 1024 * 10); // 10MB

// 释放
kfree(ptr);
vfree(vptr);

三、虚拟内存管理

3.1 地址空间布局

每个进程拥有独立的虚拟地址空间。在x86_64架构中,用户态地址空间为128TB:
+-----------------------+ 0x00007FFFFFFFFFFF
|    用户栈(向下增长)   |
+-----------------------+
|         mmap区域       |
+-----------------------+
|     ↓  堆              |
+-----------------------+
|     ↑  堆              |
+-----------------------+
|       BSS段            |
+-----------------------+
|       数据段           |
+-----------------------+
|       代码段           |
+-----------------------+ 0x0000000000400000
内核空间占据高地址部分,主要包括: - **直接映射区**:物理内存的线性映射(phys_base + offset) - **vmalloc区**:vmalloc分配的虚拟连续区域 - **模块区**:内核模块加载区域 - **固定映射区**:编译时确定的固定映射

3.2 页表管理

x86_64使用4级页表: 1. **PML4**(Page Map Level 4)- CR3寄存器指向 2. **PDPT**(Page Directory Pointer Table) 3. **PD**(Page Directory) 4. **PT**(Page Table) 每个表项512个(9位索引),4KB页面(12位偏移),共48位有效地址(256TB)。 内核使用 `pgd_offset()`、`p4d_offset()`、`pud_offset()`、`pmd_offset()`、`pte_offset_map()` 逐级查表。

3.3 VMA(Virtual Memory Area)

内核用 `vm_area_struct` 结构描述一段连续的虚拟地址区域。每个进程的所有VMA通过红黑树和链表管理:
struct vm_area_struct {
    unsigned long vm_start;      // 区域起始地址
    unsigned long vm_end;        // 区域结束地址
    struct mm_struct *vm_mm;     // 所属地址空间
    pgprot_t vm_page_prot;       // 访问权限
    unsigned long vm_flags;      // 标志(READ/WRITE/EXEC/SHARED)
    const struct vm_operations_struct *vm_ops;  // 操作函数集
    // ...
};
关键操作: - `do_mmap()`:创建新的VMA(mmap系统调用) - `do_munmap()`:销毁VMA - `find_vma()`:查找包含某地址的VMA

3.4 缺页中断(Page Fault)

当访问的虚拟地址尚未映射物理页面时,触发缺页中断,处理流程为:
CPU访问虚拟地址 → 查页表发现PTE为空
    → 触发#PF异常 → do_page_fault()
        → 查找VMA → 检查权限
            → 匿名映射:分配零页面(Copy-On-Write)
            → 文件映射:从磁盘读入页面(page cache)
            → 访问未映射地址:发送SIGSEGV
            → 写时复制(COW):复制页面并标记为可写
Copy-On-Write(COW)是fork()高效实现的关键。fork时子进程共享父进程的所有页面,且都标记为只读。当任一进程尝试写入时,触发缺页中断,内核才实际复制页面。

四、页面缓存与回写机制

4.1 Page Cache

Page Cache是Linux内存管理的核心缓存层,用于缓存磁盘文件的内容。所有的文件系统读写(通过read/write系统调用)都会经过Page Cache: - 读操作时,先从Page Cache查找,未命中则从磁盘读入 - 写操作时,写入Page Cache(异步模式),标记页面为"脏页"

4.2 脏页回写

脏页不会立即写回磁盘,而是在以下条件触发: 1. **定时回写**:wb_workfn内核线程每`dirty_writeback_centisecs`(默认500个百分之一秒=5秒)唤醒一次 2. **比例回写**:当脏页比例达到`dirty_background_ratio`(默认10%)或`dirty_ratio`(默认20%)时,阻塞写入并强制回写 3. **显式同步**:调用sync()、fsync()、fdatasync()
# 查看和设置回写参数
cat /proc/sys/vm/dirty_background_ratio        # 默认10
cat /proc/sys/vm/dirty_ratio                   # 默认20
cat /proc/sys/vm/dirty_writeback_centisecs     # 默认500(5秒)
cat /proc/sys/vm/dirty_expire_centisecs       # 默认3000(30秒)

4.3 Swap交换

当物理内存不足且页面无法回收时,内核会将匿名页面写入Swap分区:
用户进程访问被swap out的页面
    → 触发缺页中断 → do_swap_page()
        → 查找swap cache → 若已存在则直接映射
        → 否则从swap分区读入 → 重新建立映射
**Swappiness**控制swap的积极性(0-200,默认60): - 值越大,越倾向于swap匿名页面 - 值越小,越倾向于回收page cache - 0表示禁止swap(直到内存耗尽) - 100表示匿名页面和文件缓存平等对待

五、内存回收机制

5.1 LRU算法

Linux使用改进的LRU(Least Recently Used)算法来回收页面。从2.6内核开始,采用**活跃/非活跃双向链表**设计: - **活跃链表(Active List)**:被访问过的页面 - **非活跃链表(Inactive List)**:长时间未被访问的页面 页面在两个链表之间移动: - 首次加入page cache → 非活跃链表 - 第二次访问 → 提升到活跃链表 - 活跃页面长时间不访问 → 降级到非活跃链表 - 非活跃链表尾部页面 → 被回收

5.2 页面回收流程

内存分配失败
    → wakeup_kswapd() 唤醒回收线程
    → balance_pgdat()
        → shrink_node()
            → shrink_lruvec()
                → shrink_list():
                    shrink_active_list()   // 将活跃页面降级
                    shrink_inactive_list() // 扫描非活跃链表,回收页面
                        → 匿名页面:swap out
                        → 文件页面:若干净直接释放,若脏则回写后释放
            → shrink_slab()            // Slab缓存收缩

5.3 水位线(Watermark)

每个Zone有3个水位线,用于触发不同级别的回收: | 水位线 | 含义 | 触发行为 | |--------|------|----------| | WMARK_MIN | 最低水位线 | kswapd开始后台回收 | | WMARK_LOW | 低水位线 | 分配器进入直接回收 | | WMARK_HIGH | 高水位线 | kswapd认为Zone已平衡 | 实际水位线根据Zone大小和`min_free_kbytes`参数动态计算。

六、OOM Killer

6.1 触发机制

当系统内存极度紧张,所有回收手段都失败时,OOM Killer被触发。OOM Killer会选择一个进程"杀死"来释放内存。

6.2 评分机制

每个进程有一个oom_score(0-1000+),分数越高越容易被kill。评分考虑因素: - 内存使用量(RSS)占最大比重 - 运行时间(越新越容易被kill) - oom_score_adj(-1000到1000,-1000表示禁止kill)
# 查看进程的OOM评分
cat /proc/<pid>/oom_score
cat /proc/<pid>/oom_score_adj

保护关键进程(如数据库)

echo -1000 > /proc/<pid>/oom_score_adj

6.3 OOM处理流程

alloc_pages()失败
    → __alloc_pages_may_oom()
        → out_of_memory()
            → select_bad_process()  // 选择目标进程
                → oom_badness()      // 计算每个进程的badness值
            → oom_kill_process()
                → 发送SIGKILL信号
                → 打印OOM日志到dmesg

七、NUMA架构下的内存管理

7.1 NUMA基础

NUMA(Non-Uniform Memory Access)架构中,每个CPU节点有本地内存,访问远程节点的内存延迟更高:
Node 0: CPU[0-3] + Memory Bank 0 ──────┐
Node 1: CPU[4-7] + Memory Bank 1 ──────┤ 互连总线
关键的NUMA距离矩阵描述了节点间的访问延迟差异。

7.2 分配策略

Linux提供4种NUMA内存分配策略: 1. **Default**:优先从当前CPU所在节点分配 2. **Bind**:强制从指定节点分配 3. **Preferred**:优先指定节点,失败后fallback到其他节点 4. **Interleaved**:轮询交替分配到各节点
# 查看NUMA状态
numactl --hardcat
numastat

进程绑定NUMA节点

numactl --membind=0 --cpunodebind=0 ./my_program

7.3 NUMA Balancing

内核2.6.32引入了自动NUMA Balancing机制,定期扫描进程的内存访问模式,将远程页面迁移到本地节点: - `kernel.numa_balancing`:开关(默认1开启) - `numa_balancing_scan_delay_ms`:首次扫描延迟 - `numa_balancing_scan_period_min/max_ms`:扫描周期

八、实战:内存问题排查与调优

8.1 诊断工具

# 查看系统内存概况
free -h

查看详细的内存使用

cat /proc/meminfo

查看进程内存使用

top -o %MEM ps aux --sort=-%mem | head -20

查看进程详细内存映射

pmap -x <pid> cat /proc/<pid>/smaps

查看NUMA统计

numastat -p <pid>

查看slab缓存使用

slabtop cat /proc/slabinfo

查看Zone水位线

cat /proc/zoneinfo

查看vmalloc使用情况

cat /proc/vmallocinfo

8.2 内存泄漏排查

**使用slabtop发现异常增长的缓存:**
# 每秒刷新,按Cache Size排序
slabtop -s c -d 1
**使用vmstat观察内存趋势:**
# 每2秒输出一行,观察si/so(swap in/out)和free列
vmstat 2
**使用bpftrace追踪kmalloc调用:**
bpftrace -e 'kprobe:kmalloc { @[comm] = hist(arg0); }'

8.3 OOM案例分析

查看dmesg中的OOM日志:
[12345.678] myapp invoked oom-killer: gfp_mask=0x14200ca(GFP_HIGHUSER_MOVABLE)
[12345.678] Node 0 Normal free:12345kB min:1234kB low:2345kB high:3456kB ...
[12345.678] oom-kill: constraint=CONSTRAINT_NONE, nodemask=(null)
[12345.678] Out of memory: Killed process 12345 (myapp) total-vm:8000000kB
关键步骤: 1. 检查是否触发OOM 2. 查看min/low/high水位线 3. 看Kill了哪个进程,oom_score是多少 4. 分析该进程的内存增长趋势 5. 添加oom_score_adj保护关键服务

8.4 内存超配(Overcommit)

Linux提供3种overcommit策略: | 策略 | 说明 | |------|------| | 0 | 启发式overcommit(评估free+swap+pagecache-reclaimable) | | 1 | 永远不拒绝任何malloc(适合科学计算场景) | | 2 | strict模式,CommitLimit = SwapTotal * overcommit_ratio |
cat /proc/sys/vm/overcommit_memory        # 默认0
cat /proc/sys/vm/overcommit_ratio          # 默认50
cat /proc/sys/vm/swapiness                 # 默认60

8.5 Huge Pages调优

Huge Pages(大页)减少了页表项数量和TLB miss,对数据库等内存密集型应用帮助显著:
# 查看当前Huge Pages配置
cat /proc/meminfo | grep Huge

分配静态Huge Pages(2MB/页)

echo 256 > /proc/sys/vm/nr_hugepages

查看是否在使用

grep -i huge /proc/meminfo

透明Huge Pages(THP)

cat /sys/kernel/mm/transparent_hugepage/enabled # 建议设为madvise

九、关键参数调优速查

# /etc/sysctl.conf 推荐优化

减少swap倾向(数据库场景)

vm.swappiness = 10

脏页回写策略优化

vm.dirty_background_ratio = 5 vm.dirty_ratio = 10 vm.dirty_writeback_centisecs = 1000

内存超配策略(严格模式,适合生产)

vm.overcommit_memory = 2 vm.overcommit_ratio = 80

VFS缓存压力(回收dentry/inode积极性)

vm.vfs_cache_pressure = 50

最小保留内存(KB)

vm.min_free_kbytes = 262144 # 256MB

禁用透明Huge Pages(数据库推荐)

echo never > /sys/kernel/mm/transparent_hugepage/enabled

十、总结

Linux内核的内存管理是一个精密的分层系统:底层是伙伴系统管理物理页面,中间层是Slab分配器处理小对象,上层构建了虚拟内存、Page Cache和Swap等完整的抽象体系。每一个子系统都有其精细的数据结构和算法设计。 理解这些机制不仅有助于排查线上内存问题,更能帮助我们在系统设计和性能优化中做出正确决策。无论是选择Huge Pages减少TLB miss,还是调整NUMA策略降低远程访问延迟,亦或是配置合理的内存超配策略,都离不开对底层机制的深入理解。 在实际运维中,建议持续关注以下核心指标: - `si`/`so`(swap in/out)应接近0 - `free`保持合理水位(至少保留min_free_kbytes) - 脏页比例不超过dirty_background_ratio - 各Zone的min/low/high比值合理 - Slab缓存没有异常增长 - 无频繁OOM Killer触发
点赞(0) 打赏

评论列表 共有 0 条评论

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

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部