Bạn mở top hay ps, thấy một tiến trình có VSZ 3 GB và RSS 50 MB, và tự hỏi: nó dùng bao nhiêu bộ nhớ? Hai con số này thường bị lẫn lộn, và chọn nhầm con số dẫn tới hoặc hoảng loạn vô cớ (tưởng dùng 3 GB) hoặc đếm sai tổng RAM của cả hệ. Chúng đo hai thứ khác nhau: VSZ là bộ nhớ đã đặt chỗ (ảo), RSS là bộ nhớ thật đang chiếm (vật lý). Và còn một con số thứ ba ít người biết — PSS — mới là thứ đúng khi cộng nhiều tiến trình. Tôi đo cả ba từ /proc trong container gcc:13.
Ba con số, ba nghĩa khác nhau
VSZ (VmSize) — tổng không gian địa chỉ ảo mà tiến trình đã đặt chỗ (mapped): mọi vùng mmap, mọi malloc, thư viện chia sẻ, stack, guard page. Quan trọng: nó gồm cả những vùng bạn chưa hề chạm tới — malloc một vùng lớn làm VSZ tăng ngay dù chưa một byte nào được cấp RAM thật.
RSS (VmRSS) — bộ nhớ vật lý thật tiến trình đang chiếm: chỉ những trang đã thực sự được cấp RAM (đã "backing"). RSS chia nhỏ: RssAnon (bộ nhớ ẩn danh riêng — heap, stack), RssFile (trang từ file/thư viện được map — thường chia sẻ với tiến trình khác), RssShmem (shared memory phần 26).
PSS (Pss) — như RSS, nhưng các trang chia sẻ được chia theo tỉ lệ: nếu một trang thư viện dùng chung bởi 5 tiến trình, mỗi tiến trình chỉ tính 1/5 trang đó. Đây là con số đúng khi bạn muốn cộng bộ nhớ của nhiều tiến trình mà không đếm trùng.
Đo: VSZ nhảy 3GB, RSS đứng yên
Tôi đọc /proc/self/status và smaps_rollup qua bốn mốc:
VSZ RSS (Anon / File-lib) PSS
1) lúc khởi động : 2.204 KB 1.016 KB (84 / 932) 453 KB
2) sau malloc 3GB (chưa chạm): 3.147.936 KB 1.216 KB (92 / 1124) 461 KB
3) sau chạm 200 MB : 3.147.936 KB 206.208 KB (205.084 / 1124) 205.453 KB
4) sau chạm 500 MB : 3.147.936 KB 513.408 KB (512.284 / 1124) 512.653 KB
Nhìn dòng 2: sau malloc(3 GB) mà chưa chạm, VSZ nhảy vọt lên ~3,15 GB — nhưng RSS gần như không đổi (1,0 → 1,2 MB). Vùng 3 GB kia hoàn toàn trên giấy: không gian địa chỉ đã đặt chỗ, nhưng chưa một trang vật lý nào được cấp. Nếu bạn theo dõi VSZ để đo RAM, bạn sẽ báo động "tiến trình dùng 3 GB!" trong khi nó thực sự dùng ~1 MB. RSS mới là con số thật, và nó chỉ tăng khi bạn chạm: dòng 3 (chạm 200 MB) RSS lên ~201 MB, dòng 4 (chạm 500 MB) RSS lên ~501 MB — đúng bằng lượng đã chạm, trong khi VSZ giữ nguyên 3 GB suốt.
Giờ nhìn cột PSS ở dòng 1: RSS là 1.016 KB nhưng PSS chỉ 453 KB — thấp hơn hẳn. Vì sao? Vì RSS gồm RssFile = 932 KB, là các trang thư viện chia sẻ (libc, loader…) được map vào tiến trình. Những trang đó dùng chung với các tiến trình khác trong hệ, nên RSS đếm chúng đầy đủ trong mỗi tiến trình, còn PSS chỉ tính phần chia của tiến trình này. Ở dòng 3–4, sau khi chạm bộ nhớ ẩn danh riêng (RssAnon), PSS ≈ RSS — vì bộ nhớ riêng thì không chia sẻ, đếm đủ ở cả hai. Khác biệt PSS < RSS chính là phần trang chia sẻ bị RSS đếm trùng.
Một lần tôi đo hớ: "VSZ là bộ nhớ dùng", và "cộng RSS ra tổng"
Tôi vào đo với hai niềm tin. Thứ nhất: "VSZ là bộ nhớ tiến trình dùng; theo dõi VSZ để biết RAM". Sai — malloc 3 GB chưa chạm cho VSZ ~3 GB mà RSS chỉ ~1 MB. VSZ là không gian ảo đặt chỗ, phần lớn chưa được cấp RAM (đúng cái overcommit của phần 29); RSS mới là RAM thật. Thứ hai: "muốn biết tổng RAM nhiều tiến trình dùng, cộng RSS của chúng lại". Sai — RSS đếm trang chia sẻ (thư viện, shm) đầy đủ trong mỗi tiến trình; cộng RSS của 10 tiến trình cùng dùng libc sẽ đếm libc 10 lần. Đo cho thấy ngay trong một tiến trình: RSS 1016 KB nhưng PSS chỉ 453 KB — chênh lệch chính là trang thư viện bị đếm trùng.
Bài học đo lường: VSZ đo đặt chỗ (ảo), RSS đo dùng thật (vật lý), và VSZ thường lớn hơn RSS rất nhiều; còn khi cộng nhiều tiến trình, RSS đếm trùng trang chia sẻ nên PSS mới cho tổng đúng. Nếu tôi tin "VSZ = bộ nhớ dùng", tôi đã báo động sai và tối ưu nhầm; nếu tôi cộng RSS để tính tổng RAM của một nhóm tiến trình, tôi đã cộng vượt (đôi khi vượt cả RAM vật lý của máy — dấu hiệu rõ ràng rằng con số sai).
Vì sao điều này quan trọng khi lập trình
Hệ quả đầu tiên: theo dõi RAM bằng RSS (một tiến trình), không bằng VSZ. Khi giám sát bộ nhớ của một dịch vụ, RSS là con số phản ánh áp lực RAM thật; VSZ cao là bình thường và không có nghĩa gì (nhất là với chương trình mmap nhiều vùng lớn, JVM, Go runtime đặt chỗ heap ảo lớn). Đừng đặt cảnh báo trên VSZ.
Hệ quả thứ hai: khi cộng bộ nhớ nhiều tiến trình (một nhóm, một container), dùng PSS. Cộng RSS đếm trùng thư viện chia sẻ và shared memory; smaps_rollup cho Pss là cách đúng để chia phần chia sẻ. Đây là lý do các công cụ như smem báo PSS, và vì sao "tổng RSS" của nhiều worker chia sẻ thư viện luôn phóng đại mức dùng thật. (Container/cgroup phần 35 đo bộ nhớ theo cách kế toán riêng, gần với thực tế hơn cộng RSS.)
Hệ quả thứ ba là tinh thần đo lường: biết một con số đo cái gì trước khi tin nó. Con số mang theo: VSZ (VmSize) = tổng không gian địa chỉ ẢO đã đặt chỗ (gồm vùng chưa chạm, thư viện) — malloc 3GB chưa chạm cho VSZ ~3GB nhưng RSS chỉ ~1MB; RSS (VmRSS) = RAM VẬT LÝ thật đang chiếm, chỉ tăng khi CHẠM (chạm 200MB -> RSS +200MB); PSS (Pss) = như RSS nhưng trang CHIA SẺ chia tỉ lệ, nên cộng nhiều tiến trình phải dùng PSS chứ không phải RSS (đo: RSS 1016KB nhưng Pss 453KB vì trang thư viện chia sẻ đếm đủ trong RSS). Giám sát RAM: nhìn RSS/PSS, đừng nhìn VSZ. Cùng "bộ nhớ" nhưng ba con số ba nghĩa — chọn đúng con số cho câu hỏi bạn đang hỏi.
Thử ba mươi giây
Viết một chương trình malloc một vùng lớn (vài GB) không chạm, in /proc/self/status | grep -E "VmSize|VmRSS", rồi chạm dần và in lại. Bạn sẽ thấy VmSize (VSZ) nhảy ngay khi malloc và giữ nguyên, còn VmRSS chỉ bò lên khi bạn chạm. Rồi so VmRSS với dòng Pss trong /proc/self/smaps_rollup: với một chương trình dùng thư viện chia sẻ, PSS sẽ thấp hơn RSS — phần chênh là trang thư viện dùng chung. Ba mươi giây đó cho bạn thấy điều mà top không nói rõ: VSZ là lời hứa đặt chỗ, RSS là RAM thật đang giữ, và khi bạn muốn cộng nhiều tiến trình, chỉ PSS mới không đếm trùng — chọn nhầm con số là hiểu sai hoàn toàn về bộ nhớ của chương trình.