Suốt sê-ri này tôi liên tục đọc /proc/self/status, /proc/<pid>/stat, /proc/meminfo để lấy số đo. Đó cũng là cách ps, top, và mọi agent giám sát biết chuyện gì đang xảy ra trong hệ thống. Những file này trông như file thường, nhưng chúng không nằm trên đĩa — nhân sinh nội dung ngay khi bạn đọc. Vậy đọc chúng rẻ tới đâu? Bài này đo trực tiếp — và phát hiện câu trả lời không phải một con số, mà tùy vào bạn đọc file nào.
/proc là hệ thống file ảo
/proc là một hệ thống file ảo (virtual filesystem): các "file" trong đó không tồn tại trên đĩa. Khi bạn open rồi read một file như /proc/self/status, nhân tạo ra nội dung tại chỗ — nó thu thập trạng thái tiến trình từ các cấu trúc dữ liệu bên trong, định dạng thành text, và trả về. Nghĩa là đọc /proc không tốn I/O đĩa (nhanh hơn đọc file thật cần chạm đĩa), nhưng cũng không miễn phí: mỗi lần đọc là một cặp lời gọi hệ thống (open+read+close), và nhân phải định dạng lại text từ đầu mỗi lần — không có page cache để phục vụ lần đọc thứ hai (đọc lại là sinh lại).
Điều này quan trọng vì các công cụ giám sát đọc /proc liên tục: một agent có thể đọc /proc/<pid>/stat cho hàng nghìn tiến trình, mỗi giây. Cái giá tưởng nhỏ ấy cộng dồn lại — mười nghìn lần đọc mỗi giây, mỗi lần một syscall và một lần nhân dựng text, không phải chuyện nhỏ. Ngoài ra, vì nội dung sinh tại chỗ, mỗi lần đọc /proc cho một ảnh chụp nhất quán tại thời điểm đó: hai trường trong cùng một lần đọc là đồng thời, nhưng hai lần đọc khác nhau có thể thấy trạng thái đã đổi. Tôi muốn đo chi phí ấy cụ thể là bao nhiêu, và liệu mọi file /proc có rẻ như nhau không.
Đo: từ 1,5 us tới 12 us, và còn phình
Tôi đo chi phí một chu trình open+read+close cho vài file /proc, lặp nhiều lần:
| File | Chi phí đọc |
|---|---|
| /proc/self/stat | 1.480 ns |
| /proc/self/status | 2.066 ns |
| /proc/meminfo | 1.724 ns |
| /proc/self/smaps_rollup | 4.087 ns |
| /proc/self/smaps | 11.718 ns |
| /etc/hostname (file thường, để so) | 689 ns |
Đọc /proc/self/stat tốn ~1,5 micro giây — gấp đôi một file thường nhỏ (689 ns), vì nhân phải sinh nội dung thay vì đọc từ page cache. status và meminfo cũng cỡ 2 micro giây. Nghe đều đều "rẻ". Nhưng smaps nhảy vọt lên 11.718 ns — gấp gần 8 lần stat. Vì sao?
Vì smaps (và smaps_rollup) không chỉ đọc vài con số — chúng bắt nhân đi bộ qua toàn bộ bảng trang của tiến trình để tổng hợp thông tin từng vùng nhớ. Nên chi phí của chúng tỉ lệ với bộ nhớ tiến trình. Tôi kiểm bằng cách cấp và chạm 1 GB rồi đo lại:
Tiến trình nhỏ: smaps_rollup 4.165 ns | smaps 11.283 ns
Sau khi chạm 1 GB: smaps_rollup 11.614 ns | smaps 22.002 ns
Cùng lúc đó: stat vẫn 1.407 ns (KHÔNG đổi)
Chi phí smaps_rollup gần gấp ba sau khi tiến trình lớn thêm 1 GB, còn smaps gấp đôi — trong khi stat phẳng lì, không mảy may đổi. Lý do sâu xa: stat chỉ đọc vài trường có sẵn trong cấu trúc tiến trình, còn smaps phải duyệt từng vùng nhớ (VMA) và với mỗi vùng còn đếm số trang thường trú — công việc tỉ lệ thẳng với quy mô bộ nhớ. Với một tiến trình cơ sở dữ liệu chiếm hàng chục GB và hàng nghìn vùng nhớ, đọc smaps của nó có thể tốn hàng mili giây, gấp cả nghìn lần đọc stat.
Một lần tôi đo hớ: "đọc /proc rẻ" không có một đáp án
Sai lầm của tôi rất tự nhiên: tôi đo /proc/self/stat (1.480 ns) và định chốt gọn "đọc thông tin tiến trình từ /proc rẻ, cỡ 1-2 micro giây, dùng thoải mái". Kết luận đó đúng với stat, status, meminfo — nhưng sai với smaps, và tôi đã suýt khái quát từ một mẫu.
Bài học đo lường: "đọc /proc rẻ tới đâu" không có một con số — nó tùy file nào, và với smaps thì tùy cỡ tiến trình. Tôi đo đúng một file (cái rẻ và phẳng, không đổi theo trạng thái) rồi khái quát cho cả họ /proc, giấu mất những file mà chi phí phình theo trạng thái của tiến trình. Đây là kiểu đo hớ khi ta chọn một đại diện "dễ" rồi tưởng nó nói thay cho cả nhóm — trong khi nhóm đó không đồng nhất. Muốn biết một họ thao tác đắt hay rẻ, phải đo cái đắt nhất và cái thay đổi theo quy mô, không chỉ cái tiện tay nhất. Và với smaps, phải đo ở nhiều cỡ tiến trình mới thấy nó scale.
Vì sao điều này quan trọng khi lập trình
Hệ quả đầu tiên là viết agent giám sát thì chọn đúng file /proc. Nếu bạn cần thăm dò trạng thái nhiều tiến trình thường xuyên (mỗi giây, cho hàng nghìn PID), hãy dùng các file phẳng và rẻ: /proc/<pid>/stat cho CPU/trạng thái, /proc/<pid>/statm cho bộ nhớ tổng quát. Tránh đọc smaps/smaps_rollup trong vòng lặp dày, vì chi phí của chúng tăng theo bộ nhớ tiến trình và có thể ngốn CPU đáng kể trên máy nhiều RAM — chính agent giám sát của bạn trở thành kẻ ngốn CPU. Đọc smaps chỉ khi thật sự cần chi tiết từng vùng nhớ, và thưa thôi.
Hệ quả thứ hai là nhớ rằng /proc không được cache — đọc dày là trả syscall dày. Vì mỗi lần đọc sinh nội dung mới, không có chuyện "lần đọc thứ hai rẻ hơn". Một vòng lặp đọc /proc/meminfo mười nghìn lần mỗi giây là mười nghìn syscall mỗi giây, mỗi cái nhân phải dựng lại text. Nếu chỉ cần một phần thông tin, đọc file có ít nội dung hơn (stat thay vì status), hoặc giảm tần suất thăm dò. Đừng coi /proc như biến trong bộ nhớ đọc bao nhiêu cũng được — nó là một giao diện syscall khoác áo file, và mỗi lần chạm vào là một lần phiền tới nhân.
Hệ quả thứ ba, về đo lường: đừng khái quát chi phí của cả một họ thao tác từ một mẫu dễ nhất. Con số mang theo: đọc /proc là đọc file ảo do nhân sinh tại chỗ — stat/status/meminfo rẻ (~1,5-2 us, gấp ~2 lần file thường, và không cache), nhưng smaps/smaps_rollup đắt hơn nhiều (4-12 us) và còn PHÌNH theo bộ nhớ tiến trình vì nhân đi bộ bảng trang (12 us sau khi chạm 1 GB); nên agent giám sát dày nên dùng stat, tránh smaps. Cái rẻ và cái đắt nằm chung một thư mục trông giống nhau; chỉ đo mới phân biệt.
Thử ba mươi giây
Tự đo trên máy bạn: for f in /proc/self/stat /proc/self/status /proc/self/smaps; do echo -n "$f: "; ( time (for i in $(seq 1000); do cat $f >/dev/null; done) ) 2>&1 | grep real; done — bạn sẽ thấy smaps chậm hơn hẳn. Xem một công cụ giám sát đọc gì: strace -c -e trace=openat,read top -bn1 >/dev/null in ra số lần open/read nó thực hiện chỉ để vẽ một khung hình. Và để thấy smaps scale theo bộ nhớ, time cat /proc/<pid>/smaps >/dev/null trên một tiến trình nhỏ so với một tiến trình ngốn RAM lớn (database, trình duyệt) — cái sau chậm hơn rõ rệt, đúng cái phình theo bộ nhớ bài này đo. Chọn file /proc nhẹ khi cần đọc dày.