記憶體映射 (Memory Mapping)
Overview Table
| 主題 | 核心概念 | 書頁 |
|---|---|---|
| Memory mapping 定義 | Linux 以 磁碟上的物件 (object) 初始化虛擬記憶體區域 (area) 的內容 | p.869 |
| 兩種映射物件 | Regular file(一般磁碟檔案)與 Anonymous file(核心建立、全為 0,產生 demand-zero pages) | p.869 |
| Swap file / swap space | 頁面初始化後即在此特殊檔案間換入/換出;swap space 限制了系統可配置的虛擬頁總量 | p.869 |
| Shared object | 多個 process 映射同一 shared object → 物理記憶體只需 一份副本;寫入對其他 process 可見並回寫磁碟 | pp.870-871 |
| Private object / Copy-on-Write (COW) | 私有物件初期共享同一物理副本,PTE 標為 read-only;寫入觸發 protection fault → 才複製該頁 | pp.871-872 |
fork revisited |
複製 mm_struct、area structs、page tables;兩邊頁面標 read-only、area 標 private COW → 延遲複製 |
p.872 |
execve revisited |
刪除舊 user areas → 映射 private areas(code/data/bss/stack)→ 映射 shared areas(如 libc.so)→ 設定 PC |
pp.872-873 |
mmap / munmap |
使用者層級建立/刪除虛擬記憶體區域的系統呼叫 | pp.873-875 |
9.8 記憶體映射基本觀念 (p.869)
Linux 將一個虛擬記憶體區域 (area) 的初始內容與磁碟上的物件關聯起來,此過程稱為 memory mapping(記憶體映射)。area 可映射到兩類物件:
- Regular file(一般檔案):映射到磁碟檔案(如可執行目的檔)的一段連續區段。檔案區段被切成 page-size 的小塊,每塊是一個虛擬頁的初始內容。因為 demand paging(需求分頁),在 CPU 第一次觸碰(touch,即發出落在該頁範圍內的虛擬位址)之前,任何頁都不會真的被換入物理記憶體。
- Anonymous file(匿名檔案):由核心建立、內容全為二進位 0 的檔案。CPU 第一次觸碰此類區域的頁時,核心找一個 victim page(若 dirty 則先換出)、將其覆寫為全 0、更新 page table 標記為 resident。過程中磁碟與記憶體之間沒有任何實際資料傳輸——因此這類頁稱為 demand-zero pages(需求置零頁)。
無論哪種映射,虛擬頁一旦初始化,之後就在核心維護的專用 swap file(又稱 swap space / swap area)之間換入換出。任一時刻,swap space 的大小限制 (bounds) 了目前執行中的所有 process 能配置的虛擬頁總量。
若 area 大於檔案區段,超出部分以 0 填補 (padded with zeros)。另外「初始內容來自檔案」不代表載入時就讀檔——demand paging 使一切延遲到第一次存取才發生(參見 09-Virtual-Memory/01-Address-Spaces-and-VM-Caching 的 page fault 機制)。
映射物件的兩種來源:
Virtual memory area
Regular file ──切成page大小──> [VP0][VP1][VP2]... 首次touch才從檔案載入
└─不足處補 0
Anonymous file(全0)────────> [VP0][VP1]... 首次touch:找victim頁、
(kernel 建立) 覆寫為0(無磁碟I/O)
= demand-zero page
初始化後: 虛擬頁 <──swap in/out──> swap file (swap space)
swap space 大小 = 可配置虛擬頁總量上限
9.8.1 Shared Objects Revisited(共享物件)(pp.869-872)
memory mapping 的洞見:把 VM 系統整合進傳統檔案系統,即可提供簡單且高效的程式與資料載入方式。許多 process 有相同的唯讀 code area(如所有跑 bash 的 process、所有用到 printf 的 C 程式都需要標準 C 函式庫),若每個 process 在物理記憶體各留一份副本會極度浪費——memory mapping 提供了乾淨的物件共享控制機制。
物件可以兩種身分被映射進虛擬記憶體:
| Shared object(共享物件) | Private object(私有物件) | |
|---|---|---|
| 本 process 的寫入,其他映射者可見? | 可見 | 不可見 |
| 寫入是否回寫到磁碟上的原物件? | 會 | 不會 |
| 對應的 area 名稱 | shared area | private area |
| 物理記憶體副本數 | 一份 | 初始一份(COW,寫入時才複製該頁) |
mmap flag |
MAP_SHARED |
MAP_PRIVATE |
共享物件的映射 (Figure 9.29)
process 1 映射某 shared object 後,process 2 再映射同一物件(虛擬位址不必相同)。因為每個物件有唯一的檔名,核心能快速判斷 process 1 已映射過,直接把 process 2 的 page table entries 指向相同的物理頁。
- 關鍵:即使物件被映射進多個 shared areas,物理記憶體只需儲存一份副本 (p.871)。
- 圖中物理頁畫成連續只是方便,實際上不必連續。
Process 1 VM Physical memory Process 2 VM
┌───────────┐ ┌───────────┐ ┌───────────┐
│ shared │───────>│ 同一份 │<───────│ shared │
│ area │ PTEs │ 物理頁 │ PTEs │ area │
└───────────┘ └─────┬─────┘ └───────────┘
(虛擬位址可不同) │
Shared object (disk)
私有物件與 Copy-on-Write (Figure 9.30)
私有物件用 copy-on-write(寫時複製) 技術映射,起步方式與共享物件完全相同——物理記憶體只有一份副本:
- 每個映射該私有物件的 process,其 private area 對應的 PTE 全部標為 read-only,且 area struct 標為 private copy-on-write。
- 只要沒有任何 process 寫入,大家持續共享同一份物理副本。
- 一旦某 process 對 private area 中的頁寫入,觸發 protection fault。
- fault handler 發現例外來自「寫入 private COW area 的頁」→ 在物理記憶體建立該頁的新副本、更新 PTE 指向新副本、恢復該頁的寫入權限。
- handler 返回後 CPU 重新執行該寫入指令,這次在新頁上正常完成。
COW 寫入流程(process 2 寫入某私有頁):
write ──> PTE: read-only ──> Protection fault
│
fault handler:
│ 1. copy 該物理頁 → 新頁
│ 2. 更新 P2 的 PTE 指向新頁
│ 3. 恢復寫入權限
▼
返回,CPU 重新執行 write ──> 成功寫入新頁
(P1 的 PTE 仍指向原頁;其餘未寫入的頁繼續共享)
把私有物件頁面的複製延遲到最後一刻(真的有人寫入時)才做,而且只複製被寫到的那一頁,使稀缺的物理記憶體得到最有效率的利用 (p.872)。
9.8.2 The fork Function Revisited (p.872)
有了 VM 與 memory mapping 的概念,就能理解 fork 如何為新 process 建立獨立的虛擬位址空間:
- 呼叫
fork時,核心為新 process 建立各種資料結構並指派唯一 PID。 - 建立新 process 的虛擬記憶體:原樣複製目前 process 的
mm_struct、area structs、page tables。 - 將兩個 process 的每一頁都標為 read-only,每個 area struct 都標為 private copy-on-write。
fork在新 process 返回時,新 process 擁有呼叫當下虛擬記憶體的精確副本;之後任一方寫入時,COW 機制建立新頁——如此以最低成本維持「每個 process 擁有私有位址空間」的抽象。
fork 不會立即複製任何物理頁,只複製 metadata(mm_struct / area structs / page tables);而且 read-only + COW 是父子雙方都被標記,不是只有子 process。父 process 先寫入同樣會觸發 COW 複製。
9.8.3 The execve Function Revisited (pp.872-873)
execve("a.out", NULL, NULL) 在目前 process 內載入並執行 a.out,取代目前的程式(對照 08-Exceptional-Control-Flow/04-Process-Control)。載入分四步:
- Delete existing user areas — 刪除目前 process 使用者部分既有的 area structs。
- Map private areas — 為新程式的 code、data、bss、stack 建立新的 area structs,全部是 private copy-on-write:
- code (.text)、data (.data) area → 映射到
a.out檔案的.text、.datasection(file-backed)。 - bss area → demand-zero,映射到匿名檔案,大小記錄在
a.out內。 - stack、heap area → demand-zero,初始長度為 0。
- code (.text)、data (.data) area → 映射到
- Map shared areas — 若
a.out有連結共享物件(如libc.so),先動態連結、再映射進使用者虛擬位址空間的 shared region。 - Set the program counter (PC) — 最後把目前 process context 中的 PC 設為 code area 的 entry point。
下次此 process 被排程時就從 entry point 開始執行;Linux 依需求 (demand paging) 換入 code 與 data 頁。
execve 後的使用者位址空間映射 (Figure 9.31):
┌──────────────────────────┐
│ User stack │ Private, demand-zero
├──────────────────────────┤
│ Memory-mapped region │ Shared, file-backed
│ for shared libraries │ (libc.so 的 .text/.data)
├──────────────────────────┤
│ Run-time heap (malloc) │ Private, demand-zero
├──────────────────────────┤
│ Uninitialized data (.bss)│ Private, demand-zero
├──────────────────────────┤
│ Initialized data (.data) │ Private, file-backed
├──────────────────────────┤ (映射自 a.out 的 .data)
│ Code (.text) │ Private, file-backed
├──────────────────────────┤ (映射自 a.out 的 .text)
0 └──────────────────────────┘
| Area | 映射類型 | 後備物件 (backing store) |
|---|---|---|
| Code (.text) | Private (COW), file-backed | a.out 的 .text section |
| Initialized data (.data) | Private (COW), file-backed | a.out 的 .data section |
| Uninitialized data (.bss) | Private, demand-zero | 匿名檔案(大小記在 a.out) |
| Heap | Private, demand-zero | 匿名檔案(初始長度 0) |
| User stack | Private, demand-zero | 匿名檔案(初始長度 0) |
Shared libraries(如 libc.so) |
Shared, file-backed | 共享目的檔 |
fork:複製現有映射(COW 延遲實體複製)。execve:砍掉重建——刪除舊 user areas、依 a.out 重新映射。這也解釋了為何 execve 從不返回:舊程式的位址空間已被替換(參見 07-Linking/03-Executable-Loading-and-Dynamic-Linking)。
9.8.4 User-Level Memory Mapping with the mmap Function (pp.873-875)
Linux process 可用 mmap 自行建立新的虛擬記憶體 area,並把物件映射進去:
#include <unistd.h>
#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags,
int fd, off_t offset);
/* Returns: 成功回傳映射區起始指標;失敗回傳 MAP_FAILED (-1) */
mmap 要求核心建立一個新 area(最好從位址 start 開始),並把檔案描述子 fd 指定物件中「自檔案開頭 offset 位元組起、長 length 位元組」的連續區塊映射到該 area。
mmap 引數的視覺意義 (Figure 9.32):
Disk file (fd) Process virtual memory
┌──────────┐ ┌──────────┐
│ │ │ │
│▓▓▓▓▓▓▓▓▓▓│ length ───映射──>│▓▓▓▓▓▓▓▓▓▓│ length (bytes)
│▓▓▓▓▓▓▓▓▓▓│ (bytes) │▓▓▓▓▓▓▓▓▓▓│<── start(或由 kernel 決定)
├──────────┤ │ │
│ offset │ (bytes) │ │
0 └──────────┘ 0 └──────────┘
start只是提示 (hint),通常傳NULL由核心自選位址。prot:新映射 area 的存取權限位元(即對應 area struct 的vm_prot位元,對照 09-Virtual-Memory/02-VM-Management-and-Protection):
| prot 位元 | 意義 |
|---|---|
PROT_EXEC |
頁面內容為可由 CPU 執行的指令 |
PROT_READ |
頁面可讀 |
PROT_WRITE |
頁面可寫 |
PROT_NONE |
頁面不可存取 |
flags:描述被映射物件的類型:
| flags 位元 | 意義 |
|---|---|
MAP_ANON |
後備物件為匿名物件,對應虛擬頁為 demand-zero |
MAP_PRIVATE |
private copy-on-write 物件 |
MAP_SHARED |
shared 物件 |
範例——要求一塊唯讀、私有、demand-zero 的 size 位元組區域:
bufp = Mmap(NULL, size, PROT_READ, MAP_PRIVATE|MAP_ANON, 0, 0);
munmap:刪除映射區域
#include <unistd.h>
#include <sys/mman.h>
int munmap(void *start, size_t length);
/* Returns: 成功 0;失敗 -1 */
刪除從虛擬位址 start 開始、接下來 length 位元組的 area。之後再引用被刪除的區域會導致 segmentation fault (p.875)。
用 mmap 把任意大小的磁碟檔案複製到 stdout:open 檔案 → fstat 取得檔案大小 → mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0) → 對映射區指標直接 write(1, bufp, size)。重點:mmap 之後存取檔案內容就像存取記憶體,不需 read。
Exam/Test Patterns
| 情境 / 關鍵字 | 答案 |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| 「demand-zero page」是什麼、來自哪種映射? | 映射到匿名檔案(anonymous file, 全 0)的頁;首次 touch 時核心把 victim 頁覆寫為 0,無磁碟資料傳輸 |
| 虛擬頁初始化後在哪裡換入換出?其大小限制什麼? | 核心維護的 swap file (swap space);它限制目前所有 process 可配置的虛擬頁總量 |
| 兩個 process 映射同一 shared object,物理記憶體幾份副本? | 一份;核心以物件唯一檔名辨識已映射,直接讓 PTE 指向既有物理頁 |
| 兩 process 映射同一 shared object,虛擬位址必須相同? | 不必;各自可映射到不同虛擬位址 |
| shared vs. private 寫入的可見性 | shared:其他映射者可見且回寫磁碟;private:皆否(COW 產生私有副本) |
| 寫入 private COW 頁時發生什麼? | PTE 為 read-only → protection fault → handler 複製該頁、更新 PTE、恢復寫入權限 → 重新執行寫入指令 |
| COW 一次複製多少? | 只複製被寫入的那一頁,不是整個物件 |
| fork 如何處理虛擬記憶體? | 複製 mm_struct / area structs / page tables;父子雙方頁面標 read-only、area 標 private COW;不立即複製物理頁 |
| execve 載入程式的四步驟 | 1. 刪除舊 user areas 2. 映射 private areas(code/data file-backed;bss/stack/heap demand-zero)3. 映射 shared areas(動態連結的 libc.so 等)4. 設定 PC 指向 entry point |
| .bss / stack / heap 在 execve 後的映射性質 | private、demand-zero(匿名檔案);stack/heap 初始長度 0 |
| .text / .data 在 execve 後的映射性質 | private (COW)、file-backed(映射自 a.out 對應 section) |
| mmap 的 start 引數 | 只是提示,通常傳 NULL 讓核心決定 |
| mmap 失敗回傳值 / munmap 回傳值 | MAP_FAILED (−1);munmap 成功 0、失敗 −1 |
| 存取已被 munmap 刪除的區域 | segmentation fault |
| 要一塊私有、demand-zero 記憶體的 mmap 寫法 | mmap(NULL, size, PROT_READ, MAP_PRIVATE | MAP_ANON, 0, 0) |
Related Notes
- 09-Virtual-Memory/01-Address-Spaces-and-VM-Caching — demand paging、page fault、swap 的基礎機制
- 09-Virtual-Memory/02-VM-Management-and-Protection — area struct、
vm_prot、Linux VM 區域組織與保護位元 - 09-Virtual-Memory/03-Address-Translation-and-TLB — PTE 與位址轉譯,COW 依賴 PTE 權限位觸發 fault
- 09-Virtual-Memory/05-Dynamic-Memory-Allocation — heap 之上的 malloc 套件(9.9 起,較 mmap 更方便可攜)
- 08-Exceptional-Control-Flow/04-Process-Control —
fork/execve的 process 觀點 - 07-Linking/03-Executable-Loading-and-Dynamic-Linking — loader、
.text/.data/.bsssection 與動態連結libc.so