National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 72 24°C PM2.5 13
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 13

分頁與 TLB

虛擬位址怎麼變成實體位址、查表太慢怎麼辦
Interaction實作 35 minute(s)

Learning Objectives

  • 說明虛擬位址帶來的兩個好處:行程彼此隔離、實體記憶體不必連續
  • 把虛擬位址切成頁號與位移,查頁表換成實體位址
  • 說明 SIGSEGV 怎麼從頁表的權限位元來
  • 解釋 TLB 為什麼有效,並預測循序、跨頁、隨機存取的命中率
  • 說明內容切換對 TLB 的影響,以及 ASID 解決什麼問題

同一個位址,兩個行程讀到不同的值

父行程和子行程印出同一個變數的位址和值:

child:  &data = 0x404038, data = 2
parent: &data = 0x404038, data = 1

在真正的 Linux 上實際執行的結果(Compiler Explorer 的 Linux x86-64 環境,gcc 13.3,2026-11-04)。

位址一模一樣,值卻不同。如果 0x404038 是記憶體晶片上的真實位置,這不可能發生。

程式裡看到的都是虛擬位址(virtual address)。CPU 每一次讀寫,都由硬體裡的 MMU(記憶體管理單元)換成實體位址(physical address),換算表由核心替每個行程各準備一份。所以同一個虛擬位址,在兩個行程可以對到不同的實體位置。

這樣設計換來的東西:

好處 怎麼做到
隔離 行程根本說不出別人的實體位址,它能用的位址都要經過自己的換算表
不必連續 程式看到連續的陣列,實體上可以散在任何地方,外部碎片的問題就消失了
用到才配置 換算表可以先不填,真的存取時才給實體記憶體(下一課)
共用 兩個行程的不同位址可以對到同一塊實體記憶體,例如同一份 libc

代價是每一次記憶體存取都多一道換算。這一課後半段就在處理這個代價。

互動練習:打開兩個行程的頁表

教學核心的頁表是真的資料結構。用「執行一步」走到兩個行程都印完,途中在「記憶體」面板切換行程,對照「虛擬頁」和「實體頁框」兩欄。真實系統的 pid 與位址每次不同,教學核心沒有隨機化,每次都一樣。

頁、頁框與位移

如果每個位元組都記一筆換算,表會比記憶體還大。所以把記憶體切成固定大小的塊來對應:虛擬位址空間的一塊叫頁(page),實體記憶體的一塊叫頁框(frame),大小相同,x86-64 Linux 上常見的是 4 KB(2¹² 位元組)。

一頁裡的位元組順序不會變,所以只需要換算「第幾頁」,頁內的位置直接照抄:

虛擬位址  0x404abc
          ------ ---
          頁號    位移(最低 12 位元,也就是十六進位最後 3 位)
          0x404   0xabc

查頁表:第 0x404 頁在頁框 0x1f2
實體位址 = 0x1f2 × 4096 + 0xabc = 0x1f2abc

頁表(page table)就是這張「頁號 → 頁框」的對照表,由核心維護,放在記憶體裡,CPU 的 MMU 依照它換算。每個行程一份,切換行程時也切換頁表。

頁表的一項除了頁框編號,還有幾個位元:

位元 意思
Present 這頁現在有沒有在記憶體裡
Writable 能不能寫
User 使用者模式能不能存取(核心的頁在這裡擋住)
NX 不能當指令執行
Accessed、Dirty 硬體記錄「最近用過」「改過」,第 15 課會用到

下面這個工具用 16 位元的小位址空間來練習,真實的 x86-64 是 48 位元,第 14 課會講。

互動練習:親手換算位址

16 位元的位址、每頁 4096 位元組,所以十六進位最高的那一位就是頁號,後三位是位移。disk 代表這頁現在在磁碟上,空的項目代表這頁不屬於這個程式。

SIGSEGV 就是從這裡來的

每次記憶體存取,MMU 都會檢查權限。不符合就觸發分頁錯誤(page fault),控制權交給核心,由核心決定怎麼辦。如果這個存取本來就不合法,核心送 SIGSEGV 給行程。

char array[] = "hello";       // 陣列在堆疊上,可以改
const char *literal = "hello"; // 字串常數在唯讀的頁上

真實 Linux 上,子行程寫字串常數被終止:

array: Hello
child killed by signal 11 (SIGSEGV is 11)

在真正的 Linux 上實際執行的結果(Compiler Explorer 的 Linux x86-64 環境,gcc 13.3,2026-11-04)。

  • char array[] 是把字串複製一份到堆疊,可寫。
  • char *s = "hello" 只是讓指標指向唯讀區段裡的字串,寫它就是違反權限位元。
  • 讀 NULL(位址 0)同理:第 0 頁故意不對應任何東西,所以空指標會當場被抓到,不會默默讀到垃圾。

所以 SIGSEGV 不是 C 語言在檢查你的程式,C 根本不檢查;是硬體加核心在保護這個位址空間。陣列超出邊界如果還落在合法的頁裡,不會有任何錯誤。

互動練習:寫入唯讀的頁

跑一次看核心怎麼說,再把它修好。系統呼叫面板會顯示核心處理分頁錯誤時的判斷;記憶體面板看得到字串所在的頁是「唯讀」。

查頁表太慢:TLB

頁表放在記憶體裡。如果每次存取都要先讀頁表,一次記憶體存取就變成兩次;x86-64 的頁表有四層(下一課),未快取時要多讀四次。

解法是快取最近的換算結果:MMU 裡有一小塊很快的硬體叫 TLB(Translation Lookaside Buffer),記著最近用過的「頁號 → 頁框」。

虛擬位址 → 拆出頁號 → 查 TLB
                       ├─ 命中:立刻得到頁框
                       └─ 未命中:查頁表,把結果放進 TLB(可能擠掉別項)

TLB 通常只有幾十到幾千項,卻能涵蓋大部分存取,靠的是區域性(locality):

  • 空間區域性:用到一個位址,很快會用到旁邊的。一頁有 4096 位元組,循序讀 int 陣列時,同一頁連續命中 1023 次才換下一頁。
  • 時間區域性:用過的很快會再用,例如迴圈裡的變數。

下面這個工具真的產生每一次存取的位址,在一個 LRU 的 TLB 裡查,命中率是算出來的。先猜猜看,再按下去。

互動練習:陣列循序與隨機存取的命中率

藍色是頁號,綠色是位移。選好設定之後按「跑完這一輪」,每跑完一種組合,下面的表格會留一筆讓你比較。

真實機器上看得到這個差距嗎

下面是在 Linux 上用真的記憶體量的:隨機順序讀 N 頁,每頁讀一個位元組(每頁取不同位置,避開資料快取的衝突),印出平均每次讀多久。

第 1 次:    16 pages:   0.6 ns per read
   256 pages:   0.7 ns per read
  4096 pages:   9.6 ns per read
 16384 pages:   9.1 ns per read
(sum 15995392)
第 2 次:    16 pages:   0.6 ns per read
   256 pages:   0.8 ns per read
  4096 pages:   7.4 ns per read
 16384 pages:   9.0 ns per read
(sum 15995392)
第 3 次:    16 pages:   1.9 ns per read
   256 pages:   2.4 ns per read
  4096 pages:  10.0 ns per read
 16384 pages:  10.2 ns per read
(sum 15995392)

在真正的 Linux 上實際執行的結果(Compiler Explorer 的 Linux x86-64 環境,gcc 13.3,2026-11-04)。

頁數超過幾千之後明顯變慢,三次都有這個趨勢,數字本身每次不同。這個實驗不是純粹的 TLB 效應:頁數變多時,資料快取也開始裝不下,兩者一起造成了變慢。

寫程式時的實務結論和上面一樣:走訪資料的順序會影響速度。二維陣列照記憶體順序(C 語言是逐列)走,比跳著走快;資料放得緊密,比用指標串得到處都是快。

用程式寫一個 TLB

只有 2 項的 TLB,看 LRU 怎麼決定擠掉誰。注意第四個位址:第 1 頁被用過之後,變成「最近用過」,所以後面被擠掉的是第 2 頁。

內容切換時的 TLB

TLB 記的是「頁號 → 頁框」,但兩個行程的同一個頁號對到不同的頁框。切換行程之後,舊的項目就是錯的。

  • 最簡單的作法:切換時清空 TLB。 新行程的前幾次存取幾乎都未命中,這是內容切換隱藏的成本,比存暫存器還貴。
  • ASID(Address Space ID,x86 上叫 PCID):每個 TLB 項目標上屬於哪個位址空間,切換時不用清空,各查各的。

回到上面的工具,用「行程切換」選項比較一下(選「隨機、16 頁、TLB 64」):兩個行程輪流跑,每 1000 次存取切換一次。清空 TLB 時命中率會掉;有 ASID 時,64 項裝得下兩個行程的頁,幾乎不受影響。

同一個行程裡的執行緒切換不換頁表,TLB 不受影響,這也是執行緒切換比行程切換便宜的原因之一。

實作:算出命中次數,和工具對答案

補完 simulate,它接受一串位址和 TLB 大小,回傳命中與未命中次數。三組輸入對應工具裡的三種設定(循序 16 頁、跨頁 64 頁 2 遍、跨頁 16 頁 2 遍),你可以回到工具對照表格裡的數字。

你的輸出
預期輸出

確認一下

1.

頁大小是 4 KB,虛擬位址 0x7ab3c 的頁號和位移是什麼?

2.

下面這段程式會怎樣?

int a[10];
a[12] = 5;
3.

一個程式走訪一個 1 GB 的陣列,每個元素 8 位元組,循序讀一次。頁大小 4 KB,TLB 有 64 項。TLB 命中率大約多少?

4.

切換到另一個行程後,如果 CPU 沒有 ASID 也沒有清空 TLB,會發生什麼事?

5.

同一個行程裡切換執行緒,TLB 需要清空嗎?

第 1 次作答

重點整理

  • 程式看到的是虛擬位址,MMU 依每個行程自己的頁表換成實體位址,所以行程彼此隔離,實體記憶體也不必連續。
  • 位址切成頁號和位移,只換算頁號;4 KB 的頁,位移是最低 12 位元。
  • 頁表項還有權限位元。違反權限或沒有對應時觸發分頁錯誤,不合法就是 SIGSEGV;陣列越界如果落在合法的頁裡,硬體看不出來。
  • TLB 快取最近的換算結果,靠區域性才有效:循序存取幾乎全部命中,隨機存取的命中率約等於 TLB 能裝下的比例,迴圈走的頁數超過 TLB 時可能完全不命中。
  • 切換行程時舊項目是錯的,要清空或用 ASID;切換同一行程的執行緒不必。

延伸閱讀:OSTEP 的 Paging: Introduction、Paging: Faster Translations (TLBs)。

下一課:4 KB 一頁、48 位元的位址,頁表本身要多大?fork 又為什麼能這麼快?

這一課完成了!
Next lesson:多層頁表與寫入時複製
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。