全景#
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_adjOOM 觸發時 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] neveruserspace:malloc 怎麼運作#
malloc 是 libc 函式,不是 syscall。glibc 的 ptmalloc2 大致:
- 小區塊(< 128 KB):從 heap(
brk擴展)切片 - 大區塊:直接
mmap一段 anonymous free不一定回給核心;可能放回內部 free list 等待重用
每個 thread 有自己的 arena 減少鎖爭搶。jemalloc / tcmalloc / mimalloc 是替代方案,多 thread 場景更好。
從核心看,malloc(N) 大概對應一次 brk 或 mmap ── 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.max | swap 用量上限 |
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 配了哪些頁行程記憶體常見的單位:
| 名詞 | 含義 |
|---|---|
| VSS | Virtual Set Size:所有 VMA 加總(含 mmap 但未存取) |
| RSS | Resident Set Size:實際在記憶體的部分 |
| PSS | Proportional Set Size:共享頁按使用者數平分 |
| USS | Unique Set Size:只屬於這個行程的部分 |
監看記憶體洩漏看 PSS 最公平 ── 它把共享庫等資源依使用人數分攤。
小結#
Linux 記憶體管理的層次:
- 硬體:MMU 分頁、TLB
- 物理:node ➡️ zone ➡️ buddy ➡️ slab
- 行程:mm_struct ➡️ VMA ➡️ 頁表 ➡️ demand paging
- 進階:page cache、reclaim、swap、OOM、cgroup
- userspace:libc 在 syscall 之上自己做 allocator
讀懂 /proc/meminfo、cat /proc/<pid>/smaps、vmstat 1 三個輸出,已經能解 90% 的記憶體問題。