記憶體映射 (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 可映射到兩類物件:

Swap space 的上限意義

無論哪種映射,虛擬頁一旦初始化,之後就在核心維護的專用 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 指向相同的物理頁

   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(寫時複製) 技術映射,起步方式與共享物件完全相同——物理記憶體只有一份副本:

  1. 每個映射該私有物件的 process,其 private area 對應的 PTE 全部標為 read-only,且 area struct 標為 private copy-on-write
  2. 只要沒有任何 process 寫入,大家持續共享同一份物理副本
  3. 一旦某 process 對 private area 中的頁寫入,觸發 protection fault
  4. fault handler 發現例外來自「寫入 private COW area 的頁」→ 在物理記憶體建立該頁的新副本更新 PTE 指向新副本恢復該頁的寫入權限
  5. handler 返回後 CPU 重新執行該寫入指令,這次在新頁上正常完成。
COW 寫入流程(process 2 寫入某私有頁):

  write ──> PTE: read-only ──> Protection fault
                                    │
                              fault handler:
                                    │ 1. copy 該物理頁 → 新頁
                                    │ 2. 更新 P2 的 PTE 指向新頁
                                    │ 3. 恢復寫入權限
                                    ▼
                              返回,CPU 重新執行 write ──> 成功寫入新頁
  (P1 的 PTE 仍指向原頁;其餘未寫入的頁繼續共享)
COW 的精髓

把私有物件頁面的複製延遲到最後一刻(真的有人寫入時)才做,而且只複製被寫到的那一頁,使稀缺的物理記憶體得到最有效率的利用 (p.872)。


9.8.2 The fork Function Revisited (p.872)

有了 VM 與 memory mapping 的概念,就能理解 fork 如何為新 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)。載入分四步:

  1. Delete existing user areas — 刪除目前 process 使用者部分既有的 area structs。
  2. Map private areas — 為新程式的 code、data、bss、stack 建立新的 area structs,全部是 private copy-on-write:
    • code (.text)、data (.data) area → 映射到 a.out 檔案的 .text.data section(file-backed)。
    • bss area → demand-zero,映射到匿名檔案,大小記錄在 a.out 內。
    • stack、heap area → demand-zero,初始長度為 0。
  3. Map shared areas — 若 a.out 有連結共享物件(如 libc.so),先動態連結、再映射進使用者虛擬位址空間的 shared region
  4. 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 vs. execve 對虛擬記憶體的影響

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 └──────────┘
prot 位元 意義
PROT_EXEC 頁面內容為可由 CPU 執行的指令
PROT_READ 頁面可讀
PROT_WRITE 頁面可寫
PROT_NONE 頁面不可存取
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)。

Practice Problem 9.5 的思路 (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) |
| mmapstart 引數 | 只是提示,通常傳 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) |