Hình dung một tủ hồ sơ. Bạn tưởng mỗi ngăn kéo chứa sẵn một tờ giấy — kéo ra, đọc, xong. Nhưng thực tế mỗi ngăn kéo có một anh thư ký ngồi bên trong: mỗi lần bạn mở, anh ta lấy các con số trong sổ nội bộ, viết tay thành một bản báo cáo bằng chữ, đưa cho bạn — rồi bạn phải đọc bản chữ đó và dịch ngược lại thành số để dùng. Không có tờ giấy nào chờ sẵn, không có bản sao lưu; lần sau mở lại, anh ta viết lại từ đầu. /proc của Linux chính là cái tủ đó, và vì nó trông như file thường nên rất nhiều người tưởng đọc nó rẻ như đọc một biến.

/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.

/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 — đúng anh thư ký viết tay bản báo cáo mỗi lần bạn hỏi.

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 ns — 13 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ôi trường đo: container gcc:13 trên host ARM AArch64, chu kỳ open+read+close mỗi lần, mỗi số lấy sau warmup. Con số tuyệt đối đổi theo máy; cái bền vững là thứ bậc: đọc /proc >> một syscall trực tiếp >> đọc biến.)

Hai niềm tin đối nghịch, cùng sai

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. Một giao diện "trông như file" 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. Nếu 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, bạn đốt CPU đáng kể cho chính việc đo — và nếu parse cẩu thả, phần parse còn đắt hơn phần đọc.

Nhưng có một niềm tin ngược cũng sai theo hướng khác: "/proc đắt thế thì né đi, đọc thẳng bộ nhớ nhân hay bám vào cấu trúc nội bộ cho nhanh". Không — /proc tồn tại chính vì nó ổn định và an toàn: nó là giao diện công khai, thống nhất, cat được, không phụ thuộc phiên bản nhân, ai cũng đọc được không cần quyền đặc biệt. Cái giá 1,3 µs là để mua lấy sự tiện và bền đó. Vấn đề không phải "/proc tốt hay xấu" mà là dùng đúng vai: /proc được thiết kế cho con người và cho việc xem thỉnh thoảng (một lần, một cú cat, một lần khởi động), nơi tiện > nhanh; ép nó làm kênh đo tần suất cao trong vòng nóng mới là sai. Với giá trị cần liên tục, dùng syscall trực tiếp trả số sẵn (getrusage, clock_gettime, sysinfo); để dành /proc cho thông tin không có syscall tương đương hoặc chỉ xem lâu lâu. (Đây cùng tinh thần đo hớ về signal: một cái tên hay giao diện quen thuộc không đảm bảo chi phí thấp — phải đo.)

Về đến code hằng ngày

Điều đầ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.

Điều 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.

Chỗ này là nỗi đau âm thầm của cả một ngành giám sát. Trên container, chính /proc (và /sys/fs/cgroup) là nguồn mà cAdvisor, node_exporter, Prometheus scrape để lấy metric — và ở quy mô hàng nghìn container, chi phí parse /proc cộng dồn thành phần trăm CPU thật, đủ để người ta viết lại bộ parser cho nhanh. Trong Java, đọc CPU/bộ nhớ qua ManagementFactory/JMX cuối cùng cũng chạm /proc dưới nền; Go có runtime.ReadMemStats (số nội bộ, rẻ) nhưng thư viện như gopsutil thì parse /proc, và gọi nó trong vòng nóng là một bug hiệu năng kinh điển. Ngay cả top/htop giật khi có mười nghìn tiến trình cũng chính vì phải đọc + parse /proc/<pid>/stat cho từng cái. Cùng một bài học anh-thư-ký-viết-tay: giao diện dễ đọc không có nghĩa là rẻ để đọc liên tục.

Muốn tự thấy thì 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ờ)? Vài phút 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.