内存管理的核心地位

Linux内核的内存管理子系统是整个操作系统中最复杂、最核心的组件之一。它不仅负责物理内存的分配与回收,还实现了虚拟内存、页面交换、内存映射、SLAB/SLUB分配器、NUMA感知分配、内存压缩、KSM(内核同页合并)等一系列高级机制。理解这些机制,是深入Linux系统性能优化和内核开发的关键。

虚拟内存与物理内存架构

Linux采用虚拟内存机制为每个进程提供独立的4字节虚拟地址空间(32位系统)或128TB地址空间(x86_64)。内核将虚拟地址空间划分为用户空间和内核空间:用户空间从0到TASK_SIZE,内核空间从TASK_SIZE到2^64。

x86_64架构使用4级页表结构(PGD→PUD→PMD→PTE),每级页表占用9位索引。从Linux 5.14开始,x86_64支持5级页表(增加P4D级别),将虚拟地址空间从256TB扩展到128PB。

// x86_64四级页表地址分解(48位有效虚拟地址)
// 47        39 38        30 29        21 20         12 11       0
// | PGD index | PUD index | PMD index | PTE index | page offset |
//    9 bits      9 bits      9 bits      9 bits     12 bits

// 五级页表(57位虚拟地址)
// 56        48 47        39 38        30 29        21 20         12 11      0
// |P4D index| PGD index | PUD index | PMD index | PTE index | page offset |
//   9 bits    9 bits      9 bits      9 bits      9 bits     12 bits

Buddy System(伙伴系统)

伙伴系统是内核物理内存管理的基础算法。它将所有空闲页面按2的幂次方分组,维护11个空闲链表(order 0到order 10),分别管理1、2、4、8...1024个连续页面的块。

分配时,如果请求的order链表为空,就从更高一级的order链表中取出一个块,分裂成两个"伙伴"块,一半放入请求的order链表,另一半用于分配。释放时,如果对应的伙伴块也是空闲的,则合并成更大的块,向上级order传递。

// 伙伴系统核心结构(mm/page_alloc.c)
struct zone {
    free_area    free_area[MAX_ORDER];  // 各阶空闲区域
    ...
};

struct free_area {
    struct list_head free_list[MIGRATE_TYPES];  // 按迁移类型分组
    unsigned long    nr_free;                    // 空闲页面数量
};

// 分配核心:__alloc_pages_nodemask()
// 1. 使用快速路径(get_page_from_freelist)尝试直接分配
// 2. 快速路径失败则进入慢速路径(__alloc_pages_slowpath)
// 3. 慢路径触发内存回收(pageout)、压缩内存、甚至OOM killer

SLUB分配器

SLUB(the unqueued slab allocator)是Linux内核默认的小对象内存分配器,用于分配小于一个页面大小的内核对象(如task_struct、inode、dentry等)。SLUB在Buddy System之上构建了一个缓存层,减少内存碎片并提升小对象分配效率。

// SLUB核心数据结构(mm/slub.c)
struct kmem_cache {
    struct kmem_cache_cpu *cpu_slab;    // 每CPU本地缓存(快速路径)
    struct kmem_cache_node *node[MAX_NUMNODES]; // 每节点管理
    unsigned int size;                  // 对象大小
    unsigned int object_size;           // 原始对象大小
    unsigned long flags;                // 创建标志
    char name[32];                      // 缓存名称
    ...
};

struct kmem_cache_cpu {
    void **freelist;    // 空闲对象链表
    struct page *page;  // 当前使用的slab页
    struct page *partial; // 部分空闲slab
};

// 分配流程:
// 1. 优先从CPU本地缓存(freelist)获取 → 无锁快速路径
// 2. CPU缓存空时,从node的partial slab补充
// 3. partial也空时,向Buddy System申请新页面 → 慢速路径

页面回收与交换(Swap)

当系统内存紧张时,内核通过页面回收机制释放内存。核心算法是LRU(Least Recently Used)双链表算法,维护Active和Inactive两条链表。

// LRU页面回收核心(mm/vmscan.c)
enum lru_lists {
    LRU_INACTIVE_ANON,   // 非活跃匿名页
    LRU_ACTIVE_ANON,     // 活跃匿名页
    LRU_INACTIVE_FILE,   // 非活跃文件页
    LRU_ACTIVE_FILE,     // 活跃文件页
    LRU_UNEVICTABLE,     // 不可驱逐页
    NR_LRU_LISTS
};

// kswapd后台回收:
// 1. 当空闲页面低于WMARK_LOW阈值时,kswapd被唤醒
// 2. 从Inactive链表尾部扫描页面,Active链表中的页面经过两轮访问后降级
// 3. 匿名页写入swap分区(交换),文件页丢弃或回写
// 4. 直到空闲页面恢复到WMARK_HIGH阈值

// 直接回收(Direct Reclaim):
// 当进程分配内存失败时,进程亲自调用shrink_lruvec()进行同步回收
// 这是性能杀手,会导致明显的延迟尖峰

透明大页(THP)

Transparent Huge Pages(透明大页)允许应用程序无需修改代码即可使用2MB(或1GB)的大页面,减少TLB miss和页表遍历开销,提升内存访问性能。

// THP核心机制(mm/huge_memory.c)
// 1. khugepaged守护进程周期性扫描进程地址空间
// 2. 当发现连续4KB页面足够 populate 时,合并为2MB大页
// 3. 使用deferral策略避免在不必要时浪费内存

// 使用模式:
// always:    始终启用THP合并
// madvise:   仅对MADV_HUGEPAGE标记的内存区域启用
// never:     完全禁用THP

// 性能权衡:
// 优势: TLB覆盖范围提升512倍,减少页表内存占用,降低TLB miss
// 风险: 内存碎片化导致大页分配失败,khugepaged CPU开销,
//       数据库场景下可能导致延迟波动(建议对数据库使用madvise模式)

NUMA感知内存分配

NUMA(Non-Uniform Memory Access)架构下,CPU访问本地节点内存的延迟低于远程节点。Linux内核实现了完整的NUMA感知内存分配策略:

// NUMA策略(mm/mempolicy.c)
enum mpol_plain {
    MPOL_DEFAULT,    // 使用进程默认策略(本地节点优先)
    MPOL_PREFERRED,  // 优先在指定节点分配,失败时回退
    MPOL_BIND,       // 必须在指定节点列表上分配
    MPOL_INTERLEAVE, // 按帧交替分配到各节点
    MPOL_LOCAL,      // 本地节点优先(同默认)
};

// AutoNUMA平衡(AutoNUMA balancing)
// 1. 周期性扫描进程地址空间中的页面访问频率
// 2. 若页面被远程CPU访问频繁,则迁移到访问CPU所在的节点
// 3. 内核参数kernel.numa_balancing控制开关
// 4. Linux 5.14+引入层级NUMA感知,支持CXL等新型内存扩展

内存压缩(ZRAM/ZSwap)

ZRAM和ZSwap是两种内存压缩技术,用于提高内存利用率:

// ZRAM(mm/zram)
// - 将一块内存模拟为块设备作为swap空间
// - 写入ZRAM的页面先经过压缩算法(LZO/LZ4/ZSTD)再存储
// - 典型压缩比1:2到1:3,可将等效内存容量提升2-3倍
// - 适合内存受限的嵌入式设备和容器环境

// ZSwap(mm/zswap)
// - 在页面真正写入磁盘swap之前,先压缩存储到内存池
// - 当池满时,LRU页面被逐出到真实swap设备
// - 减少对磁盘swap的I/O压力,性能好于传统swap
// - 可与ZRAM串联使用,实现多层压缩存储

// 性能对比:swap分区 vs ZRAM
// 场景:8GB内存嵌入式设备运行多个服务
// 传统swap:页面读写经过块设备I/O,延迟1-10ms
// ZRAM:压缩/解压延迟约5-50μs,快200倍

KSM(内核同页合并)

KSM(Kernel Samepage Merging)通过扫描内存寻找内容相同的页面,合并为一个写时复制(CoW)页面,节省物理内存。这在KVM虚拟化环境中尤为有效,不同虚拟机的相同操作系统页面可被合并。

// KSM核心(mm/ksm.c)// struct rmap_item {
//     struct rmap_item *rmap_list;   // 相同内容页面的链表
//     struct anon_vma *anon_vma;     // 匿名映射
//     struct mm_struct *mm;          // 进程地址空间
//     unsigned long address;         // 页面地址
// };

// KSM守护进程ksmd工作流程:
// 1. 扫描稳定红黑树,验证合并页面的内容未被修改// 2. 扫描不稳定树,寻找新候选页面进行checksum比较
// 3. 相同内容页面替换为特殊CoW页面指向稳定树中的页面
// 4. 写操作触发缺页异常,重新分配独立页面// 
// 参数调优:
// /sys/kernel/mm/ksm/pages_to_scan — 每次扫描页面数(默认100)
// /sys/kernel/mm/ksm/sleep_millisecs — 扫描间隔(默认20ms)
// /sys/kernel/mm/ksm/run — 开关KSM

内存控制组(memcg)

内存控制组(memory cgroup)用于隔离和限制容器或进程组的内存使用,是容器技术的基石。cgroup v2引入了递归统计和更好的high/max优先级机制。

// memcg v2核心机制// memory.max:  硬限制,超过立即触发OOM// memory.high: 软限制,超过触发回收但不会OOM(节流)
// memory.min: 保护内存,不会被全局回收
// memory.low: 尽量保护但可被回收// memory.swap.max: swap硬限制

// OOM处理优先级// cgroup v2中,当memory.max被超出时:// 1. 触发cgroup内OOM killer(仅限该cgroup)// 2. 不会直接kill整个节点其他进程// 3. 配合memory.events文件监控oom_kill事件// 实际容器调优示例:
// docker run --memory=512m --memory-swap=768m     // 内存512MB,swap可用256MB// 对应cgroup://   memory.max=536870912//   memory.swap.max=268435456

实战性能调试与调优

诊断内存问题常用工具

# 1. vmstat — 全局内存/页面统计(每秒刷新)
$ vmstat 1
procs ----memory---- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy wa st
 1  0      0 456232 658904 2103448    0    0     7    42   89  242 12  2  0  0

# 2. smem — 按进程查看PSS/USS(更准确的内存统计)
$ smem -m -s rss -r | head
PID     Command                         RSS     PRS     USS
1234    /usr/bin/postgres              384M    352M    320M# 3. slabtop — 查看SLAB/SLUB缓存使用情况$ slabtop -o 
 Active / Total Objects (% used)    : 45231 / 47896 (94.4%)
 Active / Total Caches (% used)     :    128 /    156 (82.1%)
 Active / Total Size (% used)      : 12.45M / 14.23M (87.5%)# 观察dentry、inode_cache等大项的增长趋势

# 4. /proc/meminfo 内部分析$ cat /proc/meminfo | grep -E "MemTotal|MemFree|Buffers|Cached|Active|Inactive|SwapTotal|SwapFree|Slab|PageTables"

内核参数调优建议

# swappiness — 控制匿名页与文件页的回收比例# 0=仅回收文件页,60=默认,100=积极swap
vm.swappiness=10          # 数据库场景建议1-10# dirty_ratio — 脏页占可用内存比达到此值时开始同步写回vm.dirty_ratio=40vm.dirty_background_ratio=10

# min_free_kbytes — 保留的最小空闲内存(KB)# 防止OOM killer被过早触发,大内存系统建议适当增加
vm.min_free_kbytes=262144  # 约256MB

# 透明大页配置
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled# 数据库推荐madvise,避免延迟尖峰

总结

Linux内存管理是一个庞大而精密的子系统,从底层的Buddy System到SLUB分配器,从页面回收到大页支持,从NUMA策略到内存控制组,各层机制相互配合,在不同场景下为系统提供稳定高效的内存服务。

系统管理员和开发者在调优时,应当遵循"测量优先"原则:先通过vmstat、smem、/proc/buddyinfo等工具建立基量,再针对性地调整相关参数。盲目调优往往适得其反。而对于内核开发者而言,理解这些机制的交互和性能特征,是写出高效内核代码的基础。

点赞(0) 打赏

评论列表 共有 0 条评论

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

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部