bài về CFS ta thấy nice là trọng số chia CPU giữa các tiến trình cùng lớp. Nhưng nice không phải toàn bộ câu chuyện độ ưu tiên. Có một tầng ưu tiên nằm hẳn trên mọi mức nice — lớp real-time — và ranh giới giữa hai tầng quyết định điều mà nice một mình không làm được: bỏ đói hoàn toàn một tiến trình. Tôi đo cả hai tầng trong container, và lần đo đầu tiên cho tôi một con số sạch đẹp mà hoàn toàn sai.

nice và độ ưu tiên

Hai tầng ưu tiên

Linux xếp ưu tiên CPU thành hai tầng tách biệt. Tầng dưới là lớp thường (SCHED_OTHER), nơi gần như mọi tiến trình sống, và trong lớp này nice (-20 tới +19) là trọng số chia phần. Tầng trên là các lớp real-time (SCHED_FIFO, SCHED_RR), dành cho tác vụ cần chạy đúng hạn — xử lý âm thanh, điều khiển thiết bị. Nguyên tắc thép: mọi tiến trình real-time luôn được ưu tiên hơn mọi tiến trình thường, bất kể nice của tiến trình thường thấp đến đâu. Một tiến trình SCHED_FIFO sẵn sàng chạy sẽ chiếm CPU và không nhường cho tiến trình thường nào cho tới khi nó tự ngủ hoặc xong.

Khác biệt cốt lõi giữa hai tầng là chuyện bỏ đói. Trong lớp thường, CFS bảo đảm ai cũng nhích tới — kể cả tiến trình nice 19 (thấp nhất) cũng được một mẩu CPU. Ở lớp real-time thì không có bảo đảm đó: một tiến trình RT có thể bỏ đói mọi tiến trình thường hoàn toàn. Tôi đo để thấy rõ ranh giới này. Ghim mọi thứ vào một lõi, mỗi tiến trình chạy một vòng bận rộn 2 giây và đếm công việc làm được.

Đo tầng thường: chênh cực lớn nhưng không bỏ đói

Trước hết, hai tiến trình thường ở hai cực của thang nice:

nice -20 vs nice 19 (đều SCHED_OTHER, cùng 1 lõi):
  405627  :  395   (tỉ lệ ~100 : 0,1)

Tiến trình nice -20 nuốt gần trọn CPU, còn nice 19 chỉ được khoảng một phần nghìn. Nhưng để ý con số của kẻ thua: 395, không phải 0. Dù chênh lệch trọng số cực lớn (mỗi mức nice nhân ~1,25 lần, 39 mức là hàng nghìn lần), CFS vẫn nhét cho tiến trình nice 19 một mẩu CPU tí xíu. Trong lớp thường, không ai bị bỏ đói tuyệt đối — đó là bản chất "công bằng" của CFS.

Một lần tôi đo hớ: con số 50:50 sạch đẹp mà vô nghĩa

Rồi tôi lên tầng real-time để xem nó bỏ đói tiến trình thường ra sao. Tôi đặt một tiến trình thành SCHED_FIFO, cho nó tranh một lõi với một tiến trình thường ở nice -20 (ưu tiên cao nhất trong lớp thường), và đo:

SCHED_FIFO vs SCHED_OTHER nice -20:
  202277  :  202546   (50% : 50%)

Năm mươi năm mươi. Sạch sẽ, cân đối. Tôi suýt viết ngay: "thú vị, real-time không bỏ đói được tiến trình thường — chúng vẫn chia đều". Một kết luận trái với sách vở, nhưng con số trông thuyết phục quá.

May là trong code tôi có kiểm mã trả về của sched_setscheduler, và nó báo rtok=0 — nghĩa là lệnh đặt lịch real-time đã thất bại. Container mặc định không có quyền CAP_SYS_NICE để đặt lịch RT, nên lời gọi bị từ chối (EPERM), và cái tiến trình tôi tưởng là SCHED_FIFO vẫn là SCHED_OTHER. Con số 50:50 hoàn toàn thật — nhưng nó là hai tiến trình thường chia đều một lõi, chẳng liên quan gì tới real-time. Tôi đã đo đúng, nhưng đo nhầm đại lượng.

Đây là bài học "số bất khả tố cáo công cụ nói dối" ở dạng tinh vi nhất: kết quả không hề vô lý (50:50 là con số hợp lệ), nên nếu chỉ nhìn nó tôi đã tin. Chỉ có việc kiểm mã trả về của syscall mới lộ ra rằng thao tác tôi nghĩ mình đã làm chưa hề xảy ra. Cấp quyền và đo lại:

docker run --cap-add=SYS_NICE --ulimit rtprio=99 ...
SCHED_FIFO vs nice -20:  rtok=1
  383804  :  20296   (95,0% : 5,0%)

Giờ rtok=1, real-time thật sự có hiệu lực, và SCHED_FIFO đè bẹp tiến trình thường: 95% so với 5%.

Cái 5% không phải real-time lịch sự

Nhưng khoan — nếu real-time bỏ đói tiến trình thường hoàn toàn, sao tiến trình nice -20 vẫn được 5%? Real-time đâu có nhường? Con số 5% quá tròn để là ngẫu nhiên, nên tôi đọc thẳng cấu hình nhân:

/proc/sys/kernel/sched_rt_runtime_us = 950000
/proc/sys/kernel/sched_rt_period_us  = 1000000
-> lớp RT được dùng tối đa 950000/1000000 = 95% CPU

Đúng 95%. Cái 5% mà tiến trình thường nhận được không phải do real-time tử tế, mà là một van an toàn của nhân gọi là RT throttling: nhân cố tình chặn tổng thời gian mà các tiến trình real-time được dùng ở 95% mỗi chu kỳ, chừa lại 5% cho tiến trình thường. Lý do sống còn: một tiến trình SCHED_FIFO chạy một vòng lặp bận rộn mà không tự ngủ sẽ bỏ đói tất cả, kể cả shell của bạn và các tiến trình hệ thống — máy treo cứng, không cách nào vào giết nó. Van 5% đó bảo đảm luôn còn một khe để tiến trình thường (và bạn) giành lại quyền điều khiển. Nếu không có throttle, con số thật sẽ là 100:0 — bỏ đói tuyệt đối.

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

Hệ quả đầu tiên: real-time là con dao hai lưỡi, dùng phải rất cẩn thận. SCHED_FIFO cho bạn ưu tiên tuyệt đối trên mọi tiến trình thường — tuyệt cho một luồng xử lý âm thanh không được phép trễ. Nhưng một lỗi nhỏ (vòng lặp bận rộn không có điểm ngủ) trong một luồng RT có thể làm đói cả hệ thống. Nếu không có van RT-throttle, đó là treo máy. Quy tắc: luồng real-time phải ngắn và luôn có điểm chặn (chờ sự kiện, ngủ), không bao giờ busy-loop vô hạn.

Hệ quả thứ hai: nice và real-time giải quyết hai bài toán khác nhau. nice để nghiêng sự chia sẻ giữa các tác vụ thường cùng tồn tại — cho job nền nhường job tương tác — mà vẫn bảo đảm mọi thứ tiến triển. Real-time để bảo đảm hạn chót cho một số ít tác vụ tối quan trọng, chấp nhận rủi ro bỏ đói phần còn lại. Đừng với tới real-time chỉ vì muốn "chạy nhanh hơn"; trong lớp thường, nice -20 đã cho bạn gần như trọn CPU khi có tranh chấp mà không có rủi ro treo máy.

Hệ quả thứ ba là bài học đo lường sắc nhất của bài: luôn kiểm mã trả về của thao tác đặc quyền, đừng tin con số kết quả trần. Con số mang theo: trong lớp thường, nice -20 so nice 19 ra ~100:0,1 nhưng kẻ thua không bao giờ bị bỏ đói hẳn; real-time đè mọi mức nice và sẽ bỏ đói 100% nếu không có van RT-throttle chặn ở 95% — và một phép đo real-time chỉ có nghĩa sau khi sched_setscheduler thật sự thành công (kiểm rtok), không thì bạn đang đo hai tiến trình thường mà tưởng là real-time. Một con số hợp lý vẫn có thể là câu trả lời cho một câu hỏi khác câu bạn nghĩ mình đang hỏi.

Thử ba mươi giây

Chạy chrt -m để xem dải ưu tiên real-time mà nhân bạn hỗ trợ (thường 1–99 cho FIFO/RR). Rồi thử chrt -f 50 sleep 1 — nếu ra lỗi Operation not permitted, bạn đang thiếu quyền đặt lịch real-time, đúng cái bẫy làm phép đo của tôi câm lặng trở thành sai. Muốn xem lớp và ưu tiên của một tiến trình đang chạy: chrt -p <pid> in ra chính sách (SCHED_OTHER/SCHED_FIFO...) và ưu tiên của nó. Và để thấy van an toàn: cat /proc/sys/kernel/sched_rt_runtime_us — con số 950000 (trên một triệu) chính là cái trần 95% giữ cho một luồng real-time chạy loạn không khóa chết cả máy bạn.