/proc là cách tiện lợi nhất để xem thông tin về hệ thống và tiến trình: /proc/self/stat (thống kê tiến trình), /proc/meminfo (bộ nhớ), /proc/stat (CPU), /proc/self/status... Vì chúng trông như file thường — bạn cat được, open/read được — dễ nghĩ đọc chúng rẻ như đọc một file nhỏ, hay thậm chí như đọc một biến. Nhiều công cụ giám sát poll các file /proc này liên tục. Câu hỏi: đọc một file /proc tốn bao nhiêu, và có phải rẻ như tưởng? Tôi đo trong container gcc:13 (ARM), và con số cảnh báo về việc poll /proc quá thường.

Đọc /proc: chi phí

/proc là filesystem ảo, sinh nội dung mỗi lần đọc

Điểm mấu chốt: các "file" trong /proc không nằm trên đĩa. /proc là một filesystem ảo — nội dung của mỗi file được nhân sinh ra ngay lúc bạn đọc. Khi bạn mở và đọc /proc/self/stat, nhân lấy các con số nội bộ về tiến trình (bộ đếm CPU, số trang, trạng thái...) và định dạng chúng thành một chuỗi text cho bạn. Không có gì được lưu sẵn.

Nên một lần "đọc /proc" thực ra gồm nhiều việc: open() (một syscall, tra đường dẫn trong fs ảo), read() (một syscall, và nhân tạo nội dung text lúc này), close() (một syscall) — ba syscall, cộng công định dạng số thành text. Rồi sau đó bạn còn phải parse cái text đó ngược lại thành số để dùng. So với một syscall trực tiếp như getpid hay getrusage — trả về số sẵn, không định dạng text — /proc làm nhiều hơn hẳn.

Và vì nội dung sinh động, /proc không được page cache: đọc cùng một file /proc 1000 lần thì nhân tạo lại nội dung 1000 lần, không có chuyện "lần hai lấy từ cache" như file thật.

Đo: 1342ns một lần đọc, 13 lần một syscall

Tôi đo thời gian một chu kỳ open+read+close cho vài file /proc, so với các cách lấy thông tin trực tiếp:

ĐỌC /proc (open+read+close mỗi lần):
  /proc/self/stat   : 1.342 ns
  /proc/self/status : 1.904 ns
  /proc/meminfo     : 1.550 ns

TRUY CẬP TRỰC TIẾP:
  syscall getpid    :   100 ns
  getrusage syscall :   147 ns
  đọc biến bộ nhớ   :    ~0 ns

Đọc /proc/self/stat tốn 1.342 ns13 lần một syscall getpid (100 ns), và hàng nghìn lần một lần đọc biến trong bộ nhớ (~0 ns). /proc/self/status còn đắt hơn (1.904 ns) vì nó dài hơn (nhiều dòng text để sinh). Cái giá này là tổng của ba syscall cộng công nhân định dạng text — và chưa tính thời gian bạn parse chuỗi đó sau khi đọc.

Một lần 1,3 µs nghe nhỏ, nhưng nhân lên: một vòng giám sát đọc /proc/stat 1000 lần mỗi giây tốn ~1,3 ms CPU/giây chỉ cho việc đọc, chưa tính parse — và nếu bạn đọc nhiều file /proc cho mỗi tiến trình trong một hệ thống nhiều tiến trình (như top hay ps làm), con số cộng dồn nhanh.

Một lần tôi đo hớ: /proc không rẻ như đọc biến

Tôi vào đo với niềm tin ngầm: "/proc là cách nhẹ nhàng để lấy thông tin — đọc thoải mái, gần như miễn phí". Phép đo sửa lại: mỗi lần đọc /proc/self/stat đắt gấp 13 lần một syscall và hàng nghìn lần một đọc biến. /proc tiện, nhưng không rẻ.

Bài học đo lường: một giao diện "trông như file" có thể giấu một chi phí lớn — sự tiện lợi của API không nói gì về chi phí thực thi. /proc cho bạn một cách thống nhất, dễ đọc để xem trạng thái nhân (chỉ cần cat), nhưng cái tiện đó đổi bằng: mỗi đọc là ba syscall, nhân sinh text, không cache, rồi bạn parse. Nếu tôi tin "/proc rẻ" và viết một vòng lặp poll /proc/self/stat mỗi mili-giây để theo dõi CPU, tôi đã đốt CPU đáng kể cho chính việc đo — và tệ hơn, nếu parse cẩu thả, phần parse còn đắt hơn phần đọc. Đây cùng tinh thần đo hớ về signal: một cái tên hay giao diện quen thuộc (một "file", một kill) không đảm bảo chi phí thấp — phải đo. Với thông tin cần thường xuyên, ưu tiên syscall trực tiếp (getrusage, clock_gettime, sysinfo) trả số sẵn; để dành /proc cho việc thỉnh thoảng xem, nơi sự tiện đáng giá hơn tốc độ. Nói cách khác: /proc được thiết kế cho con người đọc (dễ, thống nhất, cat được), không cho một vòng lặp nóng đọc — dùng nó đúng vai trò đó thì tuyệt, ép nó làm kênh đo tần suất cao thì phí.

Vì sao điều này quan trọng khi lập trình

Hệ quả đầu tiên: đừng poll /proc ở tần suất cao — dùng syscall trực tiếp cho giá trị cần thường xuyên. Nếu bạn cần theo dõi CPU/bộ nhớ của chính mình liên tục, getrusage (cho tài nguyên của mình) hay clock_gettime rẻ hơn nhiều một lần đọc /proc. Chỉ đọc /proc khi bạn cần thông tin không có syscall trực tiếp (thông tin về tiến trình khác, hay chi tiết chỉ /proc phơi bày) và không đọc quá thường.

Hệ quả thứ hai: nhớ rằng đọc /proc gồm cả phần parse, và cả hai đều tốn. Chi phí thật của "lấy một số từ /proc" là 1,3 µs đọc cộng thời gian bạn tách chuỗi text để lấy đúng trường. Với các công cụ đọc /proc cho nhiều tiến trình (giám sát toàn hệ thống), cân nhắc đọc gộp (một lần mở, đọc các trường cần) và parse tối giản. Đây là lý do các thư viện giám sát hiệu năng cao cache và parse cẩn thận thay vì đọc lại /proc mỗi lần cần một trường.

Hệ quả thứ ba là tinh thần đo lường: "trông như file" không nghĩa "rẻ như file" — đo trước khi poll. Con số mang theo: /proc là filesystem ảo — mỗi lần đọc một file /proc là open+read+close (3 syscall) cộng nhân SINH nội dung text on-the-fly (không được page cache vì sinh động), nên đọc /proc/self/stat tốn ~1.342 ns, gấp 13 lần một syscall getpid (100 ns) và hàng nghìn lần một đọc biến bộ nhớ; và bạn còn phải parse text sau đó. Poll /proc tần suất cao đốt CPU thật — cho giá trị cần thường xuyên hãy dùng syscall trực tiếp (getrusage, clock_gettime). Tiện không bằng rẻ.

Thử ba mươi giây

Nếu bạn có code (hay một script) đọc /proc trong một vòng lặp — theo dõi bộ nhớ, CPU, số kết nối — hỏi: nó đọc bao nhiêu lần mỗi giây, và mỗi lần đọc file /proc nào? Chạy strace -c chương trình đó và nhìn số lần openat/read/close: nếu chúng cao bất thường, có thể bạn đang poll /proc quá thường. Mỗi chu kỳ là ~1,3 µs cộng parse — nhân với tần suất để ước lượng CPU đốt cho việc đo. Rồi hỏi: giá trị đó có syscall trực tiếp không (getrusage cho tài nguyên mình, sysinfo cho bộ nhớ hệ thống, clock_gettime cho giờ)? Ba mươi giây kiểm "poll /proc hay syscall trực tiếp" đó thường tìm ra một khoản CPU phí phạm âm thầm — và nhắc rằng một giao diện quen thuộc như "đọc một file" vẫn có thể đắt hơn bạn tưởng nhiều lần.