Bộ nhớ/MMU & TLB — dịch địa chỉ mà không chậm
17/26
Bài 17 / 26~18 phútBộ nhớ ảoMiễn phí lượt xem

MMU & TLB — dịch địa chỉ mà không chậm

MMU là phần cứng dịch mọi địa chỉ ảo qua page table; TLB cache bản dịch gần đây để gần như miễn phí. Hiểu tại sao TLB hiệu quả và chi phí khi nó miss.

TL;DR: Mỗi lần chương trình đọc hay ghi bộ nhớ, địa chỉ ảo phải được dịch sang địa chỉ vật lý. Việc đó do MMU (Memory Management Unit) — một phần cứng tích hợp trong CPU — thực hiện bằng cách walk qua page table. Vấn đề: page table nằm trong RAM (~100 ns mỗi lần truy cập), nên mỗi truy cập bộ nhớ lại tốn thêm vài lần truy cập RAM để dịch — tới ~5 lần chi phí với page table 4 cấp. TLB (Translation Lookaside Buffer) là một cache cực nhỏ, cực nhanh nằm ngay trong MMU, lưu các bản dịch VPN→PFN gần đây. TLB hit dịch địa chỉ chỉ tốn ~1 chu kỳ; TLB miss mới phải walk page table. Nhờ locality (bài module 2), TLB hit rate thường vượt 99% — dịch địa chỉ gần như miễn phí trong thực tế.

Bạn chạy một vòng lặp duyệt mảng 1 triệu phần tử. Mỗi phần tử là một truy cập bộ nhớ — tức 1 triệu lần dịch địa chỉ. Nếu mỗi lần phải walk page table 4 cấp xuống RAM (~400 ns theo ước lượng lý thuyết — con số thực tế thấp hơn nhờ page walk cache, xem mục Đào sâu), vòng lặp mất thêm hàng trăm ms chỉ cho dịch địa chỉ. Nhưng thực tế vòng lặp không chậm đến vậy.

Câu trả lời là TLB. Bài này giải thích MMU làm gì, TLB hoạt động thế nào, vì sao nó hiệu quả, và cái giá phải trả khi TLB bị flush.

1. Analogy — sổ tay tra nhanh của lễ tân

Lễ tân khách sạn (bài 01) có bảng tra phòng lớn trong kho — nhưng kho nằm ở tầng hầm, mỗi lần xuống mất 2 phút. Nếu cứ mỗi yêu cầu đặt phòng lại phải xuống hầm tra bảng, khách xếp hàng dài.

Lễ tân khôn ngoan giữ một sổ tay nhỏ trên bàn: ghi lại 50–100 cặp "số phòng ảo → số phòng thật" của những khách hay gặp nhất gần đây. Khi khách hỏi, tra sổ tay trước — tìm thấy ngay (TLB hit); không có mới xuống hầm (TLB miss), rồi cập nhật sổ tay cho lần sau.

Khách sạnMMU & TLB
Bảng tra phòng dưới hầmPage table trong RAM
Sổ tay 50 cặp trên bànTLB (~64–2048 entry)
Tra sổ tay: tức thìTLB hit: ~1 chu kỳ
Xuống hầm tra: 2 phútTLB miss: page table walk
Khách hay lui tớiVùng nhớ có locality cao
Đón đoàn khách mới (đổi ca)Context switch: flush TLB
💡 Cách nhớ

OS dựng page table; MMU (phần cứng) dùng nó để dịch. TLB là cache của MMU cho bản dịch — giống cache L1/L2 là cache cho dữ liệu. Hit = nhanh; miss = chậm hơn nhưng hiếm khi xảy ra nếu có locality.

2. MMU — phần cứng dịch mọi địa chỉ

MMU (Memory Management Unit) là phần cứng tích hợp trong CPU, đứng giữa CPU core và bus bộ nhớ. Mọi địa chỉ CPU phát ra — kể cả địa chỉ để nạp lệnh — đều đi qua MMU trước khi ra bus.

MMU dịch địa chỉ bằng cách walk page table: với page 4 cấp (x86-64), nó đọc lần lượt PML4 → PDPT → PD → PT, mỗi cấp dùng 9 bit của VPN làm chỉ số. Kết quả là PFN trong PTE cấp cuối, ghép với offset ra địa chỉ vật lý.

flowchart LR
  CPU["CPU<br/>(phat VA)"] --> TLB{"TLB<br/>hit?"}
  TLB -->|hit| PA["Dia chi vat ly (PA)"]
  TLB -->|miss| Walk["Page table walk<br/>(MMU doc RAM)"]
  Walk -->|cap nhat TLB| TLB
  Walk --> PA
  PA --> Cache["L1/L2/L3 Cache<br/>+ RAM"]

Phân biệt OS và MMU: page table là cấu trúc dữ liệu do OS tạo và cập nhật (khi cấp phát vùng nhớ, khi swap trang, khi context switch). MMU chỉ đọc page table theo địa chỉ gốc được lưu trong thanh ghi CR3 (x86) — nó là phần cứng, không có logic OS. Khi OS muốn đổi page table (context switch), nó ghi địa chỉ page table mới vào CR3.

3. Chi phí nếu không có TLB

Để thấy TLB quan trọng thế nào, hãy tính chi phí không có nó. Với x86-64 dùng 4-level page table, mỗi lần dịch địa chỉ cần 4 lần đọc RAM:

Khong co TLB — chi phi moi truy cap bo nho:
  1 lan doc PML4       ~100 ns  (RAM)
  1 lan doc PDPT       ~100 ns
  1 lan doc PD         ~100 ns
  1 lan doc PT         ~100 ns
  1 lan doc du lieu    ~100 ns  (truy cap that su)
  -----------------------------------------------
  Tong                 ~500 ns  (gap 5x chi phi goc)

Vòng lặp 1 triệu phần tử: nếu mỗi truy cập tốn 500 ns → 500 ms chỉ cho bộ nhớ. Thực tế với TLB hit rate 99%:

Co TLB (hit rate 99%):
  990.000 lan TLB hit: ~1 chu ky x 990.000    ~1 ms
   10.000 lan TLB miss: ~400 ns x 10.000      ~4 ms
  -----------------------------------------------
  Tong                                        ~5 ms

Khác biệt ~100 lần. TLB biến việc dịch địa chỉ từ nút cổ chai thành chi phí không đáng kể.

4. Vì sao TLB hit rate cao — locality (lại!)

TLB chỉ lưu vài chục tới vài nghìn entry (L1 TLB thường 64–128 entry, L2 TLB 512–2048). Tại sao lại đủ?

locality (đã gặp ở module 2): chương trình không nhảy khắp không gian địa chỉ — nó tập trung vào một số trang trong khoảng thời gian dài (temporal locality) và duyệt các trang liền nhau (spatial locality). Code ở một vài trang; dữ liệu hot (mảng đang duyệt, stack frame hiện tại) ở một vài trang.

Vi du: duyet mang 64 MB (page 4 KB = 16.384 trang)
  - Duyet tuan tu: tai mot thoi diem, chi can ~2-4 trang hien hanh
  - Moi trang phuc vu ~1.024 phan tu int
  - TLB 64 entry la du cho toan bo working set cua vong lap

Trang nào đã dịch gần đây sẽ còn trong TLB cho các truy cập tiếp theo trong cùng trang đó. Một cache line 64 byte nằm gọn trong một trang → cả block spatial locality được phục vụ từ một TLB entry.

TLB và cache dữ liệu — hai tầng cache khác nhau

TLB và cache L1/L2/L3 là hai thứ khác nhau. Cache dữ liệu lưu nội dung của bộ nhớ (dữ liệu thật). TLB lưu bản dịch địa chỉ (VPN → PFN) — không có dữ liệu, chỉ có địa chỉ. Một truy cập bộ nhớ phải qua cả hai: TLB dịch địa chỉ trước, rồi địa chỉ vật lý đi tới cache dữ liệu. TLB miss và cache miss là hai sự kiện riêng biệt; cả hai đều có chi phí, nhưng TLB miss thường đắt hơn vì cần walk page table nhiều cấp.

5. Multi-level page table — vì sao cần

Bài 02 đặt vấn đề: page table phẳng cho 48-bit address space cần 2^36 entry × 8 byte = 512 GB mỗi tiến trình — không khả thi.

Multi-level page table giải quyết bằng cách phân cấp: thay vì một mảng khổng lồ, dùng một cây thưa. x86-64 dùng 4 cấp, mỗi cấp dùng 9 bit của VPN:

Dia chi ao 48-bit x86-64:

  [9 bit PML4][9 bit PDPT][9 bit PD][9 bit PT][12 bit offset]
      ^cấp 1      ^cấp 2     ^cấp 3   ^cấp 4

  Moi cap: bang 512 entry x 8 byte = 4 KB (vua 1 trang!)
  Chi cap phat bang cho phan cay thuc su duoc dung.
CR3 → PML4 (cấp 1)
↓ 9 bit [47:39]
PDPT (cấp 2)
↓ 9 bit [38:30]
PD (cấp 3)
↓ 9 bit [29:21]
PT (cấp 4) → PFN
↓ 12 bit offset
Địa chỉ vật lý

Lợi ích then chốt: tiến trình thưa (chỉ dùng vài vùng nhỏ trong không gian ảo khổng lồ) chỉ cần cấp phát các nhánh cây thật sự ánh xạ — phần còn lại của cây không tồn tại. Chi phí thực: bộ nhớ cho page table tỉ lệ với số trang thật sự được ánh xạ (O(số trang)) thay vì cố định 512 GB; còn mỗi lần dịch chỉ tốn số bước bằng số cấp = 4 (hằng số), không phụ thuộc kích thước không gian địa chỉ.

Nhược điểm: mỗi TLB miss giờ cần 4 lần đọc RAM (4 cấp) thay vì 1 — đó là lý do TLB hit rate cao hơn lại càng quan trọng.

6. Context switch và TLB flush

Khi OS switch từ tiến trình A sang B, page table thay đổi (OS ghi CR3 mới). Tất cả entry trong TLB hiện tại đều thuộc về A — chúng không còn hợp lệ cho B.

Giải pháp đơn giản: flush TLB. Ghi CR3 trên x86 tự động flush các entry không phải global (kernel mapping được đánh dấu Global qua CR4.PGE vẫn được giữ — để khỏi nạp lại sau mỗi switch). Chi phí: entry của tiến trình cũ bị xoá, các truy cập đầu tiên sau switch đều là TLB miss → page walk.

Chi phi context switch co TLB flush:
  Sau switch, N truy cap dau tien:
    N x TLB miss x 400 ns (4-level walk)
  N phu thuoc working set size va duoc fill dan.
  He thong switch nhieu (scheduler goi nhieu) -> overhead dang ke.

Giải pháp tối ưu: ASID/PCID. Mỗi TLB entry được gắn thêm một Address Space ID (ASID) (hoặc PCID trên x86). MMU chỉ chấp nhận hit khi ASID của entry khớp với ASID hiện tại. Khi switch A → B, OS chỉ đổi ASID hiện tại — TLB entry của A vẫn còn đó nhưng bị bỏ qua. Khi quay lại A, ASID khớp → entry A vẫn dùng được, không cần refill. Chi phí switch giảm đáng kể.

7. Pitfall tổng hợp

Nhầm 1: Nghĩ TLB là cache dữ liệu. TLB chỉ lưu bản dịch địa chỉ (VPN→PFN), không lưu dữ liệu thật. TLB hit nghĩa là "đã biết địa chỉ vật lý", không phải "dữ liệu đã trong cache".

Nhầm 2: Tưởng TLB flush chỉ xảy ra khi context switch. TLB entry cũng bị vô hiệu khi OS thay đổi PTE (unmap trang, thay đổi quyền). OS phải chủ động flush entry liên quan — trên đa nhân, cần TLB shootdown: gửi IPI (inter-processor interrupt) tới mọi core để flush entry đó trên toàn hệ thống (xem Đào sâu).

Nhầm 3: Bỏ qua huge pages để tối ưu TLB. Một TLB entry cho trang 4 KB phủ 4 KB; một entry cho huge page 2 MB phủ 512 lần nhiều hơn. Với working set lớn hơn TLB coverage (nhiều trang nhỏ → nhiều miss), chuyển sang huge pages có thể giảm TLB miss rate đáng kể — đặc biệt với database, JVM heap lớn.

8. Đào sâu (tuỳ chọn)

📚 Đào sâu (tuỳ chọn)

Hardware vs software TLB walk: x86 và ARM dùng hardware page table walker — MMU tự walk page table khi miss, không cần OS can thiệp. Một số kiến trúc (MIPS, SPARC cũ) dùng software TLB miss handler: MMU miss → exception → kernel handler chạy bằng code để nạp entry vào TLB. Linh hoạt hơn (page table format tuỳ ý) nhưng miss đắt hơn (trap vào kernel).

ASID/PCID chi tiết: x86-64 hỗ trợ PCID (Process-Context Identifier) 12 bit — cho phép giữ TLB entry của nhiều address space cùng lúc. Khi CR3 được ghi với bit PCID, CPU không flush TLB. Linux dùng PCID kể từ 4.14 (cùng lúc với Meltdown/KPTI patches) để giảm overhead KPTI.

TLB shootdown đa nhân: khi một core thay đổi PTE (OS unmap, fork+CoW, munmap), các core khác có thể vẫn cache entry cũ trong TLB của họ — dẫn tới sử dụng ánh xạ stale. OS phải gửi IPI (Inter-Processor Interrupt) tới tất cả core đang chạy tiến trình đó, bắt họ flush entry liên quan. Thao tác này gọi là TLB shootdown — đắt trên hệ thống nhiều core (hàng chục µs). Đây là một lý do munmapmprotect chậm hơn bạn nghĩ.

Page walk cache: một số CPU (Intel Haswell trở lên) có thêm page walk cache — cache riêng cho các intermediate level của page table walk (PML4, PDPT, PD). TLB miss vẫn cần walk, nhưng các cấp trên thường được serve từ page walk cache (~4–10 chu kỳ) thay vì RAM (~100 ns). Kết quả: TLB miss thực tế thường ~20–40 chu kỳ, không phải ~400 ns như tính lý thuyết thuần.

9. Liên hệ các bài khác

10. Tóm tắt

  • MMU là phần cứng trong CPU dịch mọi địa chỉ ảo sang vật lý bằng cách walk page table. OS dựng và cập nhật page table; MMU chỉ đọc nó.
  • Không có TLB, mỗi truy cập bộ nhớ cần thêm 4 lần đọc RAM (4-level page walk) — tốn ~500 ns thay vì ~100 ns (ước lượng lý thuyết; thực tế thấp hơn nhờ page walk cache — xem mục 8).
  • TLB là cache nhỏ (~64–2048 entry) ngay trong MMU, lưu bản dịch VPN→PFN gần đây. TLB hit ~1 chu kỳ; TLB miss → page walk.
  • TLB hiệu quả nhờ locality: chương trình tập trung vào ít trang tại một thời điểm → hit rate thường vượt 99%.
  • Context switch đổi CR3 → flush TLB (hoặc dùng ASID/PCID để tránh flush). Chi phí flush: mọi truy cập đầu tiên sau switch đều miss → page walk.
  • Huge pages (2 MB/1 GB) giảm số TLB entry cần thiết, giúp với working set lớn hơn TLB coverage.

11. Tự kiểm tra

Tự kiểm tra
Q1
Phân biệt vai trò của OS và MMU trong việc dịch địa chỉ. Ai dựng page table, ai dùng nó?
OS (kernel) dựng và duy trì page table: tạo khi tiến trình mới, thêm entry khi cấp phát bộ nhớ, cập nhật khi swap trang, xoá khi giải phóng. OS lưu địa chỉ gốc của page table vào thanh ghi CR3 (x86). MMU là phần cứng: nó đọc CR3 để biết page table nằm ở đâu, rồi tự walk qua các cấp mỗi khi TLB miss — hoàn toàn tự động, không cần OS can thiệp (với hardware walker như x86/ARM). Sự phân chia này quan trọng: OS có quyền thay đổi cấu trúc bộ nhớ (thêm/xoá/thay đổi quyền trang), MMU chỉ thực thi ánh xạ đó trung thành. Khi OS thay đổi PTE, nó cũng phải flush TLB liên quan để MMU không dùng bản dịch stale.
Q2
Vì sao TLB hit rate thường vượt 99% dù TLB chỉ có 64–128 entry?
Vì chương trình có locality cao. Tại một thời điểm, chương trình chỉ hoạt động trên một tập nhỏ các trang — gọi là working set. Code nằm trong vài trang; dữ liệu hot (vòng lặp hiện tại, stack frame, mảng đang duyệt) cũng chỉ chiếm vài trang. Một TLB entry phủ toàn bộ trang 4 KB — khi duyệt mảng, mỗi entry phục vụ ~1.024 phần tử int hoặc ~512 lần truy cập 8-byte. Với temporal locality: cùng trang được truy cập nhiều lần liên tiếp → entry trong TLB được tái dùng nhiều. Với spatial locality: các truy cập liền nhau trong cùng trang → chỉ cần một entry. 64 entry thường đủ để phủ toàn bộ working set đang hoạt động, nên miss hiếm.
Q3
Vì sao x86-64 dùng 4-level page table thay vì page table phẳng? Chi phí của multi-level là gì?
Page table phẳng cho không gian 48-bit cần 2^36 entry × 8 byte = 512 GB mỗi tiến trình — không khả thi. 4-level page table là cây thưa: chỉ cấp phát các nhánh thật sự được dùng. Tiến trình chỉ ánh xạ vài MB (code, heap, stack) → chỉ cần vài KB page table thay vì 512 GB. Chi phí: mỗi TLB miss phải walk 4 cấp, mỗi cấp là một lần đọc RAM (~100 ns) → miss tốn ~400 ns (lý thuyết). Thực tế nhờ page walk cache (cache cho các bảng cấp trên), miss thường chỉ ~20–40 chu kỳ. Đây là lý do TLB hit rate cao lại càng quan trọng: miss đắt hơn với multi-level.
Q4
Vì sao context switch làm tăng TLB miss rate? ASID/PCID giải quyết vấn đề đó thế nào?
Context switch đổi CR3 sang page table của tiến trình mới. Tất cả entry trong TLB đều thuộc về tiến trình cũ — không còn hợp lệ. Ghi CR3 trên x86 tự động flush các TLB entry không phải global (kernel mapping đánh dấu Global vẫn được giữ). Sau switch, mọi truy cập bộ nhớ ban đầu của tiến trình mới đều là TLB miss, phải walk page table từ đầu → burst miss làm chậm hệ thống, đặc biệt khi scheduler switch nhiều. PCID (x86) gắn một ID 12-bit vào mỗi TLB entry. Khi ghi CR3 với PCID mới, CPU không flush TLB — entry tiến trình cũ vẫn còn nhưng bị bỏ qua vì PCID không khớp. Khi quay lại tiến trình cũ với PCID cũ, entry vẫn hợp lệ. Kết quả: không cần refill TLB sau mỗi switch nếu PCID đã biết — giảm đáng kể overhead context switch.
Q5
Phân biệt TLB miss và cache miss. Một truy cập bộ nhớ có thể gặp cả hai không?
TLB miss: bản dịch VPN→PFN không có trong TLB → MMU phải walk page table (đọc RAM nhiều cấp) để tìm PFN. Kết quả là địa chỉ vật lý. Cache miss: địa chỉ vật lý đã biết rồi, nhưng dữ liệu tại địa chỉ đó không có trong L1/L2/L3 cache → phải đọc RAM để lấy dữ liệu thật. Hai sự kiện hoàn toàn độc lập: một truy cập có thể gặp cả hai (TLB miss để dịch địa chỉ, rồi cache miss để lấy dữ liệu) — trường hợp xấu nhất, phải đọc RAM nhiều lần (page walk + dữ liệu). Có thể TLB hit nhưng cache miss (đã biết PFN, nhưng dữ liệu chưa trong cache). Hoặc TLB hit + cache hit (cả hai sẵn sàng) — đây là trường hợp thường gặp nhờ locality.
Q6
Vì sao huge pages (2 MB thay vì 4 KB) giúp giảm TLB pressure? Khi nào nên dùng?
Một TLB entry cho trang 4 KB phủ 4 KB không gian địa chỉ. Một TLB entry cho huge page 2 MB phủ 512 lần nhiều hơn (2 MB / 4 KB = 512). Với TLB 64 entry: trang 4 KB → phủ tối đa 256 KB; huge page 2 MB → phủ tối đa 128 MB. Khi working set lớn hơn TLB coverage (vd: database buffer pool 8 GB, JVM heap lớn, HPC array), huge pages giảm số entry cần thiết → giảm TLB miss rate đáng kể. Nên dùng khi: (1) working set lớn và liên tục, (2) đo được TLB miss cao qua perf stat -e dTLB-misses. Không nên dùng khi: working set nhỏ (lãng phí internal fragmentation lớn hơn — trang cuối có thể phí gần 2 MB) hoặc bộ nhớ cấp phát thưa.

Bài tiếp theo: Page fault & swap

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên