bài trước ta soi chương trình từ ngoài bằng cách bẫy syscall. Có một cách nhìn khác, nhẹ hơn nhiều và không làm chậm gì: hỏi thẳng nhân về một tiến trình đang sống — nó đang ở trạng thái nào, dùng bao nhiêu CPU, chiếm bao nhiêu bộ nhớ, mở bao nhiêu file. Cửa sổ đó là /proc. Bài này đọc /proc của những tiến trình thật với đủ trạng thái, và vấp một cái bẫy tinh vi về chính chữ "self".

/proc và trạng thái tiến trình

/proc là giao diện của nhân, không phải file trên đĩa

Nhìn qua, /proc/1234/stat trông y như một file: có đường dẫn, đọc được bằng cat, awk, bất cứ công cụ text nào. Nhưng nó không phải file trên đĩa. /proc là một hệ thống tệp ảo (procfs) do nhân dựng ra: mỗi lần bạn đọc một tệp trong đó, nhân sinh ra nội dung ngay tại chỗ, từ cấu trúc dữ liệu sống của tiến trình trong bộ nhớ nhân. Bằng chứng đơn giản nhất: stat -c %s /proc/self/stat báo size = 0 byte. Một file 0 byte mà cat ra cả trăm ký tự? Vì không có byte nào nằm sẵn trên đĩa cả — chúng được tạo ra vào đúng khoảnh khắc bạn mở ra đọc.

Điều đó có một hệ quả quan trọng: /proc luôn phản ánh trạng thái hiện tại. Đọc lại lần nữa, bạn có thể nhận số khác — không phải vì file bị sửa, mà vì nhân sinh lại từ trạng thái mới. Tôi kiểm: đọc VmRSS (bộ nhớ thật đang chiếm) của một tiến trình, được 1296 kB; cấp thêm 20MB rồi chạm vào từng trang, đọc lại ngay — 1708 kB. Không ai ghi vào file; nhân chỉ đơn giản báo cáo sự thật mới mỗi lần được hỏi.

Đo: bốn trạng thái R, S, D, Z

Trường đáng chú ý nhất trong /proc/<pid>/statState — tiến trình đang làm gì. Tôi tạo ba tiến trình có chủ đích và đọc trạng thái của chúng:

Tiến trình State wchan (ngủ ở đâu)
sleep 300 S (sleeping) hrtimer_nanosleep
vòng lặp bận while :; do :; done R (running) 0 (không ngủ)
con đã thoát mà cha chưa wait Z (zombie) 0

Bốn trạng thái chính của một tiến trình Linux:

  • R (running): đang chạy trên CPU, hoặc sẵn sàng chạy và chờ tới lượt. Tiến trình bận của tôi luôn ở đây.
  • S (sleeping): đang ngủ, chờ một sự kiện đánh thức (hết giờ, có dữ liệu tới). Ngủ này ngắt được — một tín hiệu đánh thức nó dậy. sleep 300 nằm ở đây, và wchan cho biết nó ngủ tại hàm nhân nào: hrtimer_nanosleep.
  • D (uninterruptible sleep): ngủ sâu, thường chờ I/O đĩa, không ngắt được bằng tín hiệu (kể cả kill -9) cho tới khi việc I/O xong. Một tiến trình kẹt ở D lâu là dấu hiệu I/O có vấn đề.
  • Z (zombie): tiến trình đã chết nhưng chưa được cha wait để lấy mã thoát và dọn. Nó không chạy, không chiếm bộ nhớ đáng kể, chỉ còn một mục trong bảng tiến trình — cho tới khi cha thu dọn.

Trường utime (thời gian CPU đã dùng, tính bằng jiffies — nhịp đồng hồ nhân) là bằng chứng sống cho tính "ảnh tức thời". Đọc utime của tiến trình bận ba lần cách nhau nửa giây: 0 → 51 → 102 — tăng đều, vì nó thật sự đốt CPU liên tục (khoảng 100 jiffies mỗi giây, đúng nhịp 100Hz). Đọc utime của sleep: 0 → 0 — đứng yên, vì nó ngủ, không tiêu CPU. Chỉ một trường số, đọc vài lần, đã phân biệt rõ tiến trình làm việc với tiến trình nằm chờ.

Một lần tôi đo hớ: "self" là kẻ đang đọc, không phải tôi

Trước khi đo trên các pid cụ thể ở trên, tôi thử một thứ nghe hiển nhiên hơn: xem CPU của chính script mình tăng dần. Kế hoạch: đọc /proc/self/stat lấy utime, chạy một vòng lặp nặng đốt CPU, rồi đọc /proc/self/stat lần nữa — utime phải tăng. Tôi chạy, và cả hai lần đều ra utime = 0.

Hai số 0 giống hệt nhau, sau khi rõ ràng đã đốt CPU ở giữa. Phản xạ đầu tiên: "nhân đếm sai, hay /proc không cập nhật?". Nhưng theo kỷ luật đo lường, một con số bất khả như vậy thường là tôi đo nhầm đại lượng, không phải công cụ hỏng. Và đúng thế.

Mấu chốt nằm ở chữ self. /proc/self không trỏ tới "script của tôi" — nó trỏ tới tiến trình đang mở nó, bất kể đó là ai. Trong shell, mỗi lệnh $(awk '{print $14}' /proc/self/stat) sinh ra một tiến trình awk mới toanh; awk đó mở /proc/self, và self lúc ấy chính là awk — một tiến trình vừa ra đời một phần nghìn giây trước, dĩ nhiên utime = 0. Vòng lặp nặng của tôi chạy trong một tiến trình khác; awk không dính dáng gì tới nó. Tôi tưởng mình đo CPU của công việc nặng, nhưng thật ra mỗi lần đo là CPU của một awk sơ sinh khác nhau — luôn bằng 0.

Sửa thì đơn giản: đừng đọc /proc/self, hãy đọc /proc/<pid-cụ-thể>/stat của đúng tiến trình muốn quan sát. Làm vậy với pid của vòng lặp bận, utime tăng 0 → 51 → 102 như mong đợi — nhân đếm hoàn toàn đúng. Bài học đo lường: /proc/self luôn là kẻ đang đọc, và hai số 0 giống nhau ở đây là dấu hiệu tôi trỏ nhầm đối tượng, chứ không phải nhân sai. Khi một phép đo ra con số bất khả, hỏi trước tiên "mình có đang đo đúng cái mình nghĩ không" — ở đây tôi đo CPU của công cụ đo, không phải của thứ cần đo.

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

Hệ quả đầu tiên: /proc là cách rẻ và mạnh nhất để soi một tiến trình đang chạy mà không làm phiền nó. Không như strace (chậm 270 lần, bài trước), đọc /proc gần như miễn phí — nhân chỉ định dạng vài cấu trúc sẵn có. Mọi công cụ quen thuộc thực chất chỉ là bộ đọc /proc cho đẹp: ps đọc stat/status, top đọc lặp lại, htop cũng vậy. Biết đọc thẳng /proc/<pid>/status (cho VmRSS, Threads, số chuyển ngữ cảnh), /proc/<pid>/fd (đếm file đang mở — hữu ích khi nghi rò rỉ fd), /proc/<pid>/wchan (tiến trình treo đang kẹt ở hàm nhân nào) cho bạn chẩn đoán mà không cần cài gì.

Hệ quả thứ hai: hiểu trạng thái tiến trình giúp đọc đúng một sự cố. Một tiến trình ps báo D dai dẳng nghĩa là nó kẹt chờ I/O không ngắt được — kill -9 cũng vô dụng, phải tìm cái đĩa/mạng đang treo. Một đống tiến trình Z (zombie) nghĩa là chương trình cha quên wait con — không tốn CPU hay bộ nhớ nhưng ăn dần số hiệu tiến trình. Phân biệt R với S cho biết máy đang thật sự bận hay chỉ nhiều tiến trình đang ngủ chờ. Không có mấy chữ cái này, "hệ thống chậm" chỉ là cảm giác; có chúng, bạn chỉ đúng vào chỗ nghẽn.

Hệ quả thứ ba là bài học chung: biết công cụ đo của mình đang trỏ vào cái gì. Con số mang theo: /proc là giao diện nhân sinh nội dung KHI ĐỌC (nên stat báo 0 byte và đọc lại ra số mới); trường State phân biệt R/S/D/Z, utime cho thấy CPU tiêu thật, wchan chỉ nơi tiến trình ngủ — nhưng /proc/self luôn là kẻ đang đọc, không phải chương trình bạn nghĩ. Một con số bất khả (CPU bằng 0 sau khi đốt CPU) hầu như luôn là dấu hiệu bạn đo nhầm đối tượng, không phải nhân nói dối — kiểm lại xem "self" hay pid bạn đang đọc có đúng là thứ cần đo hay không.

Thử ba mươi giây

Trên một máy Linux, chọn một tiến trình đang chạy (ví dụ trình duyệt): pgrep -n firefox lấy pid, rồi cat /proc/<pid>/status | grep -E 'State|VmRSS|Threads|voluntary'. Bạn sẽ thấy trạng thái, bộ nhớ thật đang chiếm, số luồng, số lần bị/được chuyển ngữ cảnh — tất cả sinh tươi ngay lúc bạn đọc. Đọc lại lần nữa sau vài giây và so VmRSS: nó đổi, vì /proc báo trạng thái sống chứ không phải một file tĩnh. Rồi thử cat /proc/self/status | grep State vài lần — mỗi lần là một tiến trình cat khác nhau, nên bạn luôn thấy R (running): chính lệnh đang đọc là thứ đang chạy, đúng cái bẫy "self" mà bài này vấp.