- Giải thích cấu trúc bộ nhớ tiến trình của Linux ở mức hoạt động thực tế, đồng thời diễn giải từng bước mối quan hệ giữa không gian địa chỉ ảo và bộ nhớ vật lý
- Tập trung vào các cơ chế cốt lõi như bảng trang, VMA, mmap, page fault, CoW để mô tả cụ thể cách tiến trình sở hữu và truy cập bộ nhớ
- Giới thiệu cách quan sát trạng thái bộ nhớ theo từng tiến trình thông qua hệ thống tệp
/proc, cùng vai trò của các công cụ chẩn đoán nâng cao như pagemap, kpageflags
- Đề cập đến tối ưu hiệu năng và kỹ thuật theo dõi dirty trong không gian người dùng thông qua các tính năng kernel mới như Transparent Huge Pages (THP), userfaultfd, PAGEMAP_SCAN
- Đồng thời giải thích các nguyên lý thiết kế kernel liên quan đến bảo mật và hiệu năng như PTI để ứng phó Meltdown, TLB flush, chính sách W^X, từ đó giúp hiểu toàn diện về quản lý bộ nhớ trên Linux
Cấu trúc cơ bản của bộ nhớ tiến trình
- Khi chương trình chạy, có vẻ như tồn tại một vùng nhớ liên tục khổng lồ, nhưng trên thực tế kernel Linux động cấu thành nó theo đơn vị trang
- CPU tra cứu bảng trang để chuyển đổi địa chỉ ảo thành frame vật lý
- Nếu không có ánh xạ, sẽ xảy ra page fault và kernel sẽ cấp phát trang mới hoặc trả về lỗi
- Khi RAM vật lý không đủ, kernel sẽ chuyển các trang không dùng tới ra đĩa hoặc loại bỏ các trang tệp để giải phóng không gian
/proc là một hệ thống tệp ảo do kernel dựng lên trong bộ nhớ, dùng để phơi bày trạng thái của tiến trình và kernel dưới dạng tệp
Không gian địa chỉ và VMA
- Mỗi tiến trình có một đối tượng không gian địa chỉ duy nhất, bên trong gồm nhiều VMA (Virtual Memory Area)
- VMA là một dải địa chỉ liên tục có cùng quyền hạn (R/W/X) và cùng backend (bộ nhớ ẩn danh hoặc tệp)
- Bảng trang là cấu trúc mà phần cứng tham chiếu tới, lưu thông tin ánh xạ (PTE) giữa trang ảo và trang vật lý
- Việc thay đổi không gian địa chỉ được thực hiện qua ba system call
mmap: tạo vùng mới
mprotect: thay đổi quyền
munmap: xóa ánh xạ
- Trang có kích thước cơ bản 4KiB, một số hệ thống còn hỗ trợ trang lớn 2MiB và 1GiB
Xem cấu trúc bộ nhớ bằng /proc/self/maps
- Có thể kiểm tra memory map của tiến trình bằng lệnh
cat /proc/self/maps
- Sẽ hiển thị mã thực thi, dữ liệu, bss, heap, ánh xạ ẩn danh, thư viện dùng chung, stack...
- Các vùng
[vdso] và [vvar] là mã và dữ liệu cho system call tốc độ cao do kernel ánh xạ vào
Nguyên lý hoạt động của mmap
mmap không phải là cấp phát bộ nhớ thực ngay lập tức, mà là ghi lại một cam kết đối với không gian địa chỉ
- Trang chỉ được cấp phát tại thời điểm truy cập đầu tiên
- Khi ánh xạ tệp,
offset phải được căn chỉnh theo trang; nếu truy cập vượt quá cuối tệp sẽ phát sinh SIGBUS
MAP_SHARED phản ánh trực tiếp vào tệp, còn MAP_PRIVATE sẽ tạo trang độc lập theo cơ chế copy-on-write (CoW) khi ghi
MAP_FIXED_NOREPLACE giúp đảm bảo an toàn bằng cách thất bại nếu địa chỉ chỉ định đã có ánh xạ
Lần truy cập đầu tiên và page fault
- Khi truy cập lần đầu vào một ánh xạ mới, nếu CPU không tìm thấy mục tương ứng trong bảng trang thì sẽ xảy ra page fault
- Kernel sẽ kiểm tra tính hợp lệ của địa chỉ, quyền truy cập và sự tồn tại
- Nếu là ánh xạ ẩn danh thì cấp phát trang mới được điền số 0; nếu là ánh xạ tệp thì đọc từ page cache
- minor fault là khi dữ liệu đã có sẵn trong RAM, còn major fault là khi cần I/O đĩa
- Stack được bảo vệ bằng guard page, nên nếu truy cập quá xa xuống dưới sẽ phát sinh
SIGSEGV
fork() và Copy-on-Write của MAP_PRIVATE
- Khi
fork, tiến trình cha và con cùng chia sẻ các trang vật lý giống nhau, và tất cả đều được đánh dấu chỉ đọc
- Chỉ tại thời điểm ghi mới sao chép sang trang mới để duy trì tính độc lập
- Ánh xạ tệp
MAP_PRIVATE cũng hoạt động theo nguyên lý tương tự
- Các tùy chọn liên quan
vfork: chia sẻ không gian địa chỉ của tiến trình cha
clone(CLONE_VM): tạo thread
MADV_DONTFORK, MADV_WIPEONFORK: loại trừ ánh xạ khỏi tiến trình con hoặc khởi tạo lại bằng 0
Thay đổi quyền và vô hiệu hóa TLB
- Khi thay đổi quyền của trang bằng
mprotect, kernel sẽ tách VMA và sửa bảng trang, sau đó thực hiện vô hiệu hóa TLB
- Theo chính sách W^X, một trang không thể vừa ghi vừa thực thi cùng lúc
- TLB (Translation Lookaside Buffer) là bộ đệm các phép chuyển đổi địa chỉ gần đây, nên việc vô hiệu hóa có thể gây ra độ trễ ngắn
Quan sát chi tiết qua /proc
- Có thể dùng
/proc/<pid>/maps, smaps, smaps_rollup để kiểm tra quyền của từng vùng, RSS và mức sử dụng HugePage
/proc/<pid>/pagemap cung cấp trạng thái theo đơn vị trang (tồn tại, swap, PFN...), nhưng PFN không công khai cho người dùng thông thường
/proc/kpagecount, /proc/kpageflags hiển thị số lượng ánh xạ và thuộc tính trang theo từng PFN (ẩn danh, tệp, dirty...)
- Có thể dùng
mincore, SEEK_DATA/SEEK_HOLE để nhận diện vùng dữ liệu/lỗ của sparse file
- Có thể kết hợp
PAGEMAP_SCAN với userfaultfd để triển khai theo dõi dirty trong không gian người dùng
Transparent Huge Pages (THP) và mTHP
- THP tự động gộp các vùng nhớ được truy cập thường xuyên thành các trang lớn hơn (như 2MiB) để cải thiện hiệu quả của TLB
- Thread
khugepaged sẽ hợp nhất các trang liền kề
- mTHP hỗ trợ trang lớn biến thiên (folio) với nhiều kích thước như 16KiB, 64KiB
- Có thể kiểm tra việc sử dụng thông qua
AnonHugePages, FilePmdMapped trong /proc/self/smaps
- Quản lý cấu hình toàn hệ thống tại
/sys/kernel/mm/transparent_hugepage/
- Có thể điều khiển theo từng vùng bằng
MADV_HUGEPAGE, MADV_NOHUGEPAGE
Theo dõi dirty trong không gian người dùng
- Có thể dùng
userfaultfd và PAGEMAP_SCAN để chỉ sao chép những trang đã thay đổi
- Kernel thực hiện quét và write-protect trong một phép toán nguyên tử duy nhất
- Hiệu quả cho snapshot, live migration và các tình huống tương tự
Cơ chế TLB flush
- Trên x86, việc vô hiệu hóa TLB có hai cách
INVLPG: vô hiệu hóa một trang đơn lẻ
- Reload gốc bảng trang để flush toàn bộ
PCID và INVPCID giúp quản lý thẻ TLB theo từng tiến trình để giảm các lần flush không cần thiết
tlb_single_page_flush_ceiling là ngưỡng để kernel chọn giữa flush theo trang hay flush toàn bộ
Ứng phó Meltdown: Page Table Isolation (PTI)
- Meltdown là lỗ hổng mà dữ liệu kernel có thể bị lộ qua cache trong quá trình thực thi suy đoán
- Linux dùng PTI (Page Table Isolation) để tách không gian địa chỉ người dùng và kernel
- Khi đi vào kernel sẽ chuyển
CR3 để dùng bảng trang dành riêng cho kernel
- Tận dụng
PCID để giảm thiểu TLB flush
- Tính năng này được bật mặc định và có thể tắt bằng
nopti
Quy trình thay đổi ánh xạ an toàn của kernel
- Khi thay đổi ánh xạ, thứ tự thực hiện là
- Xử lý quy tắc cache
- Sửa bảng trang
- Vô hiệu hóa TLB
- Các ánh xạ nội bộ của kernel (
vmap, vmalloc) cũng đồng bộ cache và TLB trước/sau I/O
- Một số kiến trúc cần flush instruction cache sau khi sao chép mã
Cấu trúc stack và lời gọi hàm trên x86
- Ở chế độ 64-bit, dùng các thanh ghi RIP, RSP, RBP, và stack tăng trưởng theo hướng xuống
- Theo System V AMD64 ABI, tham số được truyền qua RDI, RSI, RDX, RCX, R8, R9, còn giá trị trả về nằm trong RAX
- Chế độ người dùng là ring 3, kernel là ring 0, còn system call và interrupt chuyển đổi qua gate
Tình huống lỗi và chẩn đoán
mmap → EINVAL: lỗi căn chỉnh file offset
mmap → ENOMEM: thiếu không gian ảo hoặc bị giới hạn overcommit
- Truy cập ánh xạ tệp gây
SIGBUS: truy cập vượt EOF
mprotect(PROT_EXEC) → EACCES: mount noexec hoặc chính sách W^X
- RSS tăng sau
fork(): sao chép trang do CoW
- Ghi đè ánh xạ hiện có bằng
MAP_FIXED → nên dùng MAP_FIXED_NOREPLACE
Checklist thực tế
- Cần có bộ nhớ ngay:
mmap + PROT_READ|PROT_WRITE + MAP_PRIVATE|MAP_ANONYMOUS
- Khi sinh mã: duy trì W^X, dùng
mprotect(PROT_READ|PROT_EXEC)
- Khi ánh xạ tệp: căn chỉnh
offset theo trang, không truy cập vượt EOF
- Khi có nhiều page fault: dùng
MADV_WILLNEED hoặc truy cập trước
- Phân tích sử dụng bộ nhớ:
/proc/<pid>/smaps_rollup → /proc/<pid>/maps
fork tiến trình lớn: cân nhắc CoW, dùng exec trong tiến trình con
- Môi trường nhạy với độ trễ: theo dõi THP/mTHP,
mlock, và hành vi TLB
1 bình luận
Ý kiến trên Hacker News
Rất thích những bài giải thích ngắn như thế này
Dù là nội dung đã biết rồi, khi đọc vẫn giúp xác nhận lại một lần nữa
Khi thấy những cụm như “mmap, without the fog”, tôi lại có cảm giác đây là bài viết có LLM đồng tác giả, nên tự nhiên thấy bất an và khó chịu
Lại còn có những cách diễn đạt kỳ lạ như “without the fog”, nên tạo cảm giác như chatgpt cũng góp phần viết vào
Đọc đoạn nói về instruction pipelining lại khiến tôi muốn quay về thời kiến trúc đơn giản như 6502 ngày xưa
Khi đó mọi thứ hoạt động “đúng như nó là”, không cần ánh xạ hay proxy phức tạp
Nếu có interconnect đủ nhanh, có lẽ ta vẫn có thể mơ về kiểu đơn giản đó thêm lần nữa
Nhưng nhìn vào các vấn đề như Meltdown, Spectre thì rõ ràng cái giá của sự phức tạp gia tăng cũng không hề nhỏ
Trong thời điểm hiện tại, khi định luật Moore đang chạm tới giới hạn, tôi tự hỏi liệu đánh đổi về độ phức tạp như vậy còn là lựa chọn tốt nhất hay không
Tôi không nghĩ sự đơn giản lúc nào cũng tốt hơn
Trang web hiện thông báo là đã bị chặn vì thuộc miền nguy hiểm hoặc không an toàn
Kết quả kiểm tra trên VirusTotal cho thấy không có vấn đề gì
Tôi không rõ ý nói rằng báo cáo lỗi chỉ là “noise” là như thế nào