Bài time cho thấy khi real cao nhưng user+sys thấp, chương trình đang chờ — và một thủ phạm phổ biến là đĩa. Máy chậm rề, CPU vẫn rảnh, vì một tiến trình nào đó đang "quật" đĩa liên tục. Nhưng tiến trình nào, và nó thực sự chạm đĩa bao nhiêu? Linux cho câu trả lời qua /proc/<pid>/io. Có điều, cách đọc nó tiềm ẩn một cái bẫy: một tiến trình có thể có vẻ đọc rất nhiều mà thực ra không chạm đĩa chút nào — nhờ page cache. Bài này (phần 9 loạt Debug) chạy thật để đọc đúng I/O của tiến trình.

/proc//io: các cột và nghĩa

File /proc/<pid>/io cho vài bộ đếm I/O của tiến trình:

  • rchar / wchar: số byte đọc/ghi qua syscall (read/write) — kể cả dữ liệu lấy từ hoặc ghi vào RAM cache, chưa chắc chạm đĩa.
  • read_bytes / write_bytes: số byte thật đọc từ / ghi xuống đĩa (tầng block) — đây là I/O đĩa thật.
  • syscr / syscw: số lần gọi read() / write().
cat /proc/<pid>/io

Ảnh chụp đoạn mã nền tối minh hoạ theo dõi I/O của tiến trình proc pid io và bẫy page cache, máy chậm mà CPU rảnh có thể một tiến trình đang quật đĩa real cao user cộng sys thấp bài time đang chờ chờ đĩa proc pid io cho biết tiến trình đọc ghi bao nhiêu byte cat proc pid io, các cột và nghĩa rchar wchar byte đọc ghi qua syscall kể cả từ RAM cache read_bytes byte thật đọc từ đĩa tầng block write_bytes byte thật ghi xuống đĩa syscr syscw số lần gọi read write, bẫy rchar cao khác đĩa bận page cache đọc lại file vừa dùng lấy từ page cache RAM không chạm đĩa rchar bằng 100MB đọc qua syscall nhưng read_bytes bằng 0 không từ đĩa muốn biết đĩa có bận không nhìn read_bytes write_bytes, đo delta đọc io hai lần đọc write_bytes trước và sau biết tiến trình ghi bao nhiêu trong khoảng đó theo dõi tăng dần bằng tiến trình đang ghi liên tục iotop hiển thị realtime I/O mỗi tiến trình nếu có quyền

Hình 1: /proc/<pid>/io cho rchar/wchar (qua syscall, gồm cả cache) và read_bytes/write_bytes (đĩa thật); bẫy là rchar cao không có nghĩa đĩa bận — có thể từ page cache; đo delta bằng cách đọc hai lần.

Đo thật: ghi 200MB, và bẫy page cache

Ảnh chụp bảng kết quả chạy thật proc pid io output thật, một tiến trình ghi 200MB đọc proc pid io wchar 209715200 200MB ghi qua syscall syscw 200 200 lần write mỗi lần 1MB write_bytes 209715200 200MB thật xuống đĩa, hai bẫy page cache đọc file đang trong cache rchar 104885123 khoảng 100MB đọc qua syscall read_bytes 0 nhưng 0 từ đĩa lấy từ page cache RAM rchar cao không có nghĩa đĩa bận read_bytes mới là đĩa thật, ba đo delta write đọc io 2 lần write_bytes ban đầu 0 write_bytes sau ghi 150MB 157286400 tăng khoảng 150MB delta cho biết tiến trình ghi bao nhiêu trong khoảng theo dõi, kết proc pid io rchar wchar qua syscall read_bytes write_bytes đĩa thật đĩa bận thật nhìn read_bytes write_bytes đừng để rchar đánh lừa

Hình 2: Chạy thật — tiến trình ghi 200MB: wchar=209715200 (200MB qua syscall), syscw=200, write_bytes=209715200 (200MB xuống đĩa); đọc file trong cache: rchar=104885123 (~100MB) nhưng read_bytes=0 (từ page cache, không chạm đĩa); delta write_bytes 0→157286400 (~150MB).

  • Ghi thật: tiến trình ghi 200MB cho wchar = write_bytes = 209715200 (200MB), và syscw=200 (200 lần write 1MB). Cả byte-qua-syscall lẫn byte-xuống-đĩa đều 200MB — đây là ghi thật ra đĩa.
  • Bẫy page cache: đọc lại một file 100MB vừa được dùng cho rchar=104885123 (~100MB đọc qua syscall) nhưng read_bytes=0 — không một byte nào từ đĩa! Dữ liệu được kernel giữ trong page cache (RAM) từ lần dùng trước, nên read() lấy từ RAM. Nếu chỉ nhìn rchar, bạn tưởng đĩa đang bận đọc 100MB — sai. read_bytes=0 mới là sự thật: đĩa không bận.
  • Đo delta: đọc write_bytes trước (0) và sau khi ghi 150MB (157286400) — chênh ~150MB. Đọc hai lần cho biết tiến trình ghi bao nhiêu trong khoảng đó; theo dõi con số tăng dần = tiến trình đang ghi liên tục (thủ phạm quật đĩa).

Vì sao phân biệt này quan trọng

Đây là mấu chốt chẩn đoán I/O: rchar/wchar cao không có nghĩa đĩa bận. Một app đọc đi đọc lại cùng dữ liệu (từ cache) có rchar khổng lồ mà read_bytes bằng 0 — hoàn toàn lành mạnh, không chạm đĩa. Ngược lại, khi hệ thống chậm vì đĩa, cái bạn cần nhìn là read_bytes/write_bytes (I/O thật) — chúng cho biết tiến trình nào đang thực sự đọc/ghi đĩa. Nhầm hai loại dẫn tới quy tội sai và tối ưu nhầm chỗ.

Đánh đổi cần cân nhắc

write_bytes bị nhiễu bởi write-back trễ. Kernel không ghi ngay mọi thứ xuống đĩa — nó gom vào cache rồi ghi lùi (write-back) sau. Nên write_bytes của một tiến trình có thể thấp hơn wchar tại một thời điểm (dữ liệu còn trong cache, chưa xuống đĩa), rồi tăng lên sau khi kernel flush. fsync() ép ghi ngay. Khi đo, hiểu rằng ghi đĩa thật có độ trễ so với lời gọi write.

iotop cho cái nhìn realtime nhưng cần quyền. iotop (như top cho I/O) hiển thị I/O đĩa của từng tiến trình theo thời gian thực — tiện hơn đọc /proc thủ công. Nhưng nó cần CAP_NET_ADMIN/root và kernel bật accounting phù hợp, nên không phải môi trường nào cũng chạy được. Khi không có iotop, đọc /proc/<pid>/io lặp lại (hoặc pidstat -d) là phương án thủ công tương đương.

Trong container/máy ảo, I/O đĩa có thêm lớp. Như bind mount trên Docker Desktop, I/O có thể đi qua ranh giới VM hoặc lớp overlayfs, khiến read_bytes/write_bytes phản ánh khác với đĩa vật lý thật. Khi chẩn đoán I/O trong container, kết hợp /proc/<pid>/io với công cụ ở tầng host (hoặc docker stats) để có bức tranh đầy đủ.

Ba ý mang về

  1. /proc/<pid>/io cho I/O của tiến trình: đo thật, tiến trình ghi 200MB hiện wchar=write_bytes=200MB và syscw=200 — biết chính xác nó ghi bao nhiêu và bằng bao nhiêu lời gọi.
  2. Bẫy page cache — rchar cao ≠ đĩa bận: đo thật, đọc file trong cache cho rchar=100MB nhưng read_bytes=0 (từ RAM, không chạm đĩa); muốn biết đĩa có bận thật, nhìn read_bytes/write_bytes.
  3. Đo delta để tìm thủ phạm quật đĩa: đọc /proc/<pid>/io hai lần thấy write_bytes tăng (0→150MB) — tiến trình đang ghi liên tục; nhớ write-back gây trễ, và dùng iotop/pidstat -d cho cái nhìn realtime.

Nguồn

Phần sau ta giải mã một con số ai cũng thấy mà ít người hiểu: load average — ba số 1/5/15 phút thực sự đo gì, vì sao nó khác %CPU, và vì sao load cao không phải lúc nào cũng là CPU quá tải.