引言

Linux内存管理是操作系统内核最复杂的子系统之一,它负责管理物理内存、虚拟地址空间、页面缓存以及内存分配与回收。理解Linux内存管理的底层机制,对于系统调优、性能排查和内核开发至关重要。本文将从虚拟地址空间出发,深入剖析Linux内存管理的完整技术链条。

一、虚拟地址空间架构

Linux为每个进程提供独立的虚拟地址空间,在x86_64架构下,用户空间占据0x0000000000000000到0x00007FFFFFFFFFFF(128TB),内核空间占据0xFFFF80000000000以上。

// 典型进程内存布局(高地址→低地址)
┌──────────────────────┐ 0xFFFFFFFFFFFFFFFF
│      内核空间         │  (128TB, 所有进程共享)
├──────────────────────┤ 0xFFFF800000000000
│      未使用区域       │
├──────────────────────┤ 0x00007FFFFFFFFFFF
│      栈 (Stack)      │  ↓ 向下增长
├──────────────────────┤
│      mmap 区域       │  动态库、共享内存映射
├──────────────────────┤
│      堆 (Heap)       │  ↑ 向上增长 (brk/mmap)
├──────────────────────┤
│       BSS段          │  未初始化全局变量
├──────────────────────┤
│       Data段         │  已初始化全局变量
├──────────────────────┤
│       Text段         │  程序代码
└──────────────────────┘ 0x0000000000400000 (典型ELF入口)

用户空间通过mm_struct结构体进行管理:

struct mm_struct {
    pgd_t *pgd;                    // 页全局目录(PGD)基址
    unsigned long start_code, end_code;    // 代码段范围
    unsigned long start_data, end_data;    // 数据段范围
    unsigned long start_brk, brk;          // 堆边界
    unsigned long start_stack;             // 栈起始地址
    unsigned long mmap_base;               // mmap区域基址
    int map_count;                         // VMA数量
    // ...
};

虚拟内存区域通过VMA(vm_area_struct)进行细粒度管理,每个VMA代表一段具有相同权限的连续虚拟地址范围:

// 查看进程内存映射
$ cat /proc/$$/maps
55c3d8a6d000-55c3d8a6e000 r--p 00000000 08:02 1310778  /usr/bin/bash
55c3d8a6e000-55c3d8a7a000 r-xp 00001000 08:02 1310778  /usr/bin/bash
55c3d8a7a000-55c3d8a7d000 r--p 0000d000 08:02 1310778  /usr/bin/bash
55c3d8a7d000-55c3d8a7e000 rw-p 00010000 08:02 1310778  /usr/bin/bash
55c3d8a7e000-55c3d8a87000 rw-p 00000000 00:00 0        [heap]
7f1d8c000000-7f1d8c028000 rw-p 00000000 00:00 0
7f1d8c028000-7f1d90000000 ---p 00000000 00:00 0
7f1d92200000-7f1d923e7000 r-xp 00000000 08:02 1048602  /usr/lib/libc.so.6

二、多级页表机制

x86_64架构采用4级页表结构,将64位虚拟地址拆分为5个字段:

// 48位有效虚拟地址的页表索引结构
┌─────────┬─────────┬─────────┬─────────┬──────────┐
│  PML4   │   PDPT  │   PD    │   PT    │  Offset  │
│ 9 bits  │ 9 bits  │ 9 bits  │ 9 bits  │ 12 bits  │
│(bit47:39)│(bit38:30)│(bit29:21)│(bit20:12)│(bit11:0) │
└─────────┴─────────┴─────────┴─────────┴──────────┘
                                                         
PML4: Page Map Level 4 (顶级页表)
PDPT: Page Directory Pointer Table (页目录指针表)
PD:   Page Directory (页目录)
PT:   Page Table (页表)
Offset: 页内偏移 (4KB页)

2017年后Intel引入了5级页表(LA57),支持57位虚拟地址空间(128PB):

// 5级页表新增PML5层级,索引bit56:49
┌─────────┬─────────┬─────────┬─────────┬─────────┬──────────┐
│  PML5   │  PML4   │   PDPT  │   PD    │   PT    │  Offset  │
└─────────┴─────────┴─────────┴─────────┴─────────┴──────────┘

TLB(Translation Lookaside Buffer)缓存最近使用的页表项访问:

// 查看TLB信息
$ lscpu | grep TLB
L1d TLB:                   64 entries (4KB pages), 4-way
L1i TLB:                   128 entries (4KB pages), 8-way
L2 TLB:                    1536 entries (4KB pages), 6-way

// TLB刷新相关指令
// invlpg addr    - 刷新单个TLB项
// mov cr3, reg   - 刷新所有非全局TLB项

Linux内核使用struct page描述每个物理页框:

struct page {
    unsigned long flags;          // 状态标志 (PG_locked, PG_dirty等)
    atomic_t _refcount;           // 引用计数
    atomic_t _mapcount;           // 映射计数(被多少PTE指向)
    struct { 
        union {
            struct list_head lru;        // LRU链表节点
            struct page *next;           // 复合页链表
        };
    };
    unsigned long private;        // 私有数据指针
    struct address_space *mapping;// 关联地址空间
    pgoff_t index;                // 在映射中的偏移
};

三、物理内存分配器

Linux物理内存管理采用三层架构:

3.1 Buddy System(伙伴系统)

伙伴系统以2的幂次为单位管理物理页(4KB, 8KB, 16KB...2MB),核心思想是分裂与合并:

// 伙伴系统核心逻辑(简化版)
// 分配时:向上查找合适阶→分裂成两个伙伴→返回一半
// 释放时:检查伙伴是否空闲→合并→向上传导

// 阶(order)与页数对应关系
order 0:  1 page   (4KB)
order 1:  2 pages  (8KB)
order 2:  4 pages  (16KB)
order 9:  512 pages (2MB)  // 最大

// 伙伴关系示例(order=2, 16页框=64KB块)
Block A: pages 0-15    Block B: pages 16-31
若 A的地址 XOR (4 << PAGE_SHIFT) == B的地址 → 互为伙伴

// 查看区域空闲情况
$ cat /proc/buddyinfo
Node 0, zone   DMA     1    0    1    1    2    1    1    0    1    1    3
Node 0, zone  DMA32  315  287  226  133   72   36   19   10    4    1    2
Node 0, zone Normal 8014 6144 4096 2048 1024  512  256  128   64   32   16
                 ↑order0  ↑order1  ↑ ...  ↑order10(2MB)

3.2 SLAB/SLUB分配器

SLAB分配器在伙伴系统之上构建对象缓存,避免频繁分配/释放小对象的开销:

// SLUB(Linux默认分配器)核心数据结构
struct kmem_cache {
    struct kmem_cache_cpu *cpu_slab;   // CPU本地快速路径
    struct kmem_cache_node *node;       // NUMA节点级缓存
    unsigned int size;                   // 对象大小(含对齐)
    unsigned int object_size;            // 用户请求的实际大小
    unsigned long flags;                 // SLAB_ACCOUNT等
    const char *name;                    // 缓存名称(如"dentry_cache")
};

struct kmem_cache_cpu {
    void **freelist;        // 空闲对象链表头
    unsigned long tid;      // 全局事务ID(用于并发控制)
    struct page *page;      // 当前正在使用的页
    struct page *partial;   // CPU级部分空闲slab链表
};

// 典型的kmem_cache
dentry_cache      (目录缓存)
inode_cache       (索引节点)
vm_area_struct    (VMA结构体)
task_struct       (进程描述符)

// 查看SLAB分配器使用
$ cat /proc/slabinfo | head
slabinfo - version: 2.1
# name                ...
dentry              28674   33216     192   21   4    0    0
inode_cache          9874   10400     632    5   8    0    0
vm_area_struct      11234   12012     208   19   4    0    0
task_struct          1024    1080    7488    1   8    2    0

3.3 Per-CPU页框缓存

每个CPU维护一批热页和冷页的私有缓存,减少跨CPU的缓存行 bouncing:

// Per-CPU pageset
struct per_cpu_pageset {
    struct per_cpu_pages {
        int count;          // 当前页框数
        int high;           // 高水位线(超过则归还zone)
        int batch;          // 批量分配/回收数量
        
        struct list_head lists[MIGRATE_PCPTYPES];  // 按迁移类型分链
    } pcp[2];  // [0]=热页 pcp, [1]=冷页 pcp
};

四、页面回收机制

当物理内存不足时,Linux通过LRU(Least Recently Used)算法回收页面:

4.1 LRU链表设计

Linux内核采用双链表设计分别追踪活跃和不活跃页面:

// 区域LRU链表(zone→pgdat→lruvec)
enum LRU_LISTS {
    LRU_INACTIVE_ANON = 0,  // 不活跃匿名页
    LRU_ACTIVE_ANON,        // 活跃匿名页
    LRU_INACTIVE_FILE,      // 不活跃文件页
    LRU_ACTIVE_FILE,        // 活跃文件页
    LRU_UNEVICTABLE,        // 不可回收页(mlock等)
    NR_LRU_LISTS
};

// 页面在活跃/不活跃链表间的演化:
// 首次访问 → INACTIVE链表
// 第二次访问 → 提升到ACTIVE链表(promote)
// 长时间未访问 → 降级到INACTIVE链表(demote)
// 仍在INACTIVE且内存紧张 → 回收

// 相关参数
vm.swappiness = 60         // 回收倾向(0=尽量不swap,100=积极swap)
vm.min_free_kbytes = 67584 // 保留的最低空闲内存(kswapd唤醒阈值)

4.2 kswapd与直接回收

页面回收有两种路径:

// kswapd(后台异步回收)
触发条件:free < WMARK_LOW(低水位线)
工作:扫描LRU尾部,回收页面直到free > WMARK_HIGH
每次回收可唤醒后睡眠,避免CPU占用

// direct reclaim(同步直接回收)
触发条件:进程分配时free < WMARK_MIN,且kswapd来不及回收
影响:进程被阻塞,性能急剧下降(可能达秒级)
避免方法:合理设置min_free_kbytes,或预先分配

// 水位线定义(zone层)
┌─────────────────────────┐  WMARK_HIGH   ← kswapd目标
│                         │
│    可用内存充足          │  WMARK_LOW    ← kswapd唤醒阈值
│                         │
│    内存开始紧张          │  WMARK_MIN    ← 直接回收触发点
│                         │
└─────────────────────────┘  0

4.3 页面写回与交换

// 文件页回收路径(双LRU回收)
shrink_page_list() {
    for page in lru_list:
        if PageAnon(page):   // 匿名页
            if has_swap:     // 有swap分区
                写入swap(swap_cache)
                从页表移除(保留swap_entry)
        else:               // 文件页
            if PageDirty:    // 脏页
                异步写回(writeback)
                写回后尝试回收
            else:
                直接回收(来自PageCache)
}

五、OOM Killer机制

当内存极度紧张且回收也无法满足时,内核触发OOM Killer选择进程终止:

// OOM评分计算(oom_badness)
// 评分越高,得分高者被杀掉
points = total_vm_pages  // 基础分:进程使用的内存总量
points *= 1 << (oom_score_adj + 1000) / 1000  // 调整系数
// 特殊规则:
// - init/systemd (pid=1) 评分除以4
// - 内核线程忽略
// - CAP_SYS_ADMIN进程减分(受保护)

// OOM评分调整
$ echo -500 > /proc/$$/oom_score_adj  // 降低被杀概率
$ echo 1000 > /proc/$$/oom_score_adj   // 最容易被杀(测试用)

// 查看进程OOM评分
$ cat /proc/$$/oom_score           // 当前评分(只读)
$ cat /proc/$$/oom_score_adj       // 调整值(可写)

较新的内核引入了oomd(systemd配套)和BPF机制,在用户空间更早介入,避免完全依赖内核OOM:

// 使用cgroup memory.limit限制进程组
$ echo "2G" > /sys/fs/cgroup/memory/myapp/memory.limit_in_bytes
$ echo "1.5G" > /sys/fs/cgroup/memory/myapp/memory.soft_limit_in_bytes

// cgroup v2 内存控制
$ cat /sys/fs/cgroup/myapp/memory.current
$ cat /sys/fs/cgroup/myapp/memory.high      // 软限制(触发回收)
$ cat /sys/fs/cgroup/myapp/memory.max       // 硬限制(触发OOM)
$ cat /sys/fs/cgroup/myapp/memory.stat      // 详细统计

六、大页(HugePages)支持

标准4KB粒度导致TLB Miss率随内存增大而上升,大页机制降低页表深度和TLB压力:

6.1 2MB/1GB大页对比

// 页表层级与TLB覆盖
             页大小    TLB条目   TLB覆盖(典型)   页表层级
  Normal     4KB       64       256KB           4级
  HugePage   2MB       32       64MB            3级(跳过PT)
  HugePage   1GB       4        4GB             2级(跳过PT和PD)

// 查看大页配置
$ cat /proc/meminfo | grep Huge
HugePages_Total:      0
HugePages_Free:       0
Hugepagesize:       2048 kB
Hugetlb:               0 kB

// 预留静态大页(启动参数或sysctl)
$ echo 256 > /proc/sys/vm/nr_hugepages
// 或启动参数: default_hugepagesz=2M hugepagesz=2M hugepages=256

6.2 透明大页(THP)

Linux引入透明大页,由khugepaged线程主动合并连续的普通页:

// THP工作模式
$ cat /sys/kernel/mm/transparent_hugepage/enabled
always [madvise] never
  always: 对所有区域尝试合并(可能增加延迟)
  madvise: 仅对MADV_HUGEPAGE标注的映射进行
  never: 禁用THP  (数据库场景常用)

// 对关键区域显式控制
#include <sys/mman.h>
mmap(NULL, size, PROT_READ|PROT_WRITE,
     MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0);  // 显式大页
madvise(ptr, len, MADV_HUGEPAGE);   // 建议THP扫描
madvise(ptr, len, MADV_NOHUGEPAGE); // 建议THP不处理

// THP的坑:延迟与内存碎片
// 数据库场景(Redis/MySQL)通常禁用THP,避免:
// 1. 大页分配时compact导致停顿
// 2. khugepaged CPU开销
// 3. 非必要的大页合并浪费内存

七、NUMA架构下的内存管理

多处理器系统中,内存访问延迟与物理位置相关:

// NUMA拓扑查看
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7   size: 65408MB  free: 32768MB
node 1 cpus: 8 9 10 11 12 13 14 15 size: 65536MB free: 49152MB
node distances:
node   0   1
  0:  10  20
  1:  20  10
// 跨节点访问价格为本地节点2倍

// NUMA感知的分配策略
// 默认:本地节点分配(First Touch Policy)
// 绑定:numactl --membind=0 ./app
// 交错:numactl --interleave=all ./app
// 优先:numactl --preferred=0 ./app

// 查看进程NUMA统计
$ cat /proc/$$/numa_maps
7f3e80000000 default file=/usr/lib/libc.so.6
7f3e80000000 bind:1 anon=128 dirty=128 N1=128 kernelpagesize_kB=4
// N0, N1...表示各节点上的页面分布

Linux内核通过自动均衡(Auto NUMA Balancing)动态迁移页面:

// 工作流程
// 1. task_numa_fault() 记录跨节点访问的页面
// 2. 累积到一定阈值后触发迁移
// 3. migrate_misplaced_page() 将页面迁移到访问者节点

$ echo 1 > /proc/sys/kernel/numa_balancing   // 启用
$ echo 0 > /proc/sys/kernel/numa_balancing   // 禁用(固定绑定场景)

八、内存碎片整理

长时间运行后,物理内存被分割成小块,影响大页分配:

// 外部碎片问题
// 场景:即使总空闲内存足够,也可能没有连续2MB的物理块
// 解决:内存规整(page compaction)

// 碎片索引(0-10,10表示完全碎片化)
$ cat /sys/kernel/debug/extfrag/extfrag_index

// 触发碎片整理
$ echo 1 > /proc/sys/vm/compact_memory

//  compaction模式
$ cat /sys/kernel/mm/compaction/proactiveness
// 0=只在需要时触发, 100=积极预整理

// 迁移类型分组(buddy system内部)
enum migratetype {
    MIGRATE_UNMOVABLE,  // 不可移动(内核数据结构)
    MIGRATE_MOVABLE,    // 可移动(用户空间匿名页)
    MIGRATE_RECLAIMABLE,// 可回收(文件缓存)
    MIGRATE_PCPTYPES,   // Per-CPU缓存
    MIGRATE_ISOLATE,    // 隔离(hotplug等)
    MIGRATE_TYPES
};
// 目的:让相同迁移类型的页聚集,减少碎片

九、内存管理调试与观测

9.1 关键统计信息

$ cat /proc/meminfo
MemTotal:       16384000 kB
MemFree:         2048000 kB
MemAvailable:    8192000 kB    // 包含可回收缓存的可用内存
Buffers:          256000 kB    // 裸块设备缓冲
Cached:          4096000 kB    // 文件页缓存
SwapTotal:       8388608 kB
SwapFree:        8388608 kB
Slab:             512000 kB    // SLAB分配器使用
SReclaimable:     384000 kB    // 可回收SLAB(dentry等)
SUnreclaim:       128000 kB    // 不可回收SLAB
PageTables:        12000 kB    // 页表占用
NFS_Unstable:          0 kB
Writeback:           256 kB
AnonHugePages:         0 kB    // 透明大页数

// MemAvailable估算公式(kernel 3.14+)
MemAvailable = MemFree + Cached + SReclaimable + ...
              - Watermark[low] - lowmem_reserve

9.2 性能诊断工具

// vmstat - 内存/页错误/IO概览
$ vmstat 1 5
procpmemory_____________swap_____io___system_______cpu____
 r b swpd   free   buff  cache   si   so   bi   bo  in  cs  us sy id wa st
 1 0    0 2048000 256000 4096000  0    0   32    8  120 450  5  2 92  1  0
// si/so: swap in/out(应接近0)
// bi/bi: block in/out

// pmap - 进程内存映射详情
$ pmap -x $$
Address           Kbytes     RSS   Dirty Mode
000055c3d8a6d000    12      12       0 r--  bash
000055c3d8a6e000    92      92       0 r-x  bash
000055c3d8a7a000    28      28       0 r--  bash
000055c3d8a7d000     4       4       4 rw-  bash
000055c3d8a7e000    36      36      36 rw-  [heap]
// RSS=实际驻留内存, Dirty=脏页

// sar - 历史趋势
$ sar -r 1 5
10:00:01  kbmemfree kbmemused  %memused kbbuffers  kbcached kbcommit  %commit  kbactive   kbinact   kbdirty
10:00:02  2048000   14336000   87.50    256000    4096000  9876543   60.25    8192000   2048000   512

// /proc/pid/smaps - 精细映射信息
$ cat /proc/$$/smaps_rollup
00400000-7fff0000 ---p 00000000 00:00 0                              [stack]
Size:             838860 kB
Rss:              12345 kB
Pss:              12345 kB    // Proportional Set Size(共享页按比例分摊)
Shared_Clean:         0 kB
Shared_Dirty:         0 kB
Private_Clean:      512 kB
Private_Dirty:    11833 kB
Referenced:       12345 kB
Anonymous:        11833 kB

9.3 eBPF视角的内存分析

// 使用bcc工具观测页分配延迟
$ /usr/share/bcc/tools/funclatency mm_page_alloc
     nsecs               : count     distribution
       0 -> 1          : 0        |                                        |
       2 -> 3          : 0        |                                        |
       4 -> 7          : 0        |                                        |
       8 -> 15         : 12       |***                                     |
      16 -> 31         : 45       |*************                           |
      32 -> 63         : 123      |*************************************   |
      64 -> 127        : 89       |************************                 |
     128 -> 255        : 34       |**********                               |
     256 -> 511        : 8        |**                                       |
// 如果大量分配超过1μs,可能发生了direct reclaim

// 跟踪TLB miss
$ perf stat -e dTLB-load-misses,dTLB-loads ./workload
// 计算:dTLB miss ratio = misses / loads
// 高命中率(>10%)时考虑大页

十、调优实践总结

// 服务器场景通用调优参数(/etc/sysctl.conf)
vm.swappiness = 10           // 仅在必要时swap
vm.dirty_ratio = 40          // 脏页最大比例(触发同步写回)
vm.dirty_background_ratio = 10 // 后台异步写回起始比例
vm.dirty_expire_centisecs = 3000  // 脏页过期时间(30s)
vm.dirty_writeback_centisecs = 500// kwbflush周期(5s)
vm.min_free_kbytes = 209712  // 保证200MB最低保留(16GB内存机器)
vm.vfs_cache_pressure = 50   // 目录/inode缓存回收倾向(默认100)
vm.overcommit_memory = 0     // 允许合理overcommit(1=永远允许,2=严格)
vm.overcommit_ratio = 50     // 可超分物理+swap的%

// 数据库专用优化
vm.swappiness = 1
# echo never > /sys/kernel/mm/transparent_hugepage/enabled
nr_hugepages = 1024          // 为大内存数据库预留2GB大页
vm.zone_reclaim_mode = 0     // 禁用NUMA本地回收(避免本地节点抖动)

// 高并发网络服务
vm.max_map_count = 262144    // 允许大量线程的mmap
net.core.rmem_max = 16777216 // socket缓冲区
net.ipv4.tcp_rmem = 4096 131072 16777216

总结

Linux内存管理是一个精密而复杂的系统工程。从虚拟地址到物理页框的映射,到SLAB分配器的高效管理,再到LRU驱动的页面回收,每个子系统都经过数十年的演进优化。理解这些底层机制,是进行系统级性能调优和故障排查的基础。

关键要点:

  • 多级页表是虚拟内存的基石,TLB性能直接影响内存密集型应用
  • 伙伴系统 + SLUB分配器构成物理内存管理的双层架构
  • LRU双链表设计在内存压力和应用性能间取得平衡
  • 大页(THP/HugePages)是减少TLB Miss的有效手段,但需场景适配
  • NUMA感知策略在多路服务器上至关重要
  • eBPF工具链让内存行为的实时观测成为可能

随着持久内存(PMEM)、CXL互联内存等新技术的发展,Linux内存管理仍在持续演进,掌握底层原理将帮助开发者更好地驾驭未来架构。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.417696s