Mở đầu: một con tàu trên sao Hoả suýt bị mất vì đúng bug này
Tháng 7 năm 1997, tàu thám hiểm Mars Pathfinder của NASA hạ cánh thành công xuống sao Hoả — rồi vài ngày sau bắt đầu tự động reset liên tục, mất dữ liệu khoa học quý giá. Nguyên nhân không phải một thiên thạch hay lỗi phần cứng, mà là một dạng bug đồng bộ hoá kinh điển: priority inversion — một task ưu tiên THẤP vô tình khiến một task ưu tiên CAO bị trễ gần như vô thời hạn. Bài này không chỉ giải thích khái niệm — nó DỰNG LẠI chính xác cơ chế đó trên VMCU, verify bằng số thật, rồi sửa nó bằng đúng kỹ thuật NASA đã dùng để cứu con tàu từ xa qua radio.
Trước khi tới đó, bài học lắp ráp 3 công cụ đồng bộ hoá chuẩn của mọi RTOS nhúng — semaphore, mutex, queue — thay thế cho cách tắt ngắt thô bạo của Bài 8, giờ đã không còn đủ mịn khi có nhiều task cùng chạy preemptive.
1. Race trở lại, to hơn
Với preemption của Bài 14, MỘT read-modify-write (Bài 2) có thể vỡ ở BẤT KỲ dòng nào — không chỉ giữa ISR
và main như Bài 8, mà giữa bất kỳ 2 task nào, tại bất kỳ thời điểm nào. Công cụ tắt ngắt
(__disable_irq()) của Bài 8 về mặt kỹ thuật vẫn hoạt động, nhưng giờ quá thô bạo: tắt ngắt
nghĩa là giết luôn cả bộ lập lịch — không task nào khác được chạy, kể cả những task hoàn
toàn không liên quan tới tài nguyên đang cần bảo vệ. Cần công cụ mịn hơn, bảo vệ ĐÚNG tài nguyên đang
tranh chấp mà không chặn đứng toàn hệ thống.
2. Semaphore
Semaphore về bản chất là một bộ đếm tài nguyên kèm cơ chế đánh thức:
give() tăng bộ đếm (hoặc đánh thức thẳng 1 task đang chờ nếu có), take() giảm bộ
đếm nếu còn (>0) hoặc khiến task gọi nó chuyển sang Blocked nếu hết.
Pattern đẹp nhất và phổ biến nhất: ISR give — task blocked take. Một ISR (vd dữ liệu cảm
biến vừa tới) gọi give(), một task đang take() và ở trạng thái Blocked THỨC DẬY
ĐÚNG LÚC — không cần vòng lặp poll liên tục kiểu cờ volatile của Bài 8. Verify: task gọi
take() khi bộ đếm = 0 lập tức chuyển Blocked; ngay khi give() được gọi, task đó
chuyển thẳng về Ready — không có độ trễ poll nào.
Có 2 biến thể: binary semaphore (đếm chỉ 0 hoặc 1, dùng làm tín hiệu đơn — "đã xảy ra" hay chưa) và counting semaphore (đếm nhiều đơn vị — vd số ô trống trong một bể tài nguyên dùng chung).
3. Mutex
Mutex (Mutual Exclusion) trông giống binary semaphore nhưng có một khác biệt SỐNG CÒN:
khái niệm quyền sở hữu (ownership) — chỉ đúng task đã khoá được phép mở khoá lại. Verify:
một task khác gọi lock() khi mutex đã có chủ sẽ bị chặn (Blocked) ngay lập tức; khi chủ cũ
unlock(), khoá được trao THẲNG cho task đang chờ có ưu tiên cao nhất (không phải theo thứ tự
tới trước) và task đó Ready ngay lập tức.
4. Queue
Queue gửi DỮ LIỆU thật, không chỉ một tín hiệu như semaphore — đây là công cụ chuẩn cho mô hình producer-consumer trong RTOS. So với ring buffer SPSC của Bài 8:
| Đặc điểm | Ring buffer SPSC (Bài 8) | Queue (RTOS) |
|---|---|---|
| Dùng khi nào | Chia sẻ ISR ↔ main, không RTOS | Chia sẻ giữa nhiều task, có RTOS |
| Đầy/rỗng | Bên gọi tự kiểm tra, tự xử lý | Task tự động Blocked, đánh thức khi có dữ liệu/chỗ trống |
| An toàn với | Đúng 1 producer, đúng 1 consumer | Nhiều producer/consumer (RTOS lo đồng bộ hoá) |
Verify: nhận dữ liệu (receive()) khi queue rỗng khiến task Blocked ngay; ngay khi
send() đẩy dữ liệu vào, task đang chờ Ready lập tức — cùng pattern "đánh thức đúng lúc" như
semaphore, nhưng mang theo DỮ LIỆU thay vì chỉ một tín hiệu.
5. Priority inversion & Mars Pathfinder 1997
Kịch bản chính xác đã xảy ra trên tàu Pathfinder, dựng lại với 3 task:
- task_L (ưu tiên THẤP nhất) — cần một tài nguyên chia sẻ (bus dữ liệu), khoá mutex bảo vệ nó trong lúc dùng.
- task_H (ưu tiên CAO nhất) — cũng cần đúng tài nguyên đó, xin khoá NGAY SAU khi task_L đã khoá → bị chặn (Blocked), chờ task_L nhả ra.
- task_M (ưu tiên TRUNG bình) — hoàn toàn KHÔNG liên quan tới mutex đó, nhưng có một việc dài (mô phỏng tác vụ truyền thông nặng của tàu thật).
Vấn đề: ngay khi task_M trở nên sẵn sàng, nó có ưu tiên CAO HƠN task_L (đang giữ mutex) nên preempt task_L — dù task_L chỉ đang giữ một tài nguyên mà task_H cần, KHÔNG liên quan gì tới task_M. Task_L không được chạy tiếp (bị task_M chiếm CPU), nên không bao giờ nhả được mutex, nên task_H — dù có ưu tiên CAO NHẤT hệ thống — vẫn kẹt cứng chờ đợi.
Lời giải: priority inheritance. Ngay khi task_H bị chặn bởi mutex của task_L, task_L được "MƯỢN" tạm độ ưu tiên CAO của task_H — đủ để đánh bại task_M khi nó xuất hiện. Task_L chạy hết phần việc còn lại (giờ với ưu tiên vay mượn), nhả mutex, TRẢ LẠI độ ưu tiên gốc của mình, và task_H lập tức được chạy. Verify chính xác: với cùng kịch bản (task_L cần 20 đơn vị việc, đã chạy 2 trước khi bị task_H chặn), bật priority inheritance khiến task_H chạy được đúng tại tick 20 — thay vì không bao giờ.
Một góc nhìn khác đáng nhớ: deadlock (bế tắc) là một dạng lỗi đồng bộ hoá khác — xảy ra khi 2 task khoá CHÉO nhau (A giữ khoá 1 và xin khoá 2, B giữ khoá 2 và xin khoá 1), cả hai kẹt cứng vĩnh viễn, không ai chịu nhường. Quy tắc phòng tránh kinh điển: LUÔN khoá theo một thứ tự CỐ ĐỊNH toàn hệ thống (vd luôn khoá theo alphabet tên mutex) — nếu mọi task tuân thủ, deadlock kiểu này không thể xảy ra.
6. Thực hành: dựng lại Mars Pathfinder
Demo dưới đây chạy đúng kịch bản lịch sử trên mini-RTOS thật (Bài 14): bật/tắt priority inheritance và xem task ưu tiên cao nhất có được "cứu" hay không:
1. Priority Inversion — 3 task: task_L (thấp, giữ mutex), task_M (trung, không liên quan mutex, việc dài), task_H (cao nhất, cần mutex)
2. Demo bonus: Deadlock 2 mutex khoá chéo
// Mo phong dung 3 task cua Mars Pathfinder that (RTOS nhung thuc te co san co che nay)
mutex_t bus_mutex; // tao voi priority_inheritance = true
void task_L_low_priority(void) {
mutex_lock(&bus_mutex); // giu bus - neu task_H dang cho, task_L MUON tam uu tien cao cua no
read_write_bus(); // viec can bao ve
mutex_unlock(&bus_mutex); // nha khoa, TRA LAI uu tien goc, trao khoa cho task dang cho uu tien cao nhat
}
void task_H_high_priority(void) {
mutex_lock(&bus_mutex); // neu task_L dang giu -> Blocked, NHUNG kich hoat "cho muon" uu tien
read_write_bus();
mutex_unlock(&bus_mutex);
}
void task_M_medium_priority(void) {
do_long_communication_task(); // KHONG dung bus_mutex - nhung van co the preempt task_L neu khong co inheritance
}
import { MiniRTOS } from './vmcu.js';
function runScenario(priorityInheritance) {
const rtos = new MiniRTOS();
rtos.addTask('task_L', 10, 20, 1000); // uu tien THAP, viec 20 don vi (giu mutex)
rtos.addTask('task_M', 5, 50, 1000, true); // uu tien TRUNG, bat dau BLOCKED, viec DAI 50 don vi
rtos.addTask('task_H', 0, 2, 1000, true); // uu tien CAO NHAT, bat dau BLOCKED
const mutex = rtos.createMutex('bus_mutex', priorityInheritance);
const [L, , H] = rtos.tasks;
rtos.mutexLock(mutex, L); // L lay mutex ngay tu dau
rtos.tick(); rtos.tick();
rtos.wakeTask('task_H');
rtos.mutexLock(mutex, H); // H xin mutex -> bi chan boi L
rtos.wakeTask('task_M'); // M cung san sang, KHONG lien quan mutex
rtos.run(58);
return rtos.timeline.filter(e => e.event === 'run');
}
// KHONG inheritance: task_H khong bao gio chay trong 60 tick
// CO inheritance: task_H chay dung tai tick 20
Tóm lược
- ✅ Preemption khiến race condition có thể xảy ra giữa BẤT KỲ 2 task nào — tắt ngắt (Bài 8) giờ quá thô bạo vì giết luôn cả lập lịch.
- ✅ Semaphore: bộ đếm + đánh thức, pattern ISR give — task blocked take, không cần poll. Mutex: thêm ownership + priority inheritance — pitfall dùng semaphore thay mutex mất cả hai. Queue: gửi dữ liệu thật, ring buffer Bài 8 + blocking + an toàn đa task.
- ✅ Priority inversion & Mars Pathfinder 1997 verified trên VMCU thật: KHÔNG inheritance — task ưu tiên cao nhất không chạy được lần nào trong 60 tick; BẬT inheritance — chạy đúng tại tick 20. Đúng cơ chế đã cứu con tàu thật ngoài không gian.
- ✅ Deadlock (khoá chéo) verified: cả 2 task kẹt vĩnh viễn, 0 lần chạy trong 30 tick — phòng tránh bằng cách luôn khoá theo thứ tự cố định.
Trắc nghiệm ôn tập
Câu 1
Khác biệt cốt lõi giữa mutex và semaphore là gì?
Câu 2
Verified: task_M — hoàn toàn KHÔNG tương tác trực tiếp với mutex mà task_H cần — vẫn khiến task_H (ưu tiên cao nhất) không chạy được lần nào trong 60 tick khi priority inheritance tắt. Vì sao một task không liên quan lại gây ra hậu quả này?
Câu 3
Priority inheritance sửa priority inversion bằng cơ chế nào?
Câu 4
Verified: 2 task khoá chéo (A giữ mutex X xin mutex Y, B giữ mutex Y xin mutex X) khiến cả hai kẹt vĩnh viễn (0 lần chạy trong 30 tick). Quy tắc phòng tránh kinh điển cho loại deadlock này là gì?
Tải file code thực hành minh họa bài học
File JavaScript VMCU — engine MCU ảo dùng xuyên suốt cả 16 bài, Bài 15 vừa thêm mutex (có
priority inheritance bật/tắt được), semaphore, và queue vào MiniRTOS, kèm self-test đối
chiếu đúng mọi hành vi trong bài, bao gồm dựng lại chính xác Mars Pathfinder 1997 (chạy
node vmcu.js, không cần cài thêm gì):
Bình luận