Hình dung một cuộc họp đông người và một người điều phối chia lượt phát biểu. nice là bảo ai đó "nói khẽ hơn, nhường người khác một chút" — họ vẫn chen được, chỉ ít hơn. SCHED_IDLE là "chỉ lên tiếng khi cả phòng im hẳn". SCHED_BATCH nghe như đang nhường lượt, nhưng đo ra thì hoá ra chẳng nhường gì. Còn thời gian thực là "được phép cắt lời tất cả" — nguy hiểm tới mức người điều phối buộc phải giữ lại 5% thời gian cho người thường, kẻo một kẻ nói mãi không dứt làm tê liệt cả cuộc họp. Bài này đo bốn cách chia lượt ấy bằng con số.
Phần trước đo nice và tìm ra nó chỉ có tác dụng khi hai tiến trình cùng một nhân. Ở đây tôi đo bốn cách đổi ưu tiên trong đúng điều kiện đó.
Bốn cách, một phép thử
Hai tiến trình tính toán ghim vào cùng một nhân, chỉ khác ưu tiên của cái thứ hai.
| Cách đổi ưu tiên | Tiến trình thường | Tiến trình được đổi |
|---|---|---|
nice 19 |
98,7% | 1,3% |
chrt -i (SCHED_IDLE) |
99,7% | 0,3% |
chrt -b (SCHED_BATCH) |
50,0% | 50,0% |
chrt -r 10 (thời gian thực) |
5,0% | 95,0% |
Ba kết quả đáng nói.
SCHED_BATCH không hạ ưu tiên chút nào
50/50 — y hệt như không làm gì. Đây là hiểu nhầm phổ biến: tên gọi "batch" nghe như "chạy khi rảnh". Thực ra nó không đụng vào trọng số; trọng số vẫn 1024 như tiến trình thường.
SCHED_BATCH chỉ nói với nhân: tiến trình này không cần phản hồi nhanh, đừng đánh thức nó sớm khi nó trở nên sẵn sàng. Kết quả là ít lần chuyển ngữ cảnh hơn và bộ nhớ đệm CPU ấm hơn — có lợi cho thông lượng của chính nó, không có lợi cho các tiến trình khác. Dùng nó cho công việc tính toán dài không tương tác. Đừng dùng nó để nhường CPU.
SCHED_IDLE mạnh hơn nice 19 bốn lần
nice 19 1,3%
SCHED_IDLE 0,3%
nice 19 có trọng số 15 — nhỏ, nhưng vẫn là một phần của cùng công thức. SCHED_IDLE nằm ở lớp riêng, dưới toàn bộ SCHED_OTHER, và chỉ chạy khi không còn gì khác — đúng "chỉ nói khi cả phòng im hẳn". Với công việc nền thật sự — nén nhật ký, dọn dẹp, sao lưu — SCHED_IDLE là lựa chọn đúng:
chrt -i 0 ./cong-viec-nen
Nó không bao giờ làm chậm công việc chính, và nó vẫn chạy hết trong những khoảng máy rảnh.
Thời gian thực dừng đúng ở 95%
Trong 6 giây trên cùng một nhân:
tiến trình TS (thường) 30 jiffies
tiến trình RR (RT) 571 jiffies
Tỉ lệ 5% / 95%, và con số đó không ngẫu nhiên:
sched_rt_runtime_us = 950000
sched_rt_period_us = 1000000
Nhân cố ý giữ lại 5% mỗi giây cho tiến trình thường. Đây là cơ chế chống treo máy: không có nó, một vòng lặp bận ở SCHED_FIFO sẽ chiếm 100% và bạn không còn cách nào đăng nhập để giết nó. Nếu bạn từng đọc lời khuyên "đừng dùng SCHED_FIFO trừ khi biết rõ đang làm gì", đây chính là lý do — và 5% kia là tấm lưới duy nhất.
Có thể tắt lưới đó bằng sched_rt_runtime_us = -1. Đừng, trừ khi bạn đang xây hệ thống thời gian thực thật và có cách khác để phục hồi.
SCHED_FIFO so với SCHED_RR
Cả hai đều là thời gian thực và đều đứng trên CFS. Khác nhau một điểm:
SCHED_FIFOchạy tới khi tự nhường hoặc bị tiến trình RT ưu tiên cao hơn cắt ngang. Không có lát thời gian.SCHED_RRgiống hệt, nhưng các tiến trình cùng mức ưu tiên chia nhau theo lát thời gian.
Với một tiến trình RT duy nhất, hai cái như nhau. Với nhiều tiến trình RT cùng mức, FIFO để cái đầu tiên chạy mãi.
ionice: ưu tiên đĩa, không phải CPU
nice không ảnh hưởng I/O. Cho việc đó có lệnh riêng:
ionice -c 3 ./sao-luu # lop idle: chi doc/ghi khi dia ranh
ionice -c 2 -n 7 ./cong-viec # best-effort, uu tien thap nhat
Nhưng nó chỉ có tác dụng với bộ lập lịch I/O hỗ trợ ưu tiên — bfq và cfq cũ. Với mq-deadline hoặc none (mặc định trên nhiều bản Linux hiện đại cho SSD/NVMe), ionice không làm gì cả. Kiểm trước khi tin:
cat /sys/block/<thiet-bi>/queue/scheduler
Đây là cùng loại bẫy với nice ở phần trước: một lệnh chạy không lỗi, không cảnh báo, và không có tác dụng.
Nhắc lại điều kiện tiên quyết
Mọi con số trong bài này đo với hai tiến trình ghim vào cùng một nhân. Không ghim, trên máy 16 nhân với chỉ hai tiến trình, cả bốn cách đều cho 50/50 — như đo ở phần 6. Ưu tiên chỉ có nghĩa khi có tranh chấp thật. Trước khi chỉnh ưu tiên, hãy kiểm r trong vmstat: nhỏ hơn số nhân nghĩa là chưa có gì để tranh, và mọi chỉnh sửa đều vô ích.
Mẫu số chung: ưu tiên chỉ có nghĩa khi có tranh chấp, và cái tên không phải cái hành vi
Bài này gói hai bài học về mọi cơ chế ưu tiên. Thứ nhất: ưu tiên chỉ đổi được gì khi tài nguyên đang bị tranh — dư thừa thì nó vô nghĩa. Thứ hai: tên của một mức ưu tiên (BATCH, idle, low) mô tả ý định, không đảm bảo hành vi — phải đo mới biết nó thật sự làm gì. Cả hai lặp ở mọi tầng.
Trong thế giới Java, Thread.setPriority() là ví dụ kinh điển: API có 1–10 mức, nhưng trên phần lớn JVM/OS nó gần như không có tác dụng đo được — đúng cái bẫy "lệnh chạy không lỗi, không tác dụng" của ionice trên scheduler sai. Và ThreadPoolExecutor với priority queue chỉ phân biệt được tác vụ khi pool bão hoà; còn thread rảnh thì thứ tự ưu tiên chẳng đổi gì — đúng "ưu tiên chỉ có nghĩa khi có tranh chấp".
Trong Go, goroutine không có mức ưu tiên — người ta phải tự dựng bằng select trên nhiều channel có trọng số hay worker pool tách hạng; và điều đó chỉ quan trọng khi số goroutine sẵn sàng vượt GOMAXPROCS. Dưới ngưỡng đó, "ưu tiên" bạn dựng ra không biểu hiện.
Còn ở cơ sở dữ liệu, đây là resource governor / workload management: đặt một truy vấn vào hạng "thấp" chỉ ghìm nó lại khi hệ đang nghẽn CPU/IO; lúc rảnh nó chạy full tốc độ. Và các nhãn cô lập giao dịch cũng vậy — tên mức (READ COMMITTED, SERIALIZABLE) mô tả ý định, còn hành vi thật (có chặn, có phantom không) phải đọc kỹ engine cụ thể mới biết, không suy từ tên.
Mẫu số chung: mọi hệ thống ưu tiên chỉ thay đổi kết quả khi tài nguyên đang bị tranh — nên chỉnh ưu tiên lúc chưa nghẽn là công cốc; và nhãn của một mức ưu tiên là lời hứa về ý định, không phải mô tả hành vi đã kiểm chứng, nên một cái tên nghe 'nhường nhịn' có thể chẳng nhường gì. Nên trước khi chỉnh bất kỳ mức ưu tiên nào, hai câu hỏi đúng là "tài nguyên này có đang thật sự bị tranh không (nếu không thì đừng chỉnh)" và "tôi đã đo mức này làm gì, hay chỉ tin vào cái tên của nó".
Muốn tự soi có tiến trình nào đang chạy ở lớp bất thường không:
ps -eo pid,cls,ni,rtprio,pcpu,comm --sort=-pcpu | head -20
Cột CLS:
| Nghĩa | |
|---|---|
TS |
CFS thường |
IDL |
SCHED_IDLE |
B |
SCHED_BATCH |
FF |
SCHED_FIFO — thời gian thực |
RR |
SCHED_RR — thời gian thực |
Thấy FF hoặc RR ở một tiến trình bạn không cố ý đặt là điều đáng hỏi ngay: nó đứng trên mọi thứ còn lại trên máy, và chỉ có 5% dự phòng của nhân ngăn nó chiếm trọn. Và với công việc nền, so NI với lớp: NI=19 và CLS=TS vẫn nhận 1,3%; đổi sang CLS=IDL đưa nó xuống 0,3% mà không mất gì.
Phần sau: ái lực CPU và NUMA — chuyện gì xảy ra khi tiến trình nhảy giữa các lõi.