Hình dung cuốn sách bạn đang tra cứu. Nếu nó đã nằm trên bàn làm việc, mở ra đọc là chuyện tức thì — dù bạn có dán một tờ giấy "CẤM cất cuốn này xuống kho" lên nó hay không, tốc độ đọc y hệt. Tờ giấy đó không làm chữ hiện ra nhanh hơn. Cái nó làm là hứa rằng thủ thư sẽ không bao giờ mang cuốn sách xuống cái kho dưới tầng hầm xa lắc — nên bạn không bao giờ phải chờ mười phút đi lấy lại nó giữa lúc đang cần gấp. mlock chính là tờ giấy đó, và rất nhiều người nhầm nó với một nút tăng tốc.
Có một lời gọi hệ thống nghe rất hấp dẫn với người muốn tối ưu: mlock. Nó "khóa" một vùng nhớ vào RAM, ngăn hệ điều hành đẩy các trang đó ra swap. Nhiều người nghe "khóa trong RAM" liền nghĩ "vậy truy cập vùng đó sẽ nhanh hơn — nó luôn trong bộ nhớ mà". Tôi đo ba thứ trong container gcc:13: chi phí chạm-lần-đầu một vùng có/không mlock, chi phí của chính lời gọi mlock, và tốc độ truy cập steady-state khi trang đã nằm trong RAM. Kết quả buộc tôi tách bạch cái mlock thật sự làm với cái người ta tưởng nó làm.
mlock làm gì, và bộ nhớ ảo lười thế nào
Nhớ lại phần 6 về page fault: khi bạn mmap một vùng, nhân không cấp trang vật lý ngay — nó cấp lười, và trang thật chỉ được nạp vào lần đầu bạn chạm tới (một page fault). Đó là lý do lần chạm đầu tiên đắt hơn các lần sau. mlock(addr, len) bảo nhân hai điều: (1) nạp ngay mọi trang của vùng đó vào RAM (prefault), và (2) ghim chúng lại — cấm swap ra đĩa cho tới khi munlock.
Điều quan trọng cần tách bạch: mlock không làm bộ nhớ chạy nhanh hơn. RAM là RAM; một trang đã nằm trong RAM thì đọc/ghi nó tốn đúng bằng nhau, dù có bị mlock hay không — đúng cuốn sách trên bàn, đọc như nhau dù có dán giấy hay không. Cái mlock đổi là (a) khi nào trang được nạp (ngay, thay vì lười), và (b) đảm bảo trang không bao giờ rời RAM. Tôi đo để thấy rõ cả hai — và để bác cái niềm tin "nhanh hơn".
Đo: prefault thật, nhưng truy cập không nhanh hơn
Đầu tiên, chạm-lần-đầu một vùng mmap mới 16 MB (4096 trang), có và không mlock trước:
CHẠM-LẦN-ĐẦU (16 MB = 4096 trang):
KHÔNG mlock : 68,6 ns/trang (mỗi trang trả một page fault)
chi phí mlock: 50 ns/trang (mlock phải fault vào hết mọi trang -> 204 µs tổng)
CÓ mlock : 8,3 ns/trang (đã prefault, không còn fault -> nhanh 8×)
Không mlock, mỗi trang chạm lần đầu trả một page fault ~68,6 ns. Sau khi mlock, chạm-lần-đầu chỉ còn 8,3 ns/trang — nhanh 8 lần, vì trang đã resident, không còn fault. Đây là lợi ích prefault thật, giống hệt MAP_POPULATE mà phần 7 đã gặp.
Nhưng nhìn dòng giữa: bản thân mlock tốn ~50 ns/trang (204 µs cho 16 MB) — vì nó phải fault vào từng trang ngay lúc gọi. Nghĩa là mlock không xóa phí page fault; nó chỉ dời phí đó từ lười (rải ra khi bạn chạm) sang sớm (dồn vào lời gọi mlock). Tổng công gần như không đổi: mlock (50) + chạm (8,3) ≈ 58 ns/trang, xấp xỉ 68,6 ns của chạm-lười-có-fault. Cái bạn được không phải "ít việc hơn", mà là "biết trước khi nào trả phí" — một khác biệt quan trọng cho độ trễ ổn định.
Giờ đến câu hỏi cốt lõi — steady-state, khi trang đã nằm trong RAM, đọc lặp lại có nhanh hơn nhờ mlock không?
STEADY-STATE (trang đã resident, đọc mỗi 64 byte):
KHÔNG mlock : 0,66 ns/dòng-cache
CÓ mlock : 0,59 ns/dòng-cache
-> Y HỆT (chênh nằm trong nhiễu đo)
Câu trả lời rõ ràng: không. Truy cập một trang đã resident tốn ~0,6 ns/dòng-cache dù có mlock hay không — chênh lệch 0,66 so 0,59 nằm trong nhiễu đo. mlock không làm RAM nhanh hơn, không cải thiện thông lượng đọc/ghi. Một khi trang đã ở trong bộ nhớ, mlock chẳng thêm được gì cho tốc độ.
(Môi trường đo: container gcc:13 trên host ARM AArch64, vùng mmap 16 MB, mỗi số lấy sau warmup. Lưu ý: container này không cấu hình swap, nên tôi không đo trực tiếp được cảnh mlock cứu một trang khỏi bị swap — phần prefault và steady-state là đo thật, phần "đảm bảo không swap" suy từ ngữ nghĩa.)
Hai niềm tin đối nghịch, cùng sai
Tôi vào đo với đúng niềm tin trực giác: "khóa trong RAM nghĩa là truy cập nhanh hơn". Đo phá tan: steady-state y hệt. mlock không phải một nút "tăng tốc bộ nhớ". Cái nó thật sự cho là hai thứ khác: prefault (bỏ page fault lần đầu — nhưng chỉ bằng cách dời phí sang lúc gọi, không xóa) và đảm bảo không swap. Vế thứ hai mới là lý do tồn tại thật của mlock, và nó là một ngữ nghĩa chứ không phải một con số tốc độ: trang bị ghim không bao giờ bị đẩy ra đĩa, nên không bao giờ có major fault (fault phải đọc lại từ swap — đắt hàng nghìn lần một minor fault). Điều đó cho độ trễ ổn định (một vòng real-time không bị một cú swap-in bất chợt làm khựng cả mili-giây) và bảo mật (một khóa mã hoá trong vùng mlock không bao giờ bị ghi ra swap — nơi nó có thể bị đọc trộm sau này).
Nhưng có một niềm tin ngược cũng sai, và tai hại theo kiểu khác: "mlock chẳng tăng tốc gì thì vô dụng, quên nó đi". Không — với hai ca trên (độ trễ tất định, bảo mật khóa), mlock là công cụ đúng và cần, chẳng có gì thay được cái đảm bảo đó. Sai lầm không phải "dùng mlock" hay "không dùng", mà là nhầm nó là công cụ hiệu năng: rải nó khắp nơi để "cho nhanh" thì ghim vô ích hàng loạt trang (ăn RAM, đụng RLIMIT_MEMLOCK, hại cả hệ) mà không được một nano-giây; còn né nó khi thật sự cần đảm bảo thì rước lấy những cú khựng ngẫu nhiên hoặc rò rỉ bí mật ra đĩa. Dùng đúng chỗ là cả câu chuyện.
Về đến code hằng ngày
Điều đầu tiên: đừng dùng mlock để "tăng tốc" — dùng nó để đảm bảo. Nếu bạn có một vòng lặp nhạy độ trễ (xử lý âm thanh, giao dịch, điều khiển real-time) không được phép khựng vì một cú swap-in, mlock vùng làm việc của nó cho độ trễ tất định. Nếu bạn giữ bí mật (khóa riêng, mật khẩu) trong bộ nhớ, mlock để nó không rơi ra swap. Đó là hai ca dùng đúng — cả hai về bảo đảm, không về tốc độ.
Điều thứ hai: nếu chỉ cần bỏ page fault lần đầu, cân nhắc MAP_POPULATE thay vì mlock. Cả hai prefault; nhưng mlock còn ghim (tốn hạn mức RLIMIT_MEMLOCK, có thể cần quyền, và giữ RAM không cho hệ dùng lại). Ghim quá nhiều trang là ép hệ điều hành mất chỗ xoay xở, hại cả máy. Chỉ ghim đúng cái cần bảo đảm, và nhớ munlock khi xong.
Chỗ này lặp lại y hệt ở tầng cao mà bạn gặp hằng ngày. Các thư viện mật mã coi mlock là bắt buộc: libsodium có sodium_mlock, GnuPG và OpenSSL ghim vùng chứa khóa để nó không bao giờ chạm đĩa — đúng ca bảo mật ở trên. Database thì ghim buffer pool để nó không bị swap dưới áp lực bộ nhớ: Oracle có "lock SGA in memory", PostgreSQL/Redis khuyên dùng huge pages và hạ vm.swappiness. JVM có -XX:+AlwaysPreTouch — chính là ý prefault (chạm hết heap lúc khởi động để tránh page fault rải rác lúc chạy), và -XX:+UseLargePages cho vùng ghim. Điểm chung: không ai dùng những thứ này để "chạy nhanh hơn" ở steady-state — họ dùng để đảm bảo độ trễ không bị một cú swap phá, đúng bài học tờ-giấy-cấm-cất-sách.
Muốn tự thấy thì nghĩ về lần cuối bạn (hay ai đó) định dùng mlock "cho nhanh": mục tiêu thật là gì — tốc độ, hay sự chắc chắn? Nếu là tốc độ, phép đo trên nói thẳng: trang đã trong RAM không nhanh thêm vì bị khóa, bạn sẽ ghim RAM vô ích. Nếu là đảm bảo không bị khựng vì swap (một vòng real-time) hoặc không để bí mật rơi ra đĩa (một khóa mã), thì mlock đúng là công cụ — nhưng vì lý do ngữ nghĩa, không phải hiệu năng. Vài phút tự hỏi "mình cần bảo đảm hay cần tốc độ?" đó tránh cho bạn cả một lớp tối ưu sai: mlock là để hứa rằng trang sẽ luôn ở đó, không phải để làm nó chạy nhanh hơn khi đã ở đó.