Lập trình hệ thống real-time bằng Ada — thực hành ưu tiên, chu kỳ và kiểm soát thời gian thực thi

· Cập nhật ngày: · · Ada, RealTime, Ravenscar, CeilingLocking, Tasking, Scheduling, PriorityInversion, ProgrammingLanguage, Thời gian thực, Độ tin cậy cao

1. Giới thiệu — mối liên hệ sâu giữa Ada và real-time

Bài trước, “Xử lý đồng thời an toàn trong Ada”, đã trình bày nền tảng xử lý đồng thời an toàn bằng task và protected object của Ada. Lần này ta đi tiếp trên cùng đường đó, vào một vùng ràng buộc chặt hơn — hệ thống real-time.

Trong hệ thống real-time, “đúng” không chỉ là kết quả tính toán logic đúng, mà còn gồm kết quả đó phải có trong hạn. Một đáp án đúng trễ 1 millisecond cũng nguy hiểm gần như một đáp án sai.

Ada đáp ứng yêu cầu này bằng một bộ tính năng real-time toàn diện, được chuẩn hóa thành Annex D (Real-Time Systems) của đặc tả ngôn ngữ. Đây không phải “gắn thêm bằng thư viện”, mà là đảm bảo real-time được gắn sẵn trong chính runtime của ngôn ngữ.

Tính năng real-time của Ada (Annex D):
- Ưu tiên task và preemption (FIFO_Within_Priorities)
- Giao thức Ceiling_Locking (ngăn priority inversion)
- Thực thi theo chu kỳ neo thời điểm tuyệt đối bằng delay until
- Ravenscar profile (tập con tasking cho an toàn tới hạn)
- Timing event (thức dậy theo thời điểm, không cần polling)
- Giám sát thời gian thực thi (Ada.Execution_Time)
- Lập lịch đa chu kỳ

Bài viết này giải thích lần lượt các tính năng đó qua 8 ví dụ mã thực hành. Mỗi snippet có thể xem như ví dụ độc lập, nhưng các ví dụ 04/05 chứa nhiều đơn vị biên dịch nên hãy tách bằng gnatchop rồi mới gnatmake.

Đối tượng đọc và kiến thức nền: Bài viết giả định bạn đã biết nền tảng task, rendezvous và protected object ở bài trước. Nó dành cho lập trình viên quan tâm phần mềm điều khiển trên thiết bị nhúng và hệ thống độ tin cậy cao; không phải nhập môn cú pháp Ada.

Môi trường kiểm chứng: 8 ví dụ trong bài đã được xác nhận biên dịch được với GNAT 13.3.0 (Ubuntu 24.04, x86-64). Output các ví dụ có kết quả chạy trong bài (chương 3 và chương 5) cũng lấy từ cùng môi trường. Cách ưu tiên và preemption thực sự có hiệu lực phụ thuộc OS và runtime GNAT (chương 12), nên chi tiết output có thể đổi theo môi trường.

Các đoạn mã trong bài cũng được công bố trên GitHub như bộ mã tham chiếu, sắp theo từng chương.

ada-real-time-systems - komurasoft-blog-samples (GitHub)

Bản đồ tri thức của bài viết này

Annex D của đặc tả ngôn ngữ Ada gắn sẵn vào chính runtime của ngôn ngữ cả FIFO_Within_Priorities dựa trên ưu tiên task, giao thức Ceiling_Locking tự động gán ceiling priority cho protected object, thực thi theo chu kỳ bằng delay until để ngăn cumulative drift, Ravenscar profile dễ phân tích tĩnh, timing event không cần polling, và đo thời gian thực thi theo từng task. Ceiling_Locking ngăn priority inversion từng thực sự xảy ra trên Mars Pathfinder năm 1997 và khiến tàu thám hiểm reset lặp lại, bằng cách tự động nâng task vào protected object lên ceiling priority. Ravenscar profile cấm delay tương đối, câu select và một số cấu trúc khác để thu hẹp task Ada thành tập con dễ phân tích timing tĩnh; trên GNAT, bật bằng cách ghi pragma trong tệp gnat.adc. Tuy nhiên, ánh xạ ưu tiên thực tế phụ thuộc OS và runtime GNAT.

Bản đồ tri thức lập trình hệ thống real-time của AdaSơ đồ cho thấy Annex D của Ada gắn dispatch theo ưu tiên, Ceiling_Locking, delay until, Ravenscar profile, timing event và đo thời gian thực thi lên task cùng protected object thế nào, và Ceiling_Locking ngăn priority inversion kiểu sự cố Mars Pathfinder ra saosử dụngsử dụngsử dụngsử dụngsử dụngsử dụngyêu cầuyêu cầuyêu cầungăn chặncó thể gâyyêu cầuyêu cầuyêu cầucấu hình bằngcó thể gâyngăn chặnkhông khuyến nghịsử dụngsử dụngyêu cầuAnnex D (chuẩn hệ thống thời gian thực của Ada)Giao thức Ceiling_LockingFIFO_Within_Prioritiesdelay until (trì hoãn theo thời điểm tuyệt đối)Hồ sơ RavenscarTiming event (Ada.Real_Time.Timing_Events)Ada.Execution_Time (đo thời gian thực thi)Task của Ada (xử lý đồng thời)Đối tượng được bảo vệ (protected object)GNATĐảo ngược độ ưu tiên (priority inversion)Sự cố đảo ngược độ ưu tiên của Mars Pathfinderdelay tương đối (câu lệnh delay)Drift tích lũy khi chạy chu kỳ

Trong sơ đồ, đường liền nét biểu thị quan hệ luôn đúng và đường nét đứt biểu thị quan hệ có điều kiện (điều kiện nằm trong phần giải thích từng quan hệ trên trang chi tiết). Danh sách đầy đủ các quan hệ (tổng 21, kèm bằng chứng và mức chắc chắn) cùng định nghĩa các khái niệm chính được tập hợp tại trang chi tiết bản đồ tri thức (bằng tiếng Nhật). Dữ liệu: JSON-LD / Turtle

2. Hệ thống real-time là gì

Trước hết, hãy thống nhất thuật ngữ.

Khái niệm Mô tả
Hard real-time Vượt deadline nghĩa là thất bại chí mạng của hệ thống (điều khiển bay, túi khí, máy tạo nhịp)
Soft real-time Vượt deadline không mong muốn, nhưng vượt thỉnh thoảng vẫn chấp nhận được (streaming video, game)
Deadline Thời điểm tuyệt đối mà task phải hoàn thành
Period (chu kỳ) Khoảng thời gian task được kích hoạt lặp lại
WCET (Worst-Case Execution Time) Thời gian thực thi xấu nhất của task
Jitter Độ biến thiên của thực thi theo chu kỳ
Schedulability Tính chất “tập task đó có chạy được mà mọi hạn đều được giữ”. Việc kiểm trên giấy là phân tích schedulability (phân tích thời gian đáp ứng, v.v.)
Ravenscar profile (Ravenscar) Quy ước thu hẹp tasking Ada thành tập con dễ phân tích tĩnh. Tên lấy từ làng Ravenscar ở Anh, nơi hội nghị soạn thảo được tổ chức (chương 6)

Khi thiết kế hệ thống real-time, điều kiện cần quan trọng với mỗi task là “WCET <= deadline”. Nhưng chỉ thế chưa bảo đảm cả hệ thống đạt hạn. Phân tích thời gian đáp ứng — gồm thời gian bị block, gán ưu tiên, jitter, ngắt, và hành vi của runtime cùng OS — vẫn cần thêm. Thực tế thường nhắm WCET < deadline để còn dư địa. Tính năng real-time của Ada cung cấp ở cấp ngôn ngữ một mô hình thực thi dự đoán được, giúp làm phân tích đó dễ hơn.

Phân loại hệ thống real-time và điều kiện schedulabilityHard real-time, soft real-time cùng deadline, period, WCET, jitter và các cơ chế Annex D được đưa vào phân tích schedulability.Yêu cầu real-timeKhông đạt hạn = thất bại chí mạngVượt thỉnh thoảng chấp nhận đượcDeadlineThời điểm tuyệt đối phải xongPeriodKhoảng lặpWCETThời gian thực thi xấu nhấtJitterĐộ biến thiên chu kỳHard real-timeĐiều khiển bayTúi khíMáy tạo nhịpSoft real-timePhát videoGameUIAda Annex DCơ chế bảo đảm tính dự đoán đượcFIFO_Within_PrioritiesCeiling_Lockingdelay untilPhân tích schedulabilityĐiều kiện cần: WCET &lt;= deadlineĐủ hay không xác nhận bằng phân tích thời gian đáp ứng

Một trong những hiện tượng nguy hiểm nhất trong hệ thống real-time là priority inversion. Vấn đề này thực sự xảy ra trên Mars Pathfinder năm 1997 và khiến tàu thám hiểm reset lặp lại.

Priority inversionTask ưu tiên thấp giữ lock bị preempt bởi task ưu tiên trung bình, khiến task ưu tiên cao bị block vô hạn.Tài nguyên dùng chungTask ưu tiên trung bìnhTask ưu tiên caoTask ưu tiên thấpSchedulerTài nguyên dùng chungTask ưu tiên trung bìnhTask ưu tiên caoTask ưu tiên thấpSchedulerĐang chạy trong critical sectionH thức dậy nên scheduler tạm dừng LBlock vì chờ lock! (L đang giữ)H đang chờ lock nên L được chạy tiếpTiếp tục hướng tới nhả lock...M thức dậy nên scheduler tạm dừng LL không nhả được lockM tiếp tục chạy (cả H lẫn L đều không tiến được)【Priority inversion】Ưu tiên cao bị block vô hạnChiếm lockThử chiếm lock

Task ưu tiên thấp đang giữ lock bị preempt bởi task ưu tiên trung bình, và task ưu tiên cao bị block vô hạn. Biện pháp thực tế trên Mars Pathfinder là bật priority inheritance của VxWorks; Ada thì xử lý cùng lớp vấn đề bằng một cách khác, Ceiling_Locking, cung cấp như tính năng ngôn ngữ.

3. Cơ bản về ưu tiên task — FIFO_Within_Priorities

FIFO_Within_Priorities là chính sách dispatch theo ưu tiên chuẩn mà Ada Annex D cho phép chỉ định. Nếu không nêu chính sách, hành vi mặc định do implementation định nghĩa; trên GNAT, nhiều target dùng họ chính sách này. Trong cùng một mức ưu tiên, task chạy FIFO (vào trước ra trước), và task ưu tiên cao hơn preempt (cướp CPU của) task ưu tiên thấp hơn.

-- 01_task_priority.ada
-- Dạng cơ bản của ưu tiên task và FIFO_Within_Priorities
-- Configuration pragma phải đứng trước các context clause

pragma Task_Dispatching_Policy (FIFO_Within_Priorities);

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

procedure Task_Priority_Demo is

   task High_Priority_Task is
      pragma Priority (Priority'Last);
      pragma Storage_Size (4 * 1024);
   end High_Priority_Task;

   task Low_Priority_Task is
      pragma Priority (Priority'First);
      pragma Storage_Size (4 * 1024);
   end Low_Priority_Task;

   task body High_Priority_Task is
   begin
      Put_Line ("[T=0.0s] High priority task started");
      delay until Clock + Milliseconds (100);
      Put_Line ("[T=0.1s] High priority task completed");
   end High_Priority_Task;

   task body Low_Priority_Task is
   begin
      Put_Line ("[T=0.0s] Low priority task started");
      delay until Clock + Milliseconds (500);
      Put_Line ("[T=0.5s] Low priority task completed");
   end Low_Priority_Task;

begin
   Put_Line ("=== Task Priority Demo (FIFO_Within_Priorities) ===");
   Put_Line ("Main: waiting for tasks to complete...");
   delay until Clock + Milliseconds (800);
   Put_Line ("Main: done");
end Task_Priority_Demo;

Điểm cần nắm:

  • pragma Priority gán ưu tiên tĩnh cho từng task. Priority'Last là cao nhất, Priority'First là thấp nhất.
  • Thân task trong demo này không phải tính toán nặng, mà chờ tới thời điểm chỉ định bằng delay until. Điều cần xác nhận là, khi cả hai cùng runnable, task ưu tiên cao được cơ hội chạy trước.
  • Hình này cho phía preemption giữa các mức ưu tiên khác nhau của FIFO_Within_Priorities. Để xác nhận thứ tự FIFO trong cùng một mức, cần ví dụ khác với nhiều task cùng ưu tiên.
  • Trong hệ thống thực tế, cách phổ biến là thiết kế ưu tiên tương đối lấy System.Default_Priority làm mốc.
FIFO_Within_Priorities giữa hai mức ưu tiênScheduler chọn task ưu tiên cao trước; sau delay until, thứ tự hoàn thành theo thời điểm thức dậy.Task ưu tiên thấp(Priority=First)Task ưu tiên cao(Priority=Last)Task mainSchedulerTask ưu tiên thấp(Priority=First)Task ưu tiên cao(Priority=Last)Task mainSchedulerT=0ms: Cả hai task đều runnableIn log bắt đầuIn log bắt đầuT=100ms: HP thức dậyIn log hoàn thànhT=500ms: LP thức dậyIn log hoàn thành(T=800ms) Main kết thúcTạo taskTạo taskChọn HP, ưu tiên cao nhấtBlock bằng delay until T+100msChạy LP tiếpBlock bằng delay until T+500msChạy HPChạy LP

Ví dụ chạy (GNAT 13.3.0 / Ubuntu 24.04, x86-64):

$ gnatchop -w 01_task_priority.ada .     # → task_priority_demo.adb
$ gnatmake task_priority_demo.adb
$ ./task_priority_demo
[T=0.0s] High priority task started
[T=0.0s] Low priority task started
=== Task Priority Demo (FIFO_Within_Priorities) ===
Main: waiting for tasks to complete...
[T=0.1s] High priority task completed
[T=0.5s] Low priority task completed
Main: done

Chú ý hai dòng log bắt đầu của hai task xuất hiện trước dòng tiêu đề của main. Task khai báo ở declarative part được kích hoạt trước khi vào thân subprogram bao quanh, nên thứ tự kiểu này có thể xảy ra. Thêm nữa, thứ tự hai dòng log bắt đầu đó có thể đảo mỗi lần chạy. Như chương 12, pragma Priority phản ánh vào lập lịch thực tế thế nào tùy OS và runtime GNAT; trên Linux thông dụng, thứ tự khởi động đúng theo ưu tiên không được bảo đảm. Điều ví dụ này quan sát ổn định được là thứ tự hoàn thành 0,1 giây / 0,5 giây đã chỉ định bằng delay until.

Phạm vi ưu tiên Ada (mặc định GNAT):
  Priority'First  = 0   (thấp nhất)
  Priority'Last   = 30  (cao nhất, nhưng phụ thuộc OS)

4. Ceiling_Locking — ngôn ngữ ngăn priority inversion

Một trong những vấn đề khó chịu nhất trong hệ thống real-time là priority inversion. Task ưu tiên cao chờ lock mà task ưu tiên thấp đang giữ, rồi task ưu tiên thấp bị preempt bởi task ưu tiên trung bình, khiến task ưu tiên cao bị block vô hạn.

Ada gắn giao thức Ceiling_Locking trực tiếp vào protected object để xử lý vấn đề này.

-- 02_ceiling_locking.ada
-- Ngăn priority inversion bằng giao thức Ceiling_Locking
-- Configuration pragma phải đứng trước các context clause

pragma Locking_Policy (Ceiling_Locking);

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

procedure Ceiling_Locking_Demo is

   Ceiling : constant System.Any_Priority := System.Any_Priority'Last;

   protected Shared_Data is
      pragma Priority (Ceiling);
      procedure Write (V : Integer);
      function Read return Integer;
   private
      Value : Integer := 0;
   end Shared_Data;

   protected body Shared_Data is
      procedure Write (V : Integer) is
      begin
         Value := V;
      end Write;

      function Read return Integer is
      begin
         return Value;
      end Read;
   end Shared_Data;

   task Producer is
      pragma Priority (Priority'Last);
      pragma Storage_Size (4 * 1024);
   end Producer;

   task Consumer is
      pragma Priority (Priority'First);
      pragma Storage_Size (4 * 1024);
   end Consumer;

   task body Producer is
   begin
      Put_Line ("[T=0.0s] Producer (high prio): about to write");
      Shared_Data.Write (42);
      Put_Line ("[T=0.0s] Producer (high prio): write done");
      delay until Clock + Milliseconds (100);
   end Producer;

   task body Consumer is
   begin
      delay until Clock + Milliseconds (10);
      Put_Line ("[T=0.01s] Consumer (low prio): about to read");
      declare
         V : Integer;
      begin
         V := Shared_Data.Read;
         Put_Line ("[T=0.01s] Consumer (low prio): read done, got" &
                     Integer'Image (V));
      end;
      delay until Clock + Milliseconds (100);
   end Consumer;

begin
   Put_Line ("=== Ceiling_Locking Demo ===");
   Put_Line ("Main: producer priority = Last, consumer priority = First");
   Put_Line ("Ceiling = Any_Priority'Last, locking = Ceiling_Locking");
   delay until Clock + Milliseconds (300);
   Put_Line ("Main: done");
end Ceiling_Locking_Demo;

Cách Ceiling_Locking hoạt động:

  1. Gán ceiling priority cho protected object bằng pragma Priority (Ceiling).
  2. Task nào vào protected object cũng tự động được nâng lên ceiling priority lúc vào.
  3. Nhờ đó, task ưu tiên trung bình không preempt được task đang dùng protected object.
  4. Khi ra khỏi protected object, ưu tiên trở về mức ban đầu.

Hình dưới không phải vết thời gian sát từng mili giây của mã mẫu ngay trên, mà là hình khái niệm cho thấy mẫu priority inversion ở hình 2 bị Ceiling_Locking kìm thế nào.

Ceiling_Locking ngăn priority inversionTask vào protected object được nâng lên ceiling priority nên task ưu tiên trung bình không preempt được.Protected object(ceiling priority=30)Task ưu tiên cao(ưu tiên=30)Task ưu tiên trung bình(ưu tiên=20)Task ưu tiên thấp(ưu tiên=10)SchedulerProtected object(ceiling priority=30)Task ưu tiên cao(ưu tiên=30)Task ưu tiên trung bình(ưu tiên=20)Task ưu tiên thấp(ưu tiên=10)SchedulerCaller có active priority > ceiling priority thì Program_ErrorH(30) bằng ceiling(30) nên được vàoƯu tiên thực thi được nâng lên 30M thức dậyL đang chạy ở ceiling priority 30M(20) không preempt đượcH thức dậyH(30) vượt qua kiểm tra ceilingnhưng chờ vì L đang dùng POƯu tiên trở về 10Sau khi PO được nhả thì chạy HH(30) = ceiling(30) nên vào được sau khi hết tranh chấpVào protected operationThực hiện operationRa khỏi protected operationVào protected operationRa khỏi protected operation

Nguyên tắc thiết kế: Ceiling priority của protected object phải được đặt ít nhất bằng ưu tiên cao nhất trong mọi task dùng object đó. Nếu phá quy tắc này và một task có active priority cao hơn ceiling gọi protected operation, Ada phát hiện lỗi thiết kế bằng Program_Error.

Để làm tương đương với mutex pthread của C, bạn phải chủ động đặt thuộc tính PTHREAD_PRIO_PROTECT. Trong Ada, đó là tính năng chuẩn của ngôn ngữ.

5. delay until — chạy task chu kỳ mà không drift

Mẫu cơ bản của hệ thống real-time là task chu kỳ. Với task chạy lặp lại theo khoảng cố định, ngăn sai số timing cộng dồn (drift) cực kỳ quan trọng.

delay until của Ada giải quyết vấn đề này gọn.

-- 03_periodic_task.ada
-- Task chu kỳ bằng delay until — ngăn cumulative drift

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

procedure Periodic_Task_Demo is

   Period_MS : constant Time_Span := Milliseconds (200);
   Cycles    : constant Positive  := 5;

   task Sensor_Reader is
      pragma Priority (Priority'Last - 2);
      pragma Storage_Size (4 * 1024);
   end Sensor_Reader;

   task body Sensor_Reader is
      Start_Time  : constant Time := Clock;
      Next_Release : Time := Start_Time + Period_MS;
      Cycle_Count  : Natural := 0;
   begin
      Put_Line ("[Sensor] Periodic task starts, period=" &
                To_Duration (Period_MS)'Image & "s, cycles=" &
                Natural'Image (Cycles));

      for I in 1 .. Cycles loop
         delay until Next_Release;

         Cycle_Count := Cycle_Count + 1;
         Put_Line ("[Sensor] Cycle" & Natural'Image (Cycle_Count) &
                   " at" & Duration'Image (To_Duration (Clock - Start_Time)) & "s");

         Next_Release := Next_Release + Period_MS;
      end loop;

      Put_Line ("[Sensor] Periodic task finished. Actual elapsed:" &
                Duration'Image (To_Duration (Clock - Start_Time)) & "s");
   end Sensor_Reader;

begin
   Put_Line ("=== Periodic Task Demo (delay until) ===");
   Put_Line ("Main: waiting for" & Natural'Image (Cycles) & " cycles...");
   delay until Clock + Milliseconds (1500);
   Put_Line ("Main: done");
end Periodic_Task_Demo;

Vì sao dùng delay until:

Cách Vấn đề
delay Period; Thời gian xử lý mỗi vòng được cộng thêm, chu kỳ lệch dần (cumulative drift)
delay until Next_Release; Next_Release := Next_Release + Period; Neo theo thời điểm tuyệt đối, nên dù một lần xử lý bị trễ thì thời điểm phát lần sau vẫn đúng

Tuy nhiên, delay until không tự bảo đảm thời gian xử lý nằm trong chu kỳ. Nếu xử lý đã vượt thời điểm thức dậy kế tiếp, delay until đó trả về gần như ngay, và hệ thống nên coi đó là trạng thái deadline miss.

Với delay:
  T=0ms → xử lý(15ms) → delay 100ms → T=115ms → xử lý(10ms) → ...
  Khoảng thực tế: 115ms, 110ms, ... (thời gian xử lý cộng dồn)

Với delay until:
  Next_Release: 100ms, 200ms, 300ms, ... (thời điểm tuyệt đối)
  T=0ms → xử lý(15ms) → delay until 100ms → T=100ms → xử lý(10ms) → delay until 200ms
  Khoảng thực tế: 100ms, 100ms, ... (không phụ thuộc thời gian xử lý)

Ví dụ chạy (GNAT 13.3.0 / Ubuntu 24.04, x86-64):

$ gnatchop -w 03_periodic_task.ada .     # → periodic_task_demo.adb
$ gnatmake periodic_task_demo.adb
$ ./periodic_task_demo
[Sensor] Periodic task starts, period= 0.200000000s, cycles= 5
=== Periodic Task Demo (delay until) ===
Main: waiting for 5 cycles...
[Sensor] Cycle 1 at 0.200326876s
[Sensor] Cycle 2 at 0.400159550s
[Sensor] Cycle 3 at 0.600183089s
[Sensor] Cycle 4 at 0.800327451s
[Sensor] Cycle 5 at 1.000260472s
[Sensor] Periodic task finished. Actual elapsed: 1.000297359s
Main: done

Mỗi chu kỳ có trễ thức dậy khoảng 0,2–0,3 millisecond, nhưng trễ đó không được mang sang chu kỳ sau. Ở chu kỳ thứ 5, lệch so với mốc gốc vẫn dưới 1 millisecond. Nếu viết bằng delay Period;, trễ này cộng mỗi vòng và sau 5 chu kỳ sẽ thành chênh thấy được. Các số này chỉ là một ví dụ trên Linux thông dụng, không phải giá trị được bảo đảm trong môi trường hard real-time.

Mẫu delay until này được dùng cho mọi task chu kỳ từ đây trở đi.

delay Period so với delay untildelay tương đối tích lũy drift; delay until neo theo thời điểm tuyệt đối; nếu vượt chu kỳ thì delay until trả về ngay.Vượt chu kỳ - deadline misstính 130msLần sau = T+100msdelay until T+100ms trả về ngayPhát hiện trễ và xử lý như quá tảidelay until - neo thời điểm tuyệt đốitính 15msLần sau = T+100msdelay until T+100ms → thức dậy lúc 100mstính 10msLần sau = T+200ms → thức dậy lúc 200msKhoảng thực tế: 100ms, 100ms...delay Period - cumulative driftdelay 100ms → thức dậy lúc 115msT=0ms: tính 15mstính 10ms → 125msdelay 100ms → thức dậy lúc 225msKhoảng thực tế: 115ms, 110ms...Sai số cộng dồn theo thời gianNgăn cumulative drift

6. Ravenscar profile — tập con real-time có thể kiểm chứng

Tasking của Ada mạnh, nhưng trong hệ thống an toàn cực kỳ quan trọng, “mạnh quá” lại thành vấn đề. Tạo task động, câu select, câu abort làm phân tích tĩnh thời gian thực thi xấu nhất trở nên khó.

Ravenscar profile là câu trả lời của Ada: nó thu hẹp tasking thành tập con tất định, có thể phân tích tĩnh.

-- 04_ravenscar_profile.ada
-- Dạng cơ bản của Ravenscar profile
-- Lúc biên dịch, chỉ định pragma Profile (Ravenscar); trong gnat.adc
-- Build: gnatchop -w 04_ravenscar_profile.ada .
--         → tách thành ravenscar_state.ads / ravenscar_state.adb / ravenscar_demo.adb
--         chuẩn bị gnat.adc rồi gnatmake ravenscar_demo.adb

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

package Ravenscar_State is

   protected Signal is
      pragma Priority (System.Default_Priority + 5);
      entry Wait_For_Release;
      procedure Release;
   private
      Released : Boolean := False;
   end Signal;

   task Periodic_Worker is
      pragma Priority (System.Default_Priority + 1);
      pragma Storage_Size (4 * 1024);
   end Periodic_Worker;

   task Monitor is
      pragma Priority (System.Default_Priority);
      pragma Storage_Size (4 * 1024);
   end Monitor;

end Ravenscar_State;

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

package body Ravenscar_State is

   protected body Signal is
      entry Wait_For_Release when Released is
      begin
         Released := False;
      end Wait_For_Release;

      procedure Release is
      begin
         Released := True;
      end Release;
   end Signal;

   task body Periodic_Worker is
      Start_Time   : constant Time := Clock;
      Next_Release : Time := Start_Time + Milliseconds (100);
      Period       : constant Time_Span := Milliseconds (100);
      Cycle_Count  : Natural := 0;
   begin
      Put_Line ("[Worker] Ravenscar periodic task starts");

      for I in 1 .. 4 loop
         delay until Next_Release;

         Cycle_Count := Cycle_Count + 1;
         Put_Line ("[Worker] Cycle" & Natural'Image (Cycle_Count) &
                   " at" & Duration'Image (To_Duration (Clock - Start_Time)) & "s");
         Signal.Release;
         Next_Release := Next_Release + Period;
      end loop;

       Put_Line ("[Worker] Finished demo, waiting (Ravenscar: No_Task_Termination)");
      loop
         delay until Clock + Seconds (1);
      end loop;
   end Periodic_Worker;

   task body Monitor is
   begin
      Put_Line ("[Monitor] Waiting for signals...");

      for I in 1 .. 4 loop
         Signal.Wait_For_Release;
         Put_Line ("[Monitor] Received signal" & Natural'Image (I));
      end loop;

      Put_Line ("[Monitor] All signals received, waiting (Ravenscar: No_Task_Termination)");
      loop
         delay until Clock + Seconds (1);
      end loop;
   end Monitor;

end Ravenscar_State;

with Ravenscar_State; use Ravenscar_State;
with Ada.Text_IO;     use Ada.Text_IO;
with Ada.Real_Time;   use Ada.Real_Time;

procedure Ravenscar_Demo is
begin
   Put_Line ("=== Ravenscar Profile Demo ===");
   Put_Line ("(compile with: gnatmake -gnatec=gnat.adc ravenscar_demo)");
   Put_Line ("Main: waiting for Ravenscar tasks...");
   delay until Clock + Milliseconds (800);
   Put_Line ("Main: demo window elapsed; waiting forever (Ravenscar: No_Task_Termination)");
   loop
      delay until Clock + Seconds (1);
   end loop;
end Ravenscar_Demo;

Các hạn chế của Ravenscar profile:

Tính năng bị cấm Lý do
Tạo task động (new hoặc access type) Cấp phát bộ nhớ lúc chạy không tất định
Câu select Không chỉ nhiều nhánh thay thế; cả câu select làm phân tích luồng điều khiển khó hơn
Câu abort Ngắt bất đồng bộ làm trạng thái khó dự đoán
Ada.Task_Attributes Hành vi động lúc chạy
Đổi ưu tiên động Tiền đề phân tích lập lịch thay đổi lúc chạy
Delay tương đối (delay) Dễ sinh cumulative drift, nên dùng delay until theo thời điểm tuyệt đối
Nhiều entry trên một protected object Tăng điều kiện block và số trường hợp phải phân tích
Kết thúc task Ravenscar coi mọi task là không kết thúc
Câu requeue Theo dõi luồng điều khiển phức tạp hơn

Nhờ các hạn chế này, chương trình tuân Ravenscar dễ đưa vào dạng có thể phân tích timing tĩnh. Đó là đặc tính mà các tiêu chuẩn an toàn như DO-178C (phần mềm hàng không) hay ISO 26262 (an toàn chức năng ô tô) yêu cầu. Danh sách dưới là trích các hạn chế chính; profile thực tế còn gồm thêm quy tắc liên quan runtime và khả năng phân tích, chẳng hạn No_Task_HierarchyDetect_Blocking.

Các hạn chế của Ravenscar profileRavenscar thu hẹp tasking Ada thành tập con phân tích được, bắt buộc FIFO_Within_Priorities và Ceiling_Locking, phục vụ các tiêu chuẩn an toàn.Toàn bộ tasking AdaRavenscar profileHạn chếChính sách bắt buộcCấm tạo task độngCấm câu selectCấm câu abortCấm Task_AttributesGiới hạn 1 entry mỗi protected objectCấm câu requeueCấm delay tương đốidùng delay untilCấm đổi ưu tiên độngCấm kết thúc taskmọi task không kết thúcFIFO_Within_PrioritiesCeiling_LockingDễ làm hơn:phân tích timing tĩnhDO-178CPhần mềm hàng khôngISO 26262An toàn chức năng ô tôIEC 62304Phần mềm thiết bị y tế

Để bật Ravenscar profile, ghi nội dung sau vào tệp gnat.adc:

pragma Profile (Ravenscar);

7. Timing event — thức dậy theo thời điểm, không polling

Nhiều hệ thống real-time thường gặp yêu cầu “tới thời điểm chỉ định thì đánh thức task ưu tiên cao”. Cách làm mộc là polling timer, nhưng Ada cung cấp cơ chế tinh hơn — timing event.

-- 05_timing_events.ada
-- Timing event (Ada.Real_Time.Timing_Events)
-- Cơ chế đánh thức task ưu tiên cao mà không polling
-- Build: gnatchop -w 05_timing_events.ada .
--         → tách thành signal_pkg.ads / signal_pkg.adb / timing_events_demo.adb
--         gnatmake timing_events_demo.adb

pragma Locking_Policy (Ceiling_Locking);

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;

package Signal_Pkg is
   protected type Signal_Type is
      pragma Priority (System.Interrupt_Priority'Last);
      entry Wait_For_Event;
      procedure Fire (Event : in out Timing_Event);
   private
      Fired : Boolean := False;
   end Signal_Type;

   S : Signal_Type;
end Signal_Pkg;

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;

package body Signal_Pkg is
   protected body Signal_Type is
      entry Wait_For_Event when Fired is
      begin
         Fired := False;
      end Wait_For_Event;

      procedure Fire (Event : in out Timing_Event) is
      begin
         Fired := True;
      end Fire;
   end Signal_Type;
end Signal_Pkg;

with Signal_Pkg; use Signal_Pkg;

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;

procedure Timing_Events_Demo is

   pragma Priority (29);

   Timer_1 : Timing_Event;
   Timer_2 : Timing_Event;

   task Reactor is
      pragma Priority (System.Default_Priority + 5);
      pragma Storage_Size (4 * 1024);
   end Reactor;

   task body Reactor is
   begin
      Put_Line ("[Reactor] Waiting for timing events...");

      S.Wait_For_Event;
      Put_Line ("[Reactor] Got event #1");

      S.Wait_For_Event;
      Put_Line ("[Reactor] Got event #2");

      Put_Line ("[Reactor] Done");
   end Reactor;

begin
   Put_Line ("=== Timing Events Demo ===");
   Put_Line ("Scheduling two timers at +100ms and +250ms...");

   Set_Handler (Timer_1, Clock + Milliseconds (100), S.Fire'Access);
   Set_Handler (Timer_2, Clock + Milliseconds (250), S.Fire'Access);

   delay until Clock + Milliseconds (500);
   Put_Line ("Main: done");
end Timing_Events_Demo;

Cách timing event hoạt động:

1. Set_Handler(Timer_1, T+100ms, S.Fire'Access)  — đăng ký handler tại thời điểm tuyệt đối
2. T+100ms trôi qua — runtime gọi S.Fire ở **ceiling priority**
3. Fire đặt cờ Fired thành True — barrier mở
4. Task Reactor thức dậy từ Wait_For_Event

Điểm quan trọng là ví dụ này nêu rõ Ceiling_Locking, và vì handler Fire là procedure của protected object nên nó chạy ở ceiling priority. Protected procedure dùng làm handler timing event được đặt trong protected object có ceiling priority cấp interrupt; ở đây là System.Interrupt_Priority'Last. Nhờ đó, priority inversion không xảy ra trong lúc xử lý timing event.

8. Hàng đợi real-time bằng protected object

Một mẫu thường gặp trong hệ thống real-time là producer–consumer. Cảm biến sinh dữ liệu, task điều khiển tiêu thụ — lúc đó cần loại trừ tương hỗ trên bộ đệm và block hiệu quả.

Dùng protected objectentry barrier của Ada, có thể triển khai như đồng bộ dựa trên barrier. Runtime quản lý loại trừ tương hỗ bên trong, nên mã ứng dụng không cần tự viết mutex hay condition variable.

-- 06_protected_queue.ada
-- Chia sẻ dữ liệu real-time bằng protected object
-- Pipeline: Producer -> Bounded_Buffer -> Consumer
-- Lúc biên dịch, chỉ định pragma Locking_Policy (Ceiling_Locking); trong gnat.adc

pragma Locking_Policy (Ceiling_Locking);

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

procedure Protected_Queue_Demo is

   Buffer_Size : constant := 4;

   type Buf_Array is array (1 .. Buffer_Size) of Integer;

   protected Bounded_Buffer is
      pragma Priority (System.Any_Priority'Last);
      entry Put (Item : Integer);
      entry Get (Item : out Integer);
   private
      Buf    : Buf_Array;
      Count  : Natural := 0;
      Head   : Positive := 1;
      Tail   : Positive := 1;
   end Bounded_Buffer;

   protected body Bounded_Buffer is
      entry Put (Item : Integer) when Count < Buffer_Size is
      begin
         Buf (Tail) := Item;
         Tail := (Tail mod Buffer_Size) + 1;
         Count := Count + 1;
      end Put;

      entry Get (Item : out Integer) when Count > 0 is
      begin
         Item := Buf (Head);
         Head := (Head mod Buffer_Size) + 1;
         Count := Count - 1;
      end Get;
   end Bounded_Buffer;

   task Producer is
      pragma Priority (System.Default_Priority + 2);
      pragma Storage_Size (4 * 1024);
   end Producer;

   task Consumer is
      pragma Priority (System.Default_Priority + 1);
      pragma Storage_Size (4 * 1024);
   end Consumer;

   task body Producer is
      Next_Release : Time := Clock + Milliseconds (50);
      Period       : constant Time_Span := Milliseconds (50);
   begin
      for I in 1 .. 6 loop
         Bounded_Buffer.Put (I);
         Put_Line ("[Producer] Put" & Integer'Image (I));
         delay until Next_Release;
         Next_Release := Next_Release + Period;
      end loop;
      Put_Line ("[Producer] Done");
   end Producer;

   task body Consumer is
      Item         : Integer;
      Next_Release : Time := Clock + Milliseconds (80);
      Period       : constant Time_Span := Milliseconds (80);
   begin
      delay until Clock + Milliseconds (30);
      for I in 1 .. 6 loop
         Bounded_Buffer.Get (Item);
         Put_Line ("[Consumer] Got" & Integer'Image (Item));
         delay until Next_Release;
         Next_Release := Next_Release + Period;
      end loop;
      Put_Line ("[Consumer] Done");
   end Consumer;

begin
   Put_Line ("=== Protected Queue Demo (Ceiling_Locking) ===");
   Put_Line ("Buffer size = 4; Producer every 50ms, Consumer every 80ms");
   delay until Clock + Milliseconds (800);
   Put_Line ("Main: done");
end Protected_Queue_Demo;

Điểm thiết kế:

  • entry Put when Count < Buffer_Size — nếu bộ đệm đầy, Producer tự động block.
  • entry Get when Count > 0 — nếu bộ đệm rỗng, Consumer tự động block.
  • pragma Priority (System.Any_Priority'Last) — nhờ ceiling locking, priority inversion không xảy ra giữa Producer và Consumer.
  • Điều kiện barrier được định nghĩa bằng trạng thái nội bộ của protected object (Count) và được đánh giá lại tự động khi nhả lock.

Mã này không có mutex, semaphore hay condition variable phía ứng dụng. Chờ đợi cần thiết được diễn đạt bằng entry barrier của protected object.

Trạng thái hàng đợi bounded bufferProtected object chuyển Empty, Partial, Full; Get block khi rỗng và Put block khi đầy.Trạng thái ban đầuPut (thêm 1 phần tử)Put / GetGet (lấy phần tử cuối)Put (lấp chỗ trống cuối)Get (có chỗ trống)Get block (barrier Count=0)Put block (barrier Count=Buffer_Size)Rỗng / Count=0Có một phần / Count=1..Buffer_Size-1Đầy / Count=Buffer_Size

Khi Put thành công, barrier của bên chờ Get được đánh giá lại; khi Get thành công, barrier của bên chờ Put được đánh giá lại. Việc đó xảy ra lúc protected operation hoàn tất, bất kể hình đang ở trạng thái nào.

9. Đo thời gian thực thi — bước đầu giám sát execution time

Để đánh giá schedulability của hệ thống real-time, cần biết chính xác thời gian thực thi (CPU time) của từng task. Package Ada.Execution_Time của Ada cung cấp thời gian CPU tiêu thụ theo từng task.

-- 07_execution_time.ada
-- Kiểm soát thời gian thực thi (Execution_Time)
-- Đo thời gian CPU tiêu thụ theo từng task

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;
with Ada.Execution_Time;
use type Ada.Execution_Time.CPU_Time;

procedure Execution_Time_Demo is

   package ET renames Ada.Execution_Time;

   task Busy_Worker is
      pragma Priority (System.Default_Priority + 1);
      pragma Storage_Size (4 * 1024);
   end Busy_Worker;

   task body Busy_Worker is
      Wall_Start : Time;
      Cpu_Start  : ET.CPU_Time;
      Dummy      : Integer := 0;
      pragma Volatile (Dummy);
   begin
      Wall_Start := Clock;
      Cpu_Start := ET.Clock;

      Put_Line ("[Worker] Starting compute-bound work...");
      for I in 1 .. 20_000_000 loop
         Dummy := Dummy + 1;
      end loop;
      Put_Line ("[Worker] Dummy =" & Integer'Image (Dummy));

      declare
         Wall_Elapsed : constant Duration :=
            To_Duration (Clock - Wall_Start);
         Cpu_Span     : constant Time_Span :=
            ET.Clock - Cpu_Start;
      begin
         Put_Line ("[Worker] Done, wall time:" &
                   Duration'Image (Wall_Elapsed) & "s");
         Put_Line ("[Worker] CPU time consumed:" &
                   Duration'Image (To_Duration (Cpu_Span)) & "s");
      end;
   end Busy_Worker;

   Cpu_Start_Main : constant ET.CPU_Time := ET.Clock;

begin
   Put_Line ("=== Execution Time Demo ===");

   delay until Clock + Milliseconds (500);

   declare
      Cpu_Span : constant Time_Span := ET.Clock - Cpu_Start_Main;
   begin
      Put_Line ("Main: CPU time consumed after 500ms:" &
                Duration'Image (To_Duration (Cpu_Span)) & "s");
   end;

   Put_Line ("Main: done");
end Execution_Time_Demo;

Wall-clock time so với CPU time:

Wall-clock time: Ada.Real_Time.Clock
  → Thời gian trôi thực tế. Gồm cả lúc block và lúc bị preempt.

CPU time (execution time): Ada.Execution_Time.Clock
  → Chỉ thời gian task thực sự chạy trên CPU.
  → Lúc block hay bị preempt không được đếm.

Sự phân biệt này là điểm xuất phát cho giám sát thời gian thực thi lúc chạy và cho kiểm chứng WCET. Trong lúc Busy_Worker chờ bằng delay until, CPU time không tăng; nó chỉ tăng khi đang tính toán thật. Trong delay until Clock + Milliseconds(500) của task main, CPU time cũng gần như phải bằng không. Tuy nhiên, đo CPU time không bảo đảm WCET thật. WCET gồm cache, pipeline, tranh chấp bộ nhớ, v.v. vẫn cần phân tích tĩnh riêng hoặc kiểm trên môi trường target.

Wall-clock time so với CPU timeWall clock gồm chờ và preempt; CPU time chỉ đếm lúc task thực sự chạy, hữu ích khi kiểm WCET nhưng không tự bảo đảm WCET.CPU timeGồm: chỉ tính toán thậtTổng CPU time: 120msWall-clock timeGồm: tính + chờ + block + preemptTổng thời gian trôi: 500msHiệu = thời gian chờ, block, preemptCPU time quan sát chi phí tính toán thậtHỗ trợ kiểm chứng và giám sát WCETLoại thời gian chờ, block, preemptLưu ýĐo không bảo đảm WCET thậtCần phân tích tĩnh hoặc kiểm trên target

10. Demo tổng hợp — hệ thống real-time đa chu kỳ

Ghép mọi yếu tố đã học — ưu tiên, Ceiling_Locking, delay until, protected object — để dựng một hệ thống real-time đa chu kỳ điển hình.

-- 08_multiperiodic.ada
-- Demo tích hợp hệ thống real-time đa chu kỳ
-- Task đọc cảm biến chu kỳ nhanh (100ms)
-- Task điều khiển chu kỳ chậm (400ms)
-- Chia sẻ dữ liệu bằng Ceiling_Locking

pragma Locking_Policy (Ceiling_Locking);

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

procedure Multiperiodic_Demo is

   package Int_IO is new Ada.Text_IO.Integer_IO (Integer);

   protected Shared_Sensor is
      pragma Priority (System.Any_Priority'Last);
      procedure Write (V : Integer);
      function Read return Integer;
   private
      Value : Integer := 0;
   end Shared_Sensor;

   protected body Shared_Sensor is
      procedure Write (V : Integer) is
      begin
         Value := V;
      end Write;

      function Read return Integer is
      begin
         return Value;
      end Read;
   end Shared_Sensor;

   task Fast_Sensor is
      pragma Priority (System.Default_Priority + 3);
      pragma Storage_Size (4 * 1024);
   end Fast_Sensor;

   task body Fast_Sensor is
      Next_Release : Time := Clock + Milliseconds (100);
      Period       : constant Time_Span := Milliseconds (100);
      Cycle        : Natural := 0;
   begin
      Put_Line ("[Fast] Sensor reader starts (100ms period)");

      for I in 1 .. 12 loop
         delay until Next_Release;
         Cycle := Cycle + 1;
         Shared_Sensor.Write (Cycle * 10);
         Next_Release := Next_Release + Period;
      end loop;
      Put_Line ("[Fast] Done");
   end Fast_Sensor;

   task Slow_Controller is
      pragma Priority (System.Default_Priority + 2);
      pragma Storage_Size (4 * 1024);
   end Slow_Controller;

   task body Slow_Controller is
      Next_Release : Time := Clock + Milliseconds (150);
      Period       : constant Time_Span := Milliseconds (400);
      Cycle        : Natural := 0;
      Raw          : Integer;
   begin
      Put_Line ("[Slow] Controller starts (400ms period)");

      for I in 1 .. 3 loop
         delay until Next_Release;
         Cycle := Cycle + 1;
         Raw := Shared_Sensor.Read;
         Put_Line ("[Slow] Cycle" & Natural'Image (Cycle) &
                   " reads sensor =" & Integer'Image (Raw));
         Next_Release := Next_Release + Period;
      end loop;
      Put_Line ("[Slow] Done");
   end Slow_Controller;

begin
   Put_Line ("=== Multiperiodic Real-Time System Demo ===");
   Put_Line ("Fast sensor (100ms) x 12 + Slow controller (400ms) x 3");
   Put_Line ("Ceiling_Locking prevents priority inversion on shared data");
   delay until Clock + Milliseconds (2000);
   Put_Line ("Main: done");
end Multiperiodic_Demo;

Cấu trúc hệ thống:

Hình dưới là ví dụ lịch, lấy thời điểm release của mã mẫu (cảm biến nhanh chu kỳ 100ms; điều khiển chậm chu kỳ 400ms, lệch 150ms) rồi giả định thời gian thực thi để minh họa. Bản thân mã không chứa phép tính điều khiển 80ms, nên đây không phải hình đo được. Nếu cảm biến nhanh có ưu tiên cao hơn, khi release của cảm biến nhanh đến trong lúc điều khiển chậm đang chạy, điều khiển chậm bị tạm dừng. Để dễ đọc, hình chỉ vẽ một lần ngắt đại diện mỗi chu kỳ điều khiển chậm; thực tế cảm biến nhanh được release ở mỗi mốc 100ms.

Lịch đa chu kỳ cảm biến nhanh và điều khiển chậmCảm biến 100 ms preempt vòng điều khiển 400 ms tại các mốc minh họa.Điều khiển chậm chu kỳ 3 (release=950ms)Điều khiển chậm chu kỳ 2 (release=550ms)Điều khiển chậm chu kỳ 1 (release=150ms)1000-1010msCảm biến nhanh #10preempt vì P+3950-1000msĐiều khiển chậm #3 nửa đầu1010-1040msĐiều khiển chậm #3 nửa sau600-610msCảm biến nhanh #6preempt vì P+3550-600msĐiều khiển chậm #2 nửa đầu610-640msĐiều khiển chậm #2 nửa sau150-200msĐiều khiển chậm #1 nửa đầu100-110msCảm biến nhanh #1200-210msCảm biến nhanh #2preempt vì P+3210-240msĐiều khiển chậm #1 nửa sauGiả định minh họaCảm biến nhanh: xử lý 10msĐiều khiển chậm: xử lý 80ms

Mẫu này — “thu thập cảm biến nhanh + vòng điều khiển chậm” — là cấu trúc điển hình thường thấy trong hệ thống điều khiển công nghiệp và điều khiển robot.

11. Nơi tính năng real-time của Ada phát huy

Tính năng real-time của Ada đặc biệt có giá trị ở các lĩnh vực sau.

Lĩnh vực Ada Annex D phát huyHàng không vũ trụ, đường sắt, ô tô, thiết bị y tế, điều khiển công nghiệp và quốc phòng.Ada Annex DTính năng real-timeHàng không vũ trụDO-178CĐường sắtHọ EN 50128Ô tôISO 26262Thiết bị y tếIEC 62304Điều khiển công nghiệpHọ IEC 61508Quốc phòng và hệ thống độ tin cậy caoĐiều khiển bayLĩnh vực áp dụng nhiều thành tíchĐiều khiển vệ tinh và tàu vũ trụHệ thống tín hiệuĐiều khiển tàu tự độngỨng viên cho ECU liên quan an toànÁp dụng hạn chế, có chọn lọctrong lĩnh vực C / MISRA-C vẫn chủ đạoMáy tạo nhịpBơm truyền dịchĐiều khiển robotMáy công cụ NCMáy tính nhiệm vụHệ thống vận hành dài hạn

Trong hình, chỉ ô tô được ghi “áp dụng hạn chế, có chọn lọc” chủ yếu vì quy mô hệ sinh thái sẵn có, hơn là vì ngôn ngữ có hợp hay không. Phần mềm trên xe đã chất chồng trên giả định C và MISRA-C — từ đặc tả API ngành như AUTOSAR, mã do nhà cung cấp giao, compiler và công cụ kiểm đã qua chứng nhận, cho tới số kỹ sư. Đổi ngôn ngữ, dù chỉ một component, nghĩa là phải dựng lại cả bộ công cụ xung quanh cùng quy trình mua sắm và kiểm chứng. Vì thế Ada dễ đứng ở vị trí được chọn lọc cho một số component cần bảo đảm đặc biệt cao, hoặc cho tổ chức đã có tài sản Ada và tổ chức phát triển sẵn, hơn là thành chuẩn của cả xe. Ngược lại, ở lĩnh vực như hàng không vũ trụ hay đường sắt, nơi cả hệ sinh thái đã nghiêng về độ tin cậy cao, rào cản đó vốn không có.

12. Lưu ý và giới hạn

Tính năng real-time của Ada mạnh, nhưng không phải vạn năng.

1. Phụ thuộc nền tảng:

  • Ánh xạ thực tế của pragma Priority phụ thuộc môi trường chạy (OS + runtime GNAT). Trên Linux nó được map sang SCHED_FIFO, nhưng trên Windows preemption đầy đủ có thể không được bảo đảm.

2. Ràng buộc của Ravenscar:

  • Tạo task động bị cấm, nên mọi task phải được khai báo tĩnh lúc hệ thống khởi động. Điều đó hạn chế tự do thiết kế.

3. Giới hạn đo WCET:

  • Ada.Execution_Timeđo, không phải bảo đảm. WCET thật, gồm cache miss và pipeline hazard, cần kiểm riêng bằng công cụ phân tích tĩnh.

4. Overhead:

  • Đánh giá barrier của protected object được chạy tự động khi entry hoàn tất hoặc bị hủy, và khi ra khỏi protected object. Với protected object được gọi rất thường xuyên, cần tính overhead này.

5. Rào cản toolchain:

  • Để dùng hết tính năng real-time của Ada, cần cross-compiler và runtime phù hợp. Đặc biệt trên target nhúng, bạn sẽ phụ thuộc runtime do vendor cung cấp.

13. Tóm tắt

Bài viết đã lần lượt xem các tính năng real-time mà Annex D của Ada cung cấp, qua 8 ví dụ mã.

Tính năng Giá trị mang lại
Ưu tiên task Lập lịch preempt theo ưu tiên
Ceiling_Locking Ngăn priority inversion gắn sẵn trong ngôn ngữ
delay until Thực thi theo chu kỳ, ngăn cumulative drift
Ravenscar profile Tập con tasking dễ đưa vào phân tích tĩnh
Timing event Thức dậy theo thời điểm, không cần polling
Hàng đợi protected Đồng bộ dựa trên barrier của protected object
Đo thời gian thực thi Giám sát CPU time theo từng task
Tích hợp đa chu kỳ Thiết kế để các task khác chu kỳ cùng tồn tại an toàn

Bản chất tính năng real-time của Ada là chúng không phải gắn thêm sau. Quy tắc lock kìm priority inversion, chỉ định thời điểm cho thực thi theo chu kỳ, giám sát thời gian thực thi, v.v. được cung cấp như một phần của đặc tả ngôn ngữ. Việc đạt deadline vẫn phải xác nhận bằng thiết kế và phân tích, nhưng runtime ngôn ngữ xếp sẵn các tiền đề cho công việc đó — đó là thế mạnh lớn.

Bước tiếp theo, để thử phát triển hệ thống real-time bằng Ada, hãy cài toolchain GNAT qua Alire rồi build mã mẫu trong bài bằng gnatchop + gnatmake.

Nền tảng xử lý đồng thời của Ada (task, rendezvous, protected object) được trình bày trong bài trước, “Xử lý đồng thời an toàn trong Ada”.

14. Tài liệu tham khảo

Các bài viết gần đây có cùng thẻ để tìm hiểu sâu hơn những chủ đề lân cận.

Các trang này đặt chủ đề trong bối cảnh rộng hơn của dịch vụ và quyết định.

Câu hỏi thường gặp

Các câu hỏi thường gặp khi tư vấn về chủ đề của bài viết.

Annex D của Ada là gì?
Đó là nhóm tính năng hướng hệ thống real-time được chuẩn hóa như một phần của đặc tả ngôn ngữ Ada. Nó gồm lập lịch preempt theo ưu tiên bằng FIFO_Within_Priorities, giao thức Ceiling_Locking để ngăn đảo ưu tiên (priority inversion), thực thi theo chu kỳ neo thời điểm tuyệt đối bằng delay until, Ravenscar profile, timing event, và đo thời gian thực thi theo từng task bằng Ada.Execution_Time. Điểm đặc trưng là chúng không phải thư viện gắn thêm sau, mà được gắn sẵn trong chính runtime của ngôn ngữ.
Priority inversion là gì? Ada ngăn nó thế nào?
Đó là hiện tượng task ưu tiên thấp giữ lock rồi bị preempt bởi task ưu tiên trung bình, khiến task ưu tiên cao đang chờ lock đó bị block vô hạn. Sự cố thực sự xảy ra trên Mars Pathfinder năm 1997 và khiến tàu thám hiểm reset lặp lại. Ada cung cấp giao thức Ceiling_Locking như tính năng ngôn ngữ: task vào protected object được tự động nâng lên ceiling priority, nên task ưu tiên trung bình không preempt được.
Ravenscar profile là gì?
Đó là profile thu hẹp tasking Ada thành một tập con tất định, có thể phân tích tĩnh, dành cho hệ thống an toàn cực kỳ quan trọng. Nó cấm tạo task động, câu select, câu abort, delay tương đối, câu requeue, và một số cấu trúc khác. Nhờ hạn chế đó, phân tích timing tĩnh dễ làm hơn, và chương trình dễ đáp ứng các đặc tính mà tiêu chuẩn an toàn như DO-178C (phần mềm hàng không) hay ISO 26262 (an toàn chức năng ô tô) yêu cầu. Trên GNAT, bạn bật bằng cách ghi pragma Profile (Ravenscar) trong tệp gnat.adc.
Vì sao task chu kỳ dùng delay until chứ không dùng delay?
Với delay chỉ định thời gian tương đối, thời gian xử lý mỗi vòng được cộng thêm nên chu kỳ lệch dần — đó là cumulative drift. delay until lấy thời điểm tuyệt đối để quyết thời điểm phát lần sau, nên dù một lần xử lý bị trễ, các lần phát tiếp theo vẫn đúng. Tuy nhiên, nếu xử lý đã vượt thời điểm thức dậy kế tiếp, delay until trả về gần như ngay; bạn cần thiết kế riêng để phát hiện đó là deadline miss và xử lý như quá tải.

Hồ sơ tác giả

Trang giới thiệu tác giả bài viết.

Go Komura

Đại diện của KomuraSoft LLC

Chuyên về phát triển phần mềm Windows, tư vấn kỹ thuật và điều tra lỗi, đặc biệt trong các dự án có hệ thống hiện hữu và lỗi khó tái hiện.

Liên kết công khai

Quay lại blog