Khi bạn chạy ps hay top, mỗi tiến trình có một chữ cái trạng thái — R, S, D, Z... Những chữ này không phải trang trí: chúng cho biết tiến trình đang làm gì với nhân, và đọc đúng chúng là một nửa việc chẩn đoán một hệ thống chậm hay treo. Bài này dựng từng trạng thái một cách có chủ đích rồi đọc thẳng /proc để xác nhận, và trên đường đo, tôi hiểu sai một con số quen thuộc: load average.
Máy trạng thái của một tiến trình
Nhân Linux gán cho mỗi tiến trình một trạng thái, đọc được ở trường thứ ba của /proc/PID/stat:
- R (Running/Runnable): đang chạy trên một nhân, hoặc sẵn sàng chạy và chỉ đang chờ tới lượt.
- S (Sleeping, interruptible): đang ngủ nhẹ, chờ một sự kiện (dữ liệu tới, hết giờ
sleep, một khóa). Có thể đánh thức bằng tín hiệu — đây là trạng thái phổ biến nhất của các tiến trình nhàn rỗi. - D (Uninterruptible sleep): đang ngủ mà không ngắt được, thường vì đang chờ một thao tác đĩa hoàn tất. Không thể đánh thức bằng tín hiệu — thậm chí
kill -9cũng không giết được một tiến trình đang ở D cho tới khi nó tự thoát ra. - Z (Zombie): đã chết (gọi
exit) nhưng tiến trình cha chưawaitđể thu nhận mã thoát, nên một mục nhỏ vẫn nằm lại trong bảng tiến trình. - T (Stopped): bị dừng bởi một tín hiệu như
SIGSTOP(ví dụ khi bạn nhấn Ctrl-Z).
Đo: dựng từng trạng thái
Tôi tạo mỗi trạng thái một cách có kiểm soát trong container rồi đọc /proc/PID/stat:
| Cách dựng | Trạng thái đọc được |
|---|---|
| Vòng lặp busy (đốt CPU) | R |
sleep 300 (chờ hết giờ) |
S |
dd oflag=dsync (ghi đồng bộ) |
D (217/300 mẫu) |
Con exit, cha không wait |
Z (ps cũng báo Z) |
Gửi SIGSTOP |
T |
Bốn cái đầu tiên (R, S, Z, T) hiện ra đúng như mong đợi, tức thì và ổn định. Cái thứ ba đáng chú ý: tôi cho dd ghi với cờ oflag=dsync — nghĩa là mỗi khối ghi phải chờ đĩa xác nhận đã ghi xong mới ghi tiếp. Lấy mẫu trạng thái 300 lần trong lúc nó chạy, 217 lần thấy D (và 83 lần R). Vậy một tiến trình ghi đồng bộ nằm trong trạng thái ngủ không ngắt được phần lớn thời gian — nó liên tục chờ đĩa, và trong lúc chờ đó nó không phản ứng với tín hiệu nào.
Một lần tôi đo hớ: load average không phải đồng hồ tức thời
Sau khi có sẵn một tiến trình R (busy) và một tiến trình D (dd đồng bộ) cùng chạy, tôi liếc /proc/loadavg để xem load average, chắc mẩm nó sẽ vọt lên vì cả R và D đều được tính vào load. Nhưng nó đứng im ở 0,29 — gần như không đổi so với lúc rảnh. Phản xạ đầu tiên: "ơ, load average không đếm mấy trạng thái này à?".
Sai, và cái sai nằm ở chính bản chất của con số. Load average có đếm cả tiến trình R lẫn D — nhưng nó là một trung bình trượt hàm mũ trên các cửa sổ 1, 5, và 15 phút, không phải một đồng hồ đo tức thời. Một cú tải chỉ kéo dài 3 giây (đúng thời gian bài đo chạy) gần như không nhích nổi con số trung bình một phút. Để chứng minh, tôi giữ hai tiến trình busy chạy liên tục rồi theo dõi load một phút mỗi 5 giây:
0,16 -> 0,31 -> 0,44 -> 0,57 -> 0,68 -> 0,79 -> 0,89 -> 1,06 (sau 40 giây)
Giờ nó bò lên đều đặn, hướng dần tới 2 (số tiến trình đang tranh CPU) nhưng phải mất hơn một phút mới tiệm cận. Đó là bản chất trung bình trượt: nó làm mượt để lọc nhiễu, đổi lại phản ứng chậm.
Bài học đo lường: đọc một con số đã được làm mượt như thể nó tức thời là hiểu sai nó. Load average sinh ra để cho biết xu hướng tải trung hạn, không phải để chụp nhanh một khoảnh khắc — nhìn nó ngay sau khi bật tải là nhìn nhầm công cụ. Và tôi còn đo hớ ngược một chuyện về D: tôi ngỡ D là trạng thái hiếm, khó bắt được (vì "ngủ không ngắt được" nghe như phải rất ngắn). Thực ra một tiến trình I/O đồng bộ nằm trong D tới 72% thời gian — D không hiếm với tải nặng đĩa. Cái đáng sợ không phải thấy D, mà là thấy một tiến trình D không bao giờ thoát ra (đĩa hỏng, mount NFS chết) — khi đó nó treo vĩnh viễn và không kill được.
Vì sao điều này quan trọng khi lập trình
Hệ quả đầu tiên là đọc đúng trạng thái để chẩn đoán. Một hệ thống "chậm" mà nhiều tiến trình ở D nghĩa là nút thắt ở I/O (đĩa, mạng lưu trữ) — không phải CPU; thêm CPU vô ích. Nhiều tiến trình ở R nhưng máy phản hồi chậm nghĩa là thiếu CPU (nhiều thứ tranh nhau chạy). Một đống Z nghĩa là tiến trình cha bị lỗi không wait con — rò rỉ mục bảng tiến trình. Mỗi chữ cái trỏ tới một loại vấn đề khác nhau, và ps hay top cho bạn đọc chúng miễn phí.
Hệ quả thứ hai là hiểu vì sao một tiến trình đôi khi "không giết được". Nếu kill -9 mà tiến trình vẫn trơ ra, gần như chắc chắn nó đang ở D: tín hiệu đã được gửi nhưng nhân không giao được cho tới khi tiến trình thoát khỏi thao tác không-ngắt-được (thường là chờ đĩa). Với đĩa khỏe, nó thoát ra sau vài mili giây và chết ngay; với thiết bị treo, nó kẹt mãi. Thấy một tiến trình kill -9 không chết, đừng gõ kill mạnh hơn — hãy xem nó đang ở trạng thái gì và cái I/O nào đang treo.
Hệ quả thứ ba là đọc load average đúng ngữ cảnh. Vì nó gồm cả R lẫn D, load cao không nhất thiết là CPU bận — nó có thể là hàng loạt tiến trình kẹt ở D chờ một đĩa chậm, trong khi CPU rảnh rỗi. Và vì nó là trung bình trượt, đừng hoảng vì một gai tức thời cũng đừng mừng vì một khoảnh khắc thấp; nhìn cả ba số 1/5/15 phút để thấy xu hướng. Con số mang theo: mỗi tiến trình luôn ở một trạng thái đọc được ở /proc — R chạy, S ngủ nhẹ, D ngủ không-ngắt (kill -9 cũng chờ), Z zombie, T bị dừng; một tiến trình ghi đồng bộ ở D tới 72% thời gian; và load average đếm R+D nhưng là trung bình trượt 1/5/15 phút nên phản ứng chậm (0,16 lên 1,06 sau 40 giây với hai tiến trình busy). Trạng thái là ngôn ngữ tiến trình dùng để nói cho bạn biết nó đang kẹt ở đâu — học đọc nó.
Thử ba mươi giây
Chạy top (hoặc ps -eo pid,stat,comm) trên máy Linux và nhìn cột trạng thái. Đa số tiến trình sẽ là S (ngủ nhẹ, nhàn rỗi) — điều bình thường. Tìm chữ R (đang chạy), và nếu thấy D thì đó là tiến trình đang chờ I/O. Muốn tự tạo một zombie để xem: chạy python3 -c 'import os,time; os._exit(0) if os.fork()==0 else time.sleep(60)' rồi trong 60 giây đó chạy ps -el | grep Z — bạn sẽ thấy một tiến trình <defunct> ở trạng thái Z, đúng cái bài này dựng. Rồi mở tải nặng (một vòng lặp busy) và theo dõi cat /proc/loadavg mỗi vài giây: bạn sẽ thấy con số bò lên chứ không nhảy — đó là trung bình trượt đang từ từ bắt kịp, đúng như bài đo.