RTOS

Có bao nhiêu Core thì có thể chạy bấy nhiêu task song song cùng lúc.

Thường thì vi điều khiển có 1 Core, nên cần lập lịch (Scheduling) chạy các task.

Các thuật toán lập lịch

  • Round-Robin: Các task chạy trong thời gian Time-Slice như nhau, hết Time-Slice của task này thì chuyển sang task tiếp theo. Dùng bộ Timer (SysTick,...) để đếm Time Slice, sau đó sinh ra ngắt để gọi từng task 
  • Preemptive: Task có độ ưu tiên cao hơn chiếm CPU của task đang thực thi. Task đang thực thi chuyển sang trạng thái chờ, đợi task vừa chiếm CPU thực thi xong.
  • Cooperative: Task này chạy xong, mới đến task tiếp theo (giống như khi không có RTOS)

Các trạng thái của task:

  • Ready
  • Running
  • Blocked
  • Suspended

Các task được chạy theo thứ thự sau: (Thứ tự sắp xếp Task trong Queue)

  • Độ ưu tiên: Task có độ ưu tiên cao nhất được cho lên đầu Queue. Task có độ ưu tiên cao hơn xếp trên task có độ ưu tiên thấp hơn.
  • Task đến trước, được thực hiện trước (Các task đang chờ sẽ ở trong Ready queue)

Kernel gọi hàm lập lịch Scheduler để chuyển sang task đứng đầu Queue. Các trường hợp gọi hàm lập lịch - Scheduler: 

  • Task muốn thực hiện có độ ưu tiên cao hơn task đang thực thi. Nói cách khác, trong Queue có task đang xếp trên task đang thực thi. kernel gọi hàm lập lịch Scheduler() để chuyển sang Task có độ ưu tiên cao hơn.
  • taskYIELD(): Task tự nhường CPU
  • vTaskDelay(): Khi bắt đầu delay thì gọi hàm lập lịch để chuyển sang Task khác. Khi hết thời gian delay thì quay trở lại Task cũ.
  • Khi hết Time-Slice, kernel chuyển sang Task khác bằng cách gọi Scheduler.
  • Khi 1 ngắt xảy ra và gọi portYIELD_FROM_ISR(), OS cũng gọi Scheduler
  • Sự kiện định kỳ từ Timer khi đếm hết timeout.
  • Task bị block do chờ tài nguyên như chờ semaphore, mutex, queue

semaphore: biểu thị trạng thái của tài nguyên (Khi 2 task dùng chung tài nguyên - task này dùng tài nguyên này xong thì task khác mới được dùng tài nguyên này)

  • để dùng tài nguyên, tác vụ cần yêu cầu sử dụng tài nguyên - acquire semaphore
  • Nếu tài nguyên sẵn sàng (không có task nào dùng tài nguyên), task yêu cầu có thể dùng tài nguyên đó. Các task khác thấy tài nguyên đang được dùng nên sẽ phải chờ.
  • Sau khi dùng tài nguyên xong, task này cần trả lại tài nguyên - release semaphore để task khác dùng
  • Task có ưu tiên cao hơn có thể giải phóng Binary Semaphore và chiếm quyền sử dụng tài nguyên
  • Binary semaphore có thể đồng thời được sở hữu bởi nhiều thread

mutex: chìa khóa tài nguyên, Chỉ 1 task/ngắt được phép truy cập tài nguyên tại 1 thời điểm

  • Acquire: 1 task phải đợi đến khi mutex free để được cấp quyền truy cập
  • Release: Sau khi hoàn thành, task nhả Mutex để tài nguyên có thể được dùng bởi task khác
  • Khi 1 task giữ mutex, các task khác không thể xen vào. Chỉ đến khi mutex free (task nhả mutex) thì task khác mới thực thi được (Cho dù là task có ưu tiên cao hơn)
  • Mutex chỉ được sở hữu bởi 1 thread

Event Flags: Chỉ ra 1/vài sự kiện đã xảy ra, Kiểm soát trình tự thực hiện giữa các task

Khi 1 task truyền dữ liệu sang task khác, nó sẽ đặt dữ liệu đó vào queue, và ID của task đích

Khắc phục Priority Inversion

a. Priority Inheritance: 

  • Giả sử có 3 task: Task A (độ ưu tiên cao), Task B (độ ưu tiên trung bình) và Task C (độ ưu tiên thấp). Task C đang giữ mutex
  • Giải pháp: Khi Task A đang yêu cầu Mutex. đưa độ ưu tiên của Task C bằng với task A, tránh để task B thức dậy và làm trì hoãn việc hoàn thành task C, trong khi task A độ ưu tiên cao đang cần được thực thi trước (chỉ cần task B không được thực thi là được)
  • Nhược điểm: Không chặn được deadblock chéo (2 luồng giữ khóa của nhau), có thể bị chặn dây chuyền (luồng cao phải chờ nhiều luồng thấp lần lượt nhả khóa khác nhau)

b. Priority Ceiling

  • Mỗi tài nguyên được gán một độ ưu tiên trần - là độ ưu tiên cao nhất của task dùng tài nguyên đó. Khi task khóa tài nguyên, độ ưu tiên của task đó được nâng lên mức trần. Khi task nhả khóa, task đó bị trả về độ ưu tiên thấp cũ.
  • Ưu điểm: Ngăn chặn deadblock (do thiết kế trần ưu tiên không cho luồng khác xen vào). luồng mức cao bị chặn tối đa 1 lần.
  • Nhược điểm: Có thể nâng ưu tiên thừa thãi (dù chẳng có ai chờ), làm gián đoạn vô lý các luồng tầm trung.

c. Thu gọn Critical Section

Critical Section chỉ được tính từ lúc Task gọi Lock() cho đến khi gọi Unlock(). Rút ngắn Critical Section nghĩa là: Chỉ giữ khóa ở đúng những dòng code thao tác trực tiếp trên tài nguyên dùng chung, đẩy mọi thao tác khác ra bên ngoài.

Ví dụ không tối ưu Critical Section:

  1. Mutex_Lock() -> Bắt đầu Critical Section

  2. Lấy dữ liệu dùng chung.

  3. Thực hiện tính toán ma trận phức tạp (mất $200\text{ ms}$).

  4. In dữ liệu ra màn hình (thao tác I/O mất $50\text{ ms}$).

  5. Cập nhật lại dữ liệu dùng chung.

  6. Mutex_Unlock() -> Kết thúc Critical Section (Tổng thời gian giữ khóa: 250 ms)

Tất cả các Task khác bị đóng băng 250 ms chỉ vì Task này ôm khóa trong lúc tự tính toán và tự in ấn — những việc không liên quan đến tài nguyên chung.

  • Thiết kế chuẩn (Tối ưu thu nhỏ Critical Section):

    1. Mutex_Lock() -> Bắt đầu Critical Section

    2. Copy dữ liệu chung ra một biến cục bộ (Local variable).

    3. Mutex_Unlock() -> Kết thúc Critical Section (Chỉ mất 0.001 ms)

    4. Tự do tính toán ma trận với biến cục bộ (200 ms)

    5. Tự do in ấn ra màn hình (50 ms).

    6. Mutex_Lock() -> Bắt đầu Critical Section lần 2

    7. Ghi kết quả từ biến cục bộ vào tài nguyên dùng chung.

    8. Mutex_Unlock() -> Kết thúc Critical Section (Chỉ mất 0.001 ms).

Thời gian chạy của toàn bộ Task vẫn là hơn 250 ms, nhưng tổng thời gian Critical Section (thời gian chiếm khóa) đã giảm từ 250 ms xuống còn 0.002 ms. Các Task khác hoàn toàn có thể nhảy vào lấy tài nguyên trong suốt khoảng thời gian 250 ms mà Task trên đang bận tính toán.

Nhanvat

Ủng hộ chúng tôi

Nếu bạn thấy nội dung hữu ích, hãy ủng hộ chúng tôi một ly cà phê nhé

Buy Me A Coffee buymeacoffee.com/invencamp