Chương trình của bạn tưởng như sở hữu cả một dải bộ nhớ liền mạch khổng lồ, nhưng đó là bộ nhớ ảo — một ảo giác do kernel dựng nên. Mỗi tiến trình có một không gian địa chỉ ảo riêng, và kernel ánh xạ (lười biếng) các phần được dùng của nó sang RAM vật lý thật. mmap là cách trực tiếp nhất để yêu cầu một vùng không gian ảo đó. Bài này đo mmap trong container để tách bạch hai thứ mà người ta hay nhầm là một: bộ nhớ ảo đã đặt chỗ, và RAM thật đang dùng — và tôi suýt báo cáo một chương trình 1 MB là con quái vật 4 GB.
mmap đặt chỗ ảo, RAM trả sau
mmap không cấp RAM. Khi bạn gọi mmap một vùng, kernel chỉ đặt chỗ một khoảng địa chỉ ảo và ghi vào bảng ánh xạ của tiến trình rằng "khoảng này hợp lệ" — nhưng chưa có một byte RAM vật lý nào được gắn vào. Chỉ khi chương trình chạm vào một trang trong vùng đó, phần cứng gây một lỗi trang, và lúc ấy kernel mới cấp một trang RAM thật (như bài lỗi trang đã đo). Đây chính là "cấp phát lười" đứng sau malloc cho vùng lớn.
Vì cấp lười như vậy, hệ điều hành theo dõi hai con số bộ nhớ hoàn toàn khác nhau cho mỗi tiến trình, đọc được trong /proc/self/status:
- VmSize (VSZ) — tổng địa chỉ ảo đã đặt chỗ, gồm cả phần chưa hề chạm tới.
- VmRSS (RSS) — lượng RAM vật lý tiến trình đang thật sự dùng (resident set size).
Đo: mmap 4 GB, RAM không nhúc nhích
Máy trong container có 7934 MB RAM. Tôi đọc VSZ/RSS ở ba thời điểm quanh một lần mmap 4 GB, ghim lõi 0:
| Bước | VSZ (ảo) | RSS (thật) |
|---|---|---|
| Khởi đầu | 2 MB | 1 MB |
Sau mmap 4 GB (chưa chạm) |
4098 MB | 1 MB |
| Sau khi chạm 512 MB | 4098 MB | 513 MB |
Nhìn dòng giữa: mmap 4 GB làm VSZ nhảy vọt thêm 4096 MB, nhưng RSS không đổi — vẫn 1 MB. Bốn gigabyte không gian ảo được đặt chỗ mà không tốn một byte RAM nào, vì tôi chưa chạm tới. Khi tôi chạm 512 MB đầu, RSS tăng đúng 512 MB — chỉ bằng phần đã chạm — còn VSZ giữ nguyên. RSS lớn dần theo cái bạn dùng; VSZ đứng yên vì nó chỉ là ranh giới địa chỉ.
Và vì mmap chỉ đặt chỗ ảo, nó có thể đặt chỗ vùng lớn hơn cả RAM thật: mmap 64 GB trên máy 8 GB thành công — kernel vui vẻ cấp 64 GB địa chỉ ảo, biết rằng phần lớn sẽ không bao giờ được chạm (cơ chế overcommit, sẽ đo kỹ ở bài riêng). Điều tương tự xảy ra khi một chương trình nạp nhiều thư viện dùng chung: mỗi thư viện được ánh xạ vào không gian ảo, cộng dồn vào VSZ, nhưng các trang mã của chúng thường được chia sẻ giữa mọi tiến trình và chỉ nạp phần thật sự chạy — nên VSZ phồng lên trong khi RAM riêng của tiến trình vẫn khiêm tốn.
Một lần tôi đo hớ: nhìn nhầm cột
Câu hỏi tôi đặt ra rất đời thường: "chương trình này dùng bao nhiêu bộ nhớ sau khi mmap 4 GB?". Để trả lời, tôi nhìn con số bộ nhớ đập vào mắt trước nhất — và đó là VSZ, cột mà lệnh ps thường khoe to nhất. VSZ ghi 4098 MB. Tôi suýt kết luận ngay: "chương trình này ngốn 4 GB RAM, một con quái vật cần phải lo".
May là tôi đọc luôn dòng bên cạnh, VmRSS: chỉ 1 MB. Chương trình "4 GB" của tôi thật ra đang dùng vỏn vẹn một megabyte RAM. VSZ không phải bộ nhớ đang dùng — nó là địa chỉ ảo đã đặt chỗ, mà phần lớn chưa hề chạm và chẳng có RAM nào chống lưng. Nếu tôi tin VSZ, tôi đã phóng đại mức dùng RAM lên bốn nghìn lần.
Bài học đo lường: "bộ nhớ" không phải một con số — VSZ và RSS là hai đại lượng khác hẳn cùng đội lốt chữ "memory", và đo nhầm cột phóng đại tới hàng nghìn lần. Khi hỏi "tiến trình này dùng bao nhiêu RAM", câu trả lời là RSS (cột RES trong top, VmRSS trong /proc), không phải VSZ. Đây là một lỗi vận hành kinh điển: người ta hoảng vì một tiến trình có VSZ khổng lồ (thường do mmap vùng lớn, hoặc ánh xạ nhiều thư viện) trong khi RAM thật nó chiếm rất nhỏ. Phải hỏi rõ "cột nào" trước khi tin một con số bộ nhớ — đúng tinh thần "đo nhầm đại lượng" mà cả sê-ri lặp lại.
Vì sao điều này quan trọng khi lập trình
Hệ quả đầu tiên: theo dõi RSS, không phải VSZ, khi lo về mức dùng RAM. Một tiến trình mmap một file lớn, hay đặt chỗ một arena lớn để cấp phát dần, sẽ có VSZ to mà RSS nhỏ — hoàn toàn khỏe mạnh. Cảnh báo giám sát nên đặt ngưỡng trên RSS (hoặc bộ nhớ của cgroup, sẽ đo sau), vì đó mới là RAM thật đang bị chiếm. Đặt cảnh báo trên VSZ sẽ báo động giả liên tục với bất kỳ chương trình nào dùng mmap hào phóng.
Hệ quả thứ hai: mmap là công cụ mạnh nhờ đúng tính chất lười này. Bạn có thể mmap một file cực lớn và chỉ đọc phần cần (RSS chỉ tăng theo phần chạm), đặt chỗ một vùng địa chỉ lớn rồi lấp dần (cấp phát arena, sparse array), hay dành sẵn không gian mà không trả tiền RAM cho tới khi dùng. Nhiều cấu trúc dữ liệu và cơ sở dữ liệu dựa vào chính điều này: đặt chỗ ảo hào phóng, để kernel biến nó thành RAM thật đúng theo nhu cầu. Hiểu VSZ ≠ RSS giúp bạn khai thác mmap mà không sợ "tốn 4 GB".
Hệ quả thứ ba là bài học đo lường mang theo: luôn hỏi một con số đang đo đại lượng nào trước khi tin nó. Con số mang theo: mmap 4 GB làm VSZ (bộ nhớ ảo đã đặt chỗ) nhảy lên 4098 MB nhưng RSS (RAM vật lý thật) giữ ở 1 MB; RSS chỉ tăng theo phần được chạm (chạm 512 MB → RSS 513 MB); và mmap 64 GB trên máy 8 GB vẫn thành công vì chỉ là đặt chỗ ảo — nên khi hỏi "dùng bao nhiêu RAM", nhìn RSS chứ đừng nhìn VSZ. Hai cột cùng nằm cạnh nhau, cùng đơn vị megabyte, mà một cột có thể lớn hơn cột kia hàng nghìn lần — và tin nhầm cột là biến "bộ nhớ" thành một con số vô nghĩa.
Thử ba mươi giây
Chạy ps aux hoặc top và nhìn hai cột VSZ (hay VIRT) và RSS (hay RES) của vài tiến trình — bạn sẽ thấy nhiều tiến trình có VSZ lớn hơn RSS rất nhiều, đặc biệt là các chương trình lớn dùng nhiều thư viện dùng chung hoặc mmap nhiều. Rồi đọc /proc/<pid>/status và tìm VmSize với VmRSS cho con số chính xác tới kilobyte. Muốn thấy tận mắt, viết một chương trình mmap vài GB rồi in /proc/self/status: VSZ nhảy lên, RSS đứng yên. Chạm một phần vùng đó, in lại: RSS tăng đúng phần bạn chạm, VSZ không đổi. Đó là toàn bộ khác biệt giữa "đặt chỗ" và "dùng thật", hiện ra trong hai con số.