Phần 18 đo bộ lập lịch mặc định CFS: công bằng, chia thời gian đều, ai cũng có phần. Đó là điều tốt cho throughput chung, nhưng tệ cho một thứ khác — độ trễ đúng hạn. Một luồng cần phản ứng ngay (xử lý âm thanh, điều khiển động cơ, đọc cảm biến) không muốn "công bằng"; nó muốn được ưu tiên tuyệt đối. Đó là việc của lịch thời gian thực (real-time), mà lớp phổ biến nhất là SCHED_FIFO. Nhiều người nghĩ đặt luồng vào real-time nghĩa là nó "chạy nhanh hơn". Tôi đo trong container gcc:13 (cần quyền RT: --cap-add=SYS_NICE --ulimit rtprio=99), và con số cho thấy real-time không phải nhanh hơn — nó là đúng hạn hơn, và đi kèm một cái bẫy chết người.
SCHED_FIFO: ưu tiên cứng, không công bằng
SCHED_FIFO là một lớp lập lịch ưu tiên cứng. Một luồng FIFO, khi được chạy, sẽ chạy cho tới khi nó tự block (chờ I/O, khóa, ngủ) hoặc bị một luồng RT ưu tiên cao hơn tước. Nó không bị time-slice bởi các luồng cùng hoặc thấp hơn — khác hẳn CFS, nơi mọi luồng luân phiên chia lõi. Với FIFO, luồng ưu tiên cao luôn thắng luồng thấp, tức thì, không nhân nhượng.
Điều đó cho một tính chất quý: độ trễ tất định. Khi hệ đang tải nặng (nhiều luồng tranh CPU), một luồng thường (CFS) muốn thức dậy đúng hạn sẽ phải chờ tới lượt nó trong vòng luân phiên — có thể trễ cả một timeslice. Một luồng FIFO ưu tiên cao thì tước CPU của mọi luồng thường ngay lập tức, nên nó thức đúng hạn bất kể hệ bận thế nào. Tôi đo chính điều này: một luồng định kỳ (thức mỗi 5ms, đo độ trễ so với mốc bằng kỳ hạn tuyệt đối phần 30), chạy chung một lõi với 4 luồng ngốn CPU, một lần là SCHED_OTHER và một lần là SCHED_FIFO.
Đo: jitter 3ms tụt còn 12µs
Luồng định kỳ (thức mỗi 5ms), chung 1 lõi với 4 luồng ngốn CPU:
SCHED_OTHER (công bằng, tranh với hog): trễ tb = 639 µs MAX = 3001 µs (~3 ms)
SCHED_FIFO(80) (tước hog, ưu tiên cao) : trễ tb = 3,4 µs MAX = 11,7 µs
-> độ trễ MAX giảm ~256 lần (jitter 3 ms -> 12 µs)
Là SCHED_OTHER, luồng định kỳ cạnh tranh công bằng với 4 luồng ngốn CPU. Nó thức dậy trễ trung bình 639µs, và tệ nhất trễ tới 3001µs (~3ms) — đúng bằng một timeslice CFS. Với một tác vụ cần phản ứng mỗi 5ms, một cú trễ 3ms là thảm họa. Chuyển đúng luồng đó sang SCHED_FIFO ưu tiên 80: giờ nó tước CPU của các hog ngay khi cần thức, nên trễ trung bình chỉ 3,4µs và tệ nhất 11,7µs. Độ trễ tệ nhất giảm 256 lần — jitter từ 3ms xuống 12µs.
Chú ý: cái đổi được là độ trễ, không phải tốc độ tính toán. Luồng định kỳ không làm việc gì nhanh hơn; nó chỉ được chạy đúng lúc. Các luồng ngốn CPU vẫn xử lý cùng khối lượng, chỉ bị chen ngang mỗi khi luồng RT cần. Real-time là chuyện khi nào, không phải nhanh cỡ nào.
Cái bẫy: một FIFO không block đói cả hệ
Sức mạnh "ưu tiên tuyệt đối" của FIFO cũng là con dao hai lưỡi. Vì một luồng FIFO chạy tới khi tự block, một luồng FIFO không bao giờ block (một vòng lặp tính toán, hay tệ hơn, một bug vòng vô hạn) sẽ chiếm lõi mãi mãi, bỏ đói mọi luồng ưu tiên thấp hơn. Tôi đo: một luồng FIFO(80) quay CPU trong 300ms trên một lõi, và một luồng thường (SCHED_OTHER) đếm tiến độ trên cùng lõi đó.
FIFO(80) busy 300ms trên CPU0, luồng OTHER cùng lõi đếm tiến độ:
Trong lúc FIFO chạy : OTHER tiến 0 vòng (ĐÓI hoàn toàn)
200ms sau khi FIFO xong: OTHER tiến 21901 vòng (bình thường trở lại)
Luồng thường tiến được 0 vòng suốt 300ms — nó bị đói tuyệt đối, không nhích một chút nào, cho tới khi luồng FIFO tự kết thúc. Trên máy thật, nếu một luồng RT ưu tiên cao vô tình rơi vào vòng lặp không block, nó có thể treo cả hệ — kể cả các tiến trình hệ thống. (Linux có một cơ chế throttle RT mặc định — giới hạn tổng thời gian RT được chiếm — làm lưới an toàn, nhưng đừng dựa vào nó.) Đây là lý do real-time là quyền lực phải dùng rất cẩn thận.
Một lần tôi đo hớ: "real-time nghĩa là nhanh hơn"
Tôi vào đo với niềm tin phổ biến nhất về từ "real-time": "đặt luồng vào lịch thời gian thực thì nó chạy nhanh hơn". Đo phá tan: SCHED_FIFO không làm luồng tính toán nhanh hơn một chút nào, không tăng throughput. Cái nó thay đổi là độ trễ và tính tất định — luồng RT thức và phản ứng đúng hạn, với jitter thấp (12µs thay vì 3ms), dưới tải nặng. "Real-time" trong khoa học máy tính chưa bao giờ nghĩa là "nhanh"; nó nghĩa là "đúng hạn, đoán trước được" — một hệ real-time là hệ mà bạn bảo đảm được thời hạn, dù thời hạn đó có thể chậm.
Bài học đo lường: real-time không phải tối ưu tốc độ mà là tối ưu độ trễ tất định; nó đổi công bằng lấy đúng hạn, và cái giá là một luồng RT lỗi có thể bỏ đói cả hệ. Nếu tôi tin "real-time = nhanh" và rải SCHED_FIFO lên các luồng tính toán để "tăng tốc", tôi đã không được một nano-giây nào nhanh hơn, mà lại tạo nguy cơ treo máy nếu một luồng nào đó không block đúng lúc. Đo cho thấy giá trị thật của RT nằm ở phân phối độ trễ (đặc biệt phần đuôi — max/jitter), không phải ở tốc độ trung bình.
Vì sao điều này quan trọng khi lập trình
Hệ quả đầu tiên: dùng real-time cho tác vụ cần đúng hạn, không phải cho tác vụ cần nhanh. Xử lý âm thanh (không được rớt buffer), điều khiển (chu kỳ cứng), đọc cảm biến định kỳ — đó là chỗ SCHED_FIFO tỏa sáng: nó bảo đảm luồng phản ứng đúng hạn dù hệ bận. Còn một tác vụ tính toán nặng muốn xong nhanh thì real-time không giúp gì; số lõi và thuật toán mới quyết định.
Hệ quả thứ hai: luồng real-time phải block, và phải bị giới hạn. Một luồng RT nên làm một mẩu việc ngắn rồi ngủ/chờ sự kiện tiếp theo — không bao giờ quay CPU vô hạn. Hãy ghim nó vào một lõi riêng nếu có thể, đặt ưu tiên vừa đủ (không phải max), và để dành lõi cho các luồng hệ thống. Một vòng lặp bận ở ưu tiên RT là công thức treo máy.
Hệ quả thứ ba là tinh thần đo lường: đo phần đuôi độ trễ, không chỉ trung bình — và biết real-time đổi gì lấy gì. Con số mang theo: SCHED_FIFO (real-time) KHÔNG làm chương trình nhanh hơn (không tăng tốc CPU/throughput); nó cho độ trễ TẤT ĐỊNH — đo trễ đánh thức MAX của luồng định kỳ dưới tải tụt từ 3001µs (SCHED_OTHER, công bằng) xuống 11,7µs (SCHED_FIFO ưu tiên cao), thấp hơn 256 lần. Đổi lại: một FIFO không block ở ưu tiên cao đói mọi luồng thường (đo: luồng OTHER tiến 0 vòng suốt 300ms FIFO busy), có thể treo cả hệ — nên phải ghim lõi, giới hạn, dùng ưu tiên hợp lý. Real-time là lời hứa về đúng hạn, không phải một nút tăng tốc — và là quyền lực đủ mạnh để tự bắn vào chân nếu dùng ẩu.
Thử ba mươi giây
Nghĩ về một tác vụ bạn muốn "ưu tiên cao": bạn cần nó nhanh hơn (xong sớm hơn) hay đúng hạn hơn (phản ứng đều, không trễ đột biến)? Nếu là nhanh hơn, real-time không phải câu trả lời — nó không tăng tốc gì, chỉ đổi thứ tự; hãy nhìn vào thuật toán và số lõi. Nếu là đúng hạn (một vòng lặp phải chạy đều mỗi X ms bất kể hệ bận), thì SCHED_FIFO có thể cắt jitter của bạn hàng trăm lần — nhưng chỉ khi luồng đó block giữa các lần chạy và bạn giới hạn nó cẩn thận. Ba mươi giây tự hỏi "mình cần nhanh hay cần đúng hạn?" đó tách được hai thứ mà chữ "real-time" hay bị nhầm là một — và tránh cho bạn vừa không được tốc độ, vừa rước nguy cơ treo máy.