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.


📚 Điều kiện tiên quyết
Bắt buộc: Bài 14 (task, preemption, priority — nền tảng bắt buộc để hiểu priority inversion), Bài 8 (race condition, critical section — công cụ cũ cần thay thế).

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.

⚠️ Pitfall: dùng semaphore làm mutex
Semaphore KHÔNG có khái niệm sở hữu — về lý thuyết, task A có thể "give" một semaphore mà task B chưa từng "take", hoặc ngược lại, không có gì kiểm tra ai là chủ hợp lệ. Semaphore cũng KHÔNG hỗ trợ priority inheritance (Mục 5) — dùng nó thay cho mutex để bảo vệ tài nguyên dùng chung là mở toang cửa cho chính bug priority inversion mà Mục 5 sắp mổ xẻ.

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:

  1. 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.
  2. 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.
  3. 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.

⚠️ Verify: priority inversion không có giới hạn thời gian
Chạy mô phỏng 60 tick: task_H (ưu tiên cao nhất hệ thống) không chạy được một lần nào — hoàn toàn bị "khoá" gián tiếp bởi task_M, một task nó chưa từng tương tác trực tiếp. Trên tàu Pathfinder thật, hệ quả là một watchdog timer phát hiện task cao không "báo cáo còn sống" đúng hạn → tự động reset toàn hệ thống, lặp lại nhiều lần liên tiếp trước khi kỹ sư NASA xác định đúng nguyên nhân từ xa qua radio.

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:

🚀 Mars Pathfinder Recreation — VMCU thật

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)

Đang tải…

2. Demo bonus: Deadlock 2 mutex khoá chéo

Bấm nút để chạy thí nghiệm.
priority_inheritance_mutex.c
// 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
}
pathfinder_demo.js (đúng logic đang chạy ở tab Xem trước)
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ì):

Tải về vmcu.js

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 14: RTOS preemptive: context switch & task states Bài 16: Capstone: Trạm đo nhiệt độ hoàn chỉnh Quay lại Lộ trình Series Hệ Thống Nhúng

Bình luận