全景#

Linux mm 是核心最複雜的子系統之一。它要同時解決:

  • 把實體記憶體切塊配給核心與 userspace
  • 給每個行程獨立的虛擬位址空間(隔離 + 同名指標)
  • 記憶體不夠時:reclaim(回收)、swap、OOM kill
  • 大頁(huge page)、NUMA、記憶體熱插拔

本章從硬體開始,一路上到 userspace 看到的 malloc

硬體基礎:分頁#

CPU 看到的是虛擬位址,MMU 透過頁表翻譯成實體位址。基本單位是「頁(page)」,常見 4 KB(也有 16K、64K,視架構)。

x86-64 四級頁表#

virtual address (48-bit)
+--------+--------+--------+--------+----------------+
| PGD(9) | PUD(9) | PMD(9) | PTE(9) | offset(12)     |
+--------+--------+--------+--------+----------------+

CR3 ─► PGD ─► PUD ─► PMD ─► PTE ─► physical page
                                      + offset

每級表項是 8 byte,每張表 4 KB(512 entry)。一次 TLB miss ➡️ 4 次記憶體存取(page walk)。

5-level paging(Ice Lake+)擴到 57-bit 虛擬位址,多一級 P4D。

TLB#

CPU 內建 TLB(Translation Lookaside Buffer)快取最近用的翻譯,命中只需 1 cycle。TLB miss 觸發 page walk,可能花 100+ cycles。TLB 容量很小(L1 typically 64 entry,cover 256 KB),所以工作集大時 TLB miss 會是隱性瓶頸。

解法:huge page。一張 2 MB / 1 GB 的頁,一個 TLB entry 涵蓋更多記憶體。

核心視角的記憶體分區#

+------------------+ ZONE_HIGHMEM    (32-bit 才有;64-bit 不需要)
+------------------+ ZONE_NORMAL     (核心可直接映射的部分)
+------------------+ ZONE_DMA32      (4 GB 以下,給 32-bit DMA)
+------------------+ ZONE_DMA        (16 MB 以下,舊 ISA DMA)

64-bit 系統幾乎全部記憶體在 NORMAL,DMA32 只在有對齊限制的硬體用。每個 zone 是獨立的 free list。

NUMA 機器有多個 node(每 socket 一個),每個 node 有自己的 zone 集合。配置時偏好「local node」以避免跨 socket 存取的延遲。

實體記憶體配置:buddy allocator#

整個 zone 的 free 頁用 buddy 系統管理:把 free 頁按 2 的次方大小分組(1 page、2 pages、4 pages、…、1024 pages = 4 MB)。

配置 5 pages ➡️ 向上取整到 8 ➡️ 從 order=3 list 拿一塊;如果 order=3 沒有,往 order=4 找,拆成兩個 order=3,一個給配置者、一個放回 order=3 list(這就是「buddy」)。

釋放時,檢查 buddy 是否也 free,若是則合併成更大塊(向上 coalesce)。

cat /proc/buddyinfo
# Node 0, zone Normal      842    310    198    102    ...
#                          ↑      ↑      ↑      ↑
#                       order0 order1 order2 order3 ...

碎片是 buddy 的天敵 ── 當 high-order 塊湊不出來時,就算 free pages 加總很多也無法配置大塊。compaction 機制會搬遷頁面以釋放連續區域。

核心物件配置:slab / slub#

buddy 配置的單位是頁,但核心常需要小物件(task_struct ~10 KB、inode ~1 KB、dentry ~200 bytes)。slab allocator 從 buddy 拿頁,再切成等大物件 ── 類似 userspace 的 memory pool。

cat /proc/slabinfo
# name           <active_objs> <num_objs> <objsize> <objperslab> ...
# kmalloc-512    1024          1280       512       8            ...
# task_struct    412           480        9472      3            ...
# inode_cache    9821          12000      888       4            ...

Linux 主流是 SLUB(簡化版 slab)。SLAB(舊)已 deprecated,SLOB(極簡)給超小型嵌入式。

API:

// 通用配置
void *p = kmalloc(size, GFP_KERNEL);
kfree(p);

// 大塊(> 2 page,可能不連續)
void *p = vmalloc(size);
vfree(p);

// 自己的 slab
struct kmem_cache *cache = kmem_cache_create("my_obj", sizeof(my_obj_t),
                                              0, SLAB_HWCACHE_ALIGN, NULL);
my_obj_t *obj = kmem_cache_alloc(cache, GFP_KERNEL);
kmem_cache_free(cache, obj);
kmem_cache_destroy(cache);

kmalloc vs vmalloc

API連續性大小限制效能
kmalloc物理 + 虛擬都連續通常 ≤ 4 MB快(直接映射)
vmalloc僅虛擬連續,物理可散大塊可用慢(page walk)

對 DMA 等需要物理連續的用 kmalloc;對「我只是要 100 MB buffer 給核心結構」用 vmalloc。

行程位址空間:mm_struct 與 vma#

每個 process 有一個 struct mm_struct,描述其位址空間:

struct mm_struct {
    struct maple_tree mm_mt;    // VMA 樹(6.1 之前是 rb_tree)
    pgd_t *pgd;                 // 頁表根
    atomic_t mm_users;          // user-space 使用者數(thread group)
    atomic_t mm_count;          // 總引用數
    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;
    // ...
};

每個有獨立位址含義的區段叫一個 VMA(virtual memory area):

struct vm_area_struct {
    unsigned long vm_start, vm_end;
    pgprot_t vm_page_prot;
    unsigned long vm_flags;
    struct file *vm_file;       // 對應的檔案(mmap 來的)
    pgoff_t vm_pgoff;
    const struct vm_operations_struct *vm_ops;
    // ...
};

/proc/<pid>/maps 就是把 VMAs 列出來:

55b8c0000000-55b8c0001000 r-xp 00000000 fd:00 1234567 /usr/bin/cat
55b8c0001000-55b8c0002000 r--p 00001000 fd:00 1234567 /usr/bin/cat
55b8c0002000-55b8c0003000 rw-p 00002000 fd:00 1234567 /usr/bin/cat
55b8c1234000-55b8c1255000 rw-p 00000000 00:00 0       [heap]
7f...000-7f...000         r-xp 00000000 fd:00 ...     /lib/x86_64-linux-gnu/libc.so.6
7ffd...000-7ffd...000     rw-p 00000000 00:00 0       [stack]
7ffd...000-7ffd...000     r-xp 00000000 00:00 0       [vdso]
ffffffffff600000-ff... -- p 00000000 00:00 0          [vsyscall]

每行就是一個 VMA。

Demand paging 與 page fault#

mmap 一個 1 GB 檔案 ── 沒有立刻配 1 GB 物理記憶體。VMA 建好了,PTE 還是空的。第一次存取才觸發 page fault:

1. user code 讀位址 X
2. MMU 找 PTE,發現 not present → trap to kernel
3. do_page_fault() 走 vma 找對應 VMA
4. 配置物理頁、從檔案讀內容(或 zero-fill)
5. 填 PTE,flush TLB
6. 返回 user,重試該指令

幾種 page fault:

類型場景
Minor fault頁在 page cache(已在記憶體),只需建立映射
Major fault需要從磁碟讀入(昂貴)
COW fault寫入 RO 共享頁,複製出私有頁
Anonymous fault第一次存取 anonymous mapping,配 zero page
ps -o pid,min_flt,maj_flt,cmd -p <pid>

Page cache#

所有從磁碟讀進來的檔案內容都會放進 page cache(除非 O_DIRECT)。後續讀同樣的位置直接從 cache 給 ── 這是 Linux「閒置記憶體都是浪費」哲學的體現。

        +----------------+
read()  |  page cache    |  ─ hit ─► 立刻回
        +-------+--------+
                │ miss
                ▼
        +----------------+
        |  filesystem    |  ─► block layer ─► disk
        +----------------+

free 的 “buff/cache” 欄就是 page cache。系統需要記憶體時,page cache 是第一個被回收的。

Anonymous memory 與 swap#

不是檔案 backing 的記憶體(堆、stack、mmap(MAP_ANONYMOUS))叫 anonymous。沒有「磁碟原本的位置」可丟回去 ── 所以需要 swap 來「放逐」這些頁。

記憶體緊張時:
  page cache (file backed)  ─► 直接丟(dirty 先寫回,clean 直接丟)
  anonymous                  ─► 必須寫到 swap,才能回收物理頁

開了 zswap / zram:先試壓縮放在 RAM 的一個壓縮池裡,放不下才寫真的 swap 裝置。對 SSD 壽命友善、對啟動時間敏感的桌面系統很有幫助。

vm.swappiness(0~200):核心對 swap anonymous 的偏好程度。值越高越積極 swap,越低越積極丟 page cache。預設 60。資料庫伺服器常設低值(10、甚至 1),避免 swap 拖慢延遲。

reclaim:kswapd 與 direct reclaim#

當 free 記憶體低於水位線(low watermark),核心啟動回收:

  • kswapd:每個 NUMA node 有一個背景 thread,水位低時被喚醒
  • direct reclaim:配置時 free 太少 ── 配置者自己走 reclaim 路徑(同步阻塞)

回收掃 LRU 鏈:

active list ─► inactive list ─► 回收 / 寫回 swap

頁有 referenced bit,每次被存取就「promote」。久未存取的滑到 inactive list,最終被回收。

OOM killer#

如果 reclaim 都救不回來,最後手段是殺一個行程釋放記憶體。OOM killer 用啟發式評分(oom_score)選犧牲者:

  • 用越多 RSS 分數越高
  • root 行程稍微減分
  • oom_score_adj(-1000 ~ +1000)人為調整
  • oom_score_adj = -1000 表示「絕不殺我」
cat /proc/<pid>/oom_score
echo -1000 > /proc/<pid>/oom_score_adj

OOM 觸發時 dmesg 會印「Out of memory: Killed process xxx (yyy)」。

Huge pages#

兩種機制:

機制設定透明度
Hugetlbfs預先保留應用要明確 mmap(HUGETLB) 或用 hugetlbfs
THP(Transparent Huge Pages)madvise 或全自動對應用透明

THP 簡化了使用,但有「khugepaged 在背景搬遷時暫停」的延遲尖刺。Database(MongoDB、Redis 文件常勸退)通常建議關閉 THP。

cat /sys/kernel/mm/transparent_hugepage/enabled
# always [madvise] never

userspace:malloc 怎麼運作#

malloc 是 libc 函式,不是 syscall。glibc 的 ptmalloc2 大致:

  1. 小區塊(< 128 KB):從 heap(brk 擴展)切片
  2. 大區塊:直接 mmap 一段 anonymous
  3. free 不一定回給核心;可能放回內部 free list 等待重用

每個 thread 有自己的 arena 減少鎖爭搶。jemalloc / tcmalloc / mimalloc 是替代方案,多 thread 場景更好。

從核心看,malloc(N) 大概對應一次 brkmmap ── userspace 的「記憶體洩漏」常常是 RSS 並沒有立刻歸還核心,但程式內部已經把指標弄丟。

cgroup 記憶體控制#

容器時代的關鍵能力:限制一群行程的記憶體用量。

# cgroup v2
echo 1G > /sys/fs/cgroup/mygroup/memory.max
echo $$ > /sys/fs/cgroup/mygroup/cgroup.procs

達到 memory.max 時,先 reclaim、不行就在那群裡 OOM。可以細分:

參數作用
memory.high軟限制,超過會被 throttle
memory.max硬限制
memory.swap.maxswap 用量上限
memory.low保證留給這群的最低量

容器執行階段(Docker、Kubernetes)就是把容器的行程放進對應 cgroup 達成隔離。

觀測工具#

free -h                                      # 系統總覽
cat /proc/meminfo                            # 詳細
cat /proc/<pid>/status | grep Vm             # 行程記憶體
cat /proc/<pid>/smaps                        # 每個 VMA 詳細
pmap -X <pid>                                # 同上,較易讀
vmstat 1                                     # 動態:si/so 是 swap in/out
sar -B 1                                     # page fault、scan rate
slabtop                                      # SLUB 物件即時排序
cat /sys/kernel/debug/page_owner/...         # 哪個 caller 配了哪些頁

行程記憶體常見的單位:

名詞含義
VSSVirtual Set Size:所有 VMA 加總(含 mmap 但未存取)
RSSResident Set Size:實際在記憶體的部分
PSSProportional Set Size:共享頁按使用者數平分
USSUnique Set Size:只屬於這個行程的部分

監看記憶體洩漏看 PSS 最公平 ── 它把共享庫等資源依使用人數分攤。

小結#

Linux 記憶體管理的層次:

  1. 硬體:MMU 分頁、TLB
  2. 物理:node ➡️ zone ➡️ buddy ➡️ slab
  3. 行程:mm_struct ➡️ VMA ➡️ 頁表 ➡️ demand paging
  4. 進階:page cache、reclaim、swap、OOM、cgroup
  5. userspace:libc 在 syscall 之上自己做 allocator

讀懂 /proc/meminfocat /proc/<pid>/smapsvmstat 1 三個輸出,已經能解 90% 的記憶體問題。