תכנות מערכות real-time ב-Ada — עדיפויות, מחזוריות ובקרת execution time

· עודכן בתאריך: · · Ada, RealTime, Ravenscar, CeilingLocking, Tasking, Scheduling, PriorityInversion, ProgrammingLanguage, זמן אמת, אמינות גבוהה

1. מבוא — הקשר העמוק בין Ada ל-real-time

במאמר הקודם, “עיבוד מקבילי בטוח ב-Ada”, הסברנו את היסודות של עיבוד מקבילי בטוח עם tasks ו-protected objects ב-Ada. הפעם ממשיכים משם לתחום עם אילוצים חמורים יותר — מערכות real-time.

במערכת real-time, “נכונות” אינה רק תוצאה לוגית נכונה. היא כוללת גם שהתוצאה מגיעה בתוך ה-deadline. תשובה נכונה שמאחרה ב-1ms מסוכנת כמו תשובה שגויה.

Ada עונה על הדרישה הזו באוסף יכולות real-time שתוקנן כ-Annex D (Real-Time Systems) במפרט השפה. זו לא “ספרייה שמוסיפים אחר כך”, אלא הבטחת real-time שמובנית ב-runtime של השפה עצמה.

יכולות real-time של Ada (Annex D):
- עדיפות של tasks ו-preemption (FIFO_Within_Priorities)
- פרוטוקול Ceiling_Locking (מניעת priority inversion)
- הרצה מחזורית לפי זמן מוחלט עם delay until
- פרופיל Ravenscar (תת-קבוצה ל-safety-critical)
- timing events (wakeup לפי זמן, בלי polling)
- ניטור זמן ריצה (Ada.Execution_Time)
- תזמון רב-מחזורי

במאמר הזה נעבור עליהם בהדרגה, בשמונה דוגמאות קוד מעשיות. אפשר לראות כל קטע כדוגמה עצמאית, אבל בדוגמאות 04/05 יש כמה יחידות קומפילציה: מפצלים ב-gnatchop ואז בונים ב-gnatmake.

קהל היעד ורקע נדרש: המאמר מניח שאתם מכירים את היסודות שכוסו במאמר הקודם — tasks, rendezvous ו-protected objects. הוא מיועד למפתחים שמתעניינים בתוכנת בקרה למערכות embedded או high-integrity, ולא מלמד את תחביר Ada מאפס.

סביבת הבדיקה: שמונה הדוגמאות במאמר אומתו כניתנות ל-build ב-GNAT 13.3.0 (Ubuntu 24.04, x86-64). פלט ההרצה בפרקים 3 ו-5 נלקח מאותה סביבה. איך עדיפות ו-preemption באמת מתנהגות תלוי ב-OS וב-runtime של GNAT (פרק 12), ולכן פרטי הפלט יכולים להשתנות בין סביבות.

קטעי הקוד במאמר מסודרים לפי פרק ופורסמו ב-GitHub כאוסף ייחוס.

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

knowledge map של המאמר

Annex D במפרט שפת Ada משלב ב-runtime של השפה עצמה את FIFO_Within_Priorities לפי עדיפות של task, את פרוטוקול Ceiling_Locking שמקצה אוטומטית ceiling priority ל-protected object, הרצה מחזורית עם delay until שמונעת cumulative drift, פרופיל Ravenscar שקל יותר לנתח סטטית, timing events בלי צורך ב-polling, ומדידת execution time לכל task. Ceiling_Locking מונע את ה-priority inversion שקרה בפועל ב-Mars Pathfinder ב-1997 וגרם לגשושית לחזור על reset, בכך ש-task שנכנס ל-protected object עולה אוטומטית ל-ceiling priority. פרופיל Ravenscar אוסר delay יחסי, משפטי select ועוד, ומצמצם את ה-tasks של Ada לתת-קבוצה שקל יותר לעשות לה static timing analysis; ב-GNAT מפעילים אותו עם כתיבת pragma בקובץ gnat.adc. אבל המיפוי בפועל של העדיפויות תלוי ב-OS וב-runtime של GNAT.

knowledge map: תכנות מערכות real-time ב-Adaתרשים שמראה איך Annex D של Ada משלב על גבי tasks ו-protected objects dispatch מבוסס-עדיפות, Ceiling_Locking, delay until, פרופיל Ravenscar, timing events ומדידת execution time, ואיך Ceiling_Locking מונע priority inversion מהסוג שקרה ב-Mars Pathfinder.משתמש במשתמש במשתמש במשתמש במשתמש במשתמש בדורשדורשדורשמונעעלול לגרום לדורשדורשדורשמוגדר בעלול לגרום למונעלא מומלץ למשתמש במשתמש בדורשAnnex D (Real-Time Systems Annex)Ceiling_LockingFIFO_Within_Prioritiesdelay until (לפי זמן מוחלט)Ravenscar profileAda.Real_Time.Timing_EventsAda.Execution_TimeAda taskprotected objectGNATpriority inversionתקרית priority inversion ב-Mars Pathfinderdelay יחסיcumulative drift בביצוע מחזורי

ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 21, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle

2. מהי מערכת real-time

נתחיל מהמונחים.

מושג הסבר
hard real-time חריגה מ-deadline היא כשל קטלני של המערכת (בקרת טיסה, כריות אוויר, קוצב לב)
soft real-time חריגה מ-deadline אינה רצויה, אבל חריגה נדירה מותרת (סטרימינג, משחקים)
deadline הזמן המוחלט שבו ה-task חייב להסתיים
period מרווח הזמן שבו ה-task מופעל שוב ושוב
WCET (Worst-Case Execution Time) זמן הריצה הגרוע ביותר של ה-task
jitter השונות בזמני ההפעלה המחזורית
schedulability התכונה “האם קבוצת ה-tasks יכולה לרוץ ולעמוד בכל ה-deadlines”. העבודה לבדוק זאת על הנייר היא ניתוח schedulability (למשל response-time analysis)
פרופיל Ravenscar פרופיל שמצמצם את יכולות ה-tasking של Ada לתת-קבוצה שקל יותר לנתח סטטית. השם בא מהכפר Ravenscar בבריטניה, שבו נערך הכנס שבו גובש הפרופיל (פרק 6)

בתכנון מערכת real-time, תנאי הכרחי חשוב לכל task הוא WCET <= deadline. זה לבדו אינו ערובה שהמערכת כולה תעמוד ב-deadlines. צריך בנפרד response-time analysis שכולל זמן blocking, הקצאת עדיפויות, jitter, interrupts, והתנהגות ה-runtime וה-OS. בפועל שואפים ל-WCET < deadline כדי להשאיר מרווח. יכולות real-time של Ada נותנות מודל הרצה predictable, ברמת השפה, כך שהניתוח הזה נהיה קל יותר.

מושגי יסוד במערכת real-timeתרשים שמקשר hard real-time ו-soft real-time, deadline, period, WCET ו-jitter לניתוח schedulability, ומציג את מנגנוני Annex D שתומכים ב-predictability.דרישות real-timeאי-עמידה ב-deadline = כשל קטלניחריגה נדירה מותרתdeadlineזמן מוחלט לסיוםperiodמרווח חזרהWCETזמן ריצה גרוע ביותרjitterשונות במחזורhard real-timeבקרת טיסהכריות אווירקוצב לבsoft real-timeסטרימינגמשחקיםUIAda Annex Dמנגנונים שתומכים ב-predictabilityFIFO_Within_PrioritiesCeiling_Lockingdelay untilניתוח schedulabilityתנאי הכרחי: WCET &lt;= deadlineתנאי מספיק נבדק בניתוח response time

אחת התופעות המסוכנות ביותר במערכת real-time היא priority inversion. הבעיה קרה בפועל ב-Mars Pathfinder ב-1997, וגרמה לגשושית לחזור על reset.

איך נוצר priority inversiontask בעדיפות נמוכה מחזיק lock, task בעדיפות בינונית עושה לו preemption, ו-task בעדיפות גבוהה נחסם בלי הגבלה.משאב משותףtask בעדיפות בינוניתtask בעדיפות גבוההtask בעדיפות נמוכהschedulerמשאב משותףtask בעדיפות בינוניתtask בעדיפות גבוההtask בעדיפות נמוכהschedulerבתוך critical sectionH מתעורר, וה-scheduler עוצר את Lנחסם בהמתנה ל-lock (L עדיין מחזיק)H ממתין ל-lock, ולכן L ממשיךממשיך לקראת שחרור ה-lock...M מתעורר, וה-scheduler עוצר את LL לא יכול לשחרר את ה-lockM ממשיך לרוץ (גם H וגם L תקועים)[priority inversion] עדיפות גבוהה חסומה בלי הגבלהתופס lockמנסה לתפוס lock

task בעדיפות נמוכה מחזיק lock, task בעדיפות בינונית עושה לו preemption, ו-task בעדיפות גבוהה נחסם בלי הגבלה. ב-Mars Pathfinder הטיפול בפועל היה הפעלת priority inheritance ב-VxWorks. Ada פותרת את אותו סוג בעיה בשיטה אחרת: היא מספקת Ceiling_Locking כיכולת שפה.

3. יסודות עדיפות של task — FIFO_Within_Priorities

FIFO_Within_Priorities היא מדיניות dispatching סטנדרטית מבוססת-עדיפות שאפשר לבקש ב-Ada Annex D. אם לא מציינים מדיניות, ברירת המחדל היא implementation-defined; ב-GNAT, ב-targets רבים, משתמשים במדיניות מהמשפחה הזו. בתוך אותה עדיפות הריצה היא FIFO (first-in, first-out), ו-task בעדיפות גבוהה יותר עושה preemption ל-task בעדיפות נמוכה יותר.

-- 01_task_priority.ada
-- צורה בסיסית של עדיפות task ו-FIFO_Within_Priorities
-- configuration pragma חייב לבוא לפני context clauses

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;

נקודות:

  • pragma Priority נותן לכל task עדיפות סטטית. Priority'Last היא הגבוהה ביותר, Priority'First הנמוכה ביותר.
  • גוף ה-task בדוגמה הזו אינו חישוב כבד; הוא ממתין עם delay until עד הזמן שצוין. מה שרוצים לראות הוא שכאשר שניהם runnable, ה-task בעדיפות הגבוהה מקבל קודם הזדמנות לרוץ.
  • התרשים מציג את צד ה-preemption בין עדיפויות שונות ב-FIFO_Within_Priorities. כדי לראות סדר FIFO בתוך אותה עדיפות צריך דוגמה אחרת, עם כמה tasks באותה עדיפות.
  • במערכת אמיתית בדרך כלל מתכננים עדיפויות יחסית ל-System.Default_Priority.
FIFO_Within_Priorities בין עדיפויות שונותכששני tasks רצים יחד, ה-scheduler בוחר קודם את ה-task בעדיפות הגבוהה, ואחרי delay until הוא חוזר להריץ לפי זמני ה-wakeup.task בעדיפות נמוכה(Priority=First)task בעדיפות גבוהה(Priority=Last)main taskschedulertask בעדיפות נמוכה(Priority=First)task בעדיפות גבוהה(Priority=Last)main taskschedulerT=0ms: שני ה-tasks במצב runnableמדפיס לוג התחלהמדפיס לוג התחלהT=100ms: HP מתעוררמדפיס לוג סיוםT=500ms: LP מתעוררמדפיס לוג סיום(T=800ms) main מסתייםיצירת taskיצירת taskבוחר את HP, העדיפות הגבוהה ביותרblock עם delay until T+100msמריץ את LP הבאblock עם delay until T+500msמריץ את HPמריץ את LP

דוגמת הרצה (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

שימו לב שלוגי ההתחלה של שני ה-tasks יוצאים לפני שורת הכותרת של main. tasks שמוצהרים בחלק ההצהרות מופעלים לפני שנכנסים לגוף של ה-subprogram שעוטף אותם, ולכן סדר כזה יכול להופיע. בנוסף, סדר שתי שורות לוג ההתחלה יכול להתחלף בין הרצות. כפי שמוסבר בפרק 12, איך pragma Priority משתקף בתזמון בפועל תלוי ב-OS וב-runtime של GNAT, ועל general-purpose Linux אין ערובה שסדר ההפעלה יתאים לעדיפות. מה שכן יציב בדוגמה הזו הוא סדר הסיום לפי 0.1s / 0.5s שצוינו ב-delay until.

טווח העדיפויות ב-Ada (ברירת המחדל של GNAT):
  Priority'First  = 0   (הנמוכה ביותר)
  Priority'Last   = 30  (הגבוהה ביותר; תלוי ב-OS)

4. Ceiling_Locking — השפה מונעת priority inversion

אחת הבעיות הקשות במערכת real-time היא priority inversion: task בעדיפות גבוהה מחכה ל-lock שמוחזק בידי task בעדיפות נמוכה, וה-task הנמוך מקבל preemption מ-task בעדיפות בינונית, כך שה-task הגבוה נחסם בלי הגבלה.

Ada משלבת מול זה את פרוטוקול Ceiling_Locking ישירות ב-protected objects.

-- 02_ceiling_locking.ada
-- מניעת priority inversion באמצעות פרוטוקול Ceiling_Locking
-- configuration pragma חייב לבוא לפני context clauses

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;

איך Ceiling_Locking עובד:

  1. מגדירים ceiling priority על ה-protected object עם pragma Priority (Ceiling).
  2. כל task שנכנס ל-protected object עולה אוטומטית ל-ceiling priority בכניסה.
  3. לכן task בעדיפות בינונית לא יכול לעשות preemption ל-task שנמצא כרגע בתוך ה-protected object.
  4. ביציאה מה-protected object, העדיפות חוזרת למקורית.

התרשים למטה אינו trace מדויק של זמני הקוד שקדם לו. הוא תרשים מושגי שמראה איך דפוס ה-priority inversion מתרשים 2 נחסם עם Ceiling_Locking.

Ceiling_Locking מונע priority inversiontask שנכנס ל-protected object עולה ל-ceiling priority, ולכן task בעדיפות בינונית לא יכול לעשות לו preemption.protected object(ceiling priority=30)task בעדיפות גבוהה(עדיפות=30)task בעדיפות בינונית(עדיפות=20)task בעדיפות נמוכה(עדיפות=10)schedulerprotected object(ceiling priority=30)task בעדיפות גבוהה(עדיפות=30)task בעדיפות בינונית(עדיפות=20)task בעדיפות נמוכה(עדיפות=10)schedulerקורא עם active priority > ceiling priority מקבל Program_ErrorH(30) שווה ל-ceiling (30), ולכן מותר להיכנסעדיפות הריצה עולה ל-30M מתעוררL רץ ב-ceiling priority 30M(20) לא יכול לעשות preemptionH מתעוררH(30) עובר את בדיקת ה-ceilingאבל ממתין כי L משתמש ב-POהעדיפות חוזרת ל-10אחרי שחרור PO מריצים את HH(30) = ceiling (30), ולכן אפשר להיכנס אחרי שהתחרות נגמרהנכנס לפעולה מוגנתמבצע את הפעולהיוצא מהפעולה המוגנתנכנס לפעולה מוגנתיוצא מהפעולה המוגנת

כלל תכנון: ceiling priority של protected object צריך להיות גבוה לפחות כמו העדיפות הגבוהה ביותר של כל task שמשתמש בו. אם שוברים את זה, ו-task עם active priority גבוהה מה-ceiling קורא לפעולה מוגנת, Ada יכולה לזהות את טעות התכנון עם Program_Error.

כדי לקבל אפקט מקביל עם mutex של pthread ב-C צריך להגדיר במפורש את התכונה PTHREAD_PRIO_PROTECT. ב-Ada זו יכולת סטנדרטית של השפה.

5. delay until — task מחזורי בלי drift

הדפוס הבסיסי במערכת real-time הוא task מחזורי. ב-task שרץ שוב ושוב במרווח קבוע, חשוב מאוד למנוע שגיאת תזמון מצטברת (drift).

delay until של Ada פותר את זה בצורה נקייה.

-- 03_periodic_task.ada
-- task מחזורי עם delay until — בלי 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;

למה delay until:

שיטה בעיה
delay Period; זמן העיבוד של כל איטרציה מתווסף, והמחזור זז בהדרגה (cumulative drift)
delay until Next_Release; Next_Release := Next_Release + Period; העוגן הוא זמן מוחלט, כך שגם אם עיבוד אחד מאחר, זמן ה-release הבא נשאר נכון

עם זאת, delay until אינו מבטיח מעצמו שזמן העיבוד נכנס בתוך המחזור. אם העיבוד כבר עבר את זמן ה-wakeup הבא, אותו delay until חוזר כמעט מיד, והמערכת צריכה לטפל בזה כ-deadline miss וכעומס יתר.

עם delay:
  T=0ms → עיבוד (15ms) → delay 100ms → T=115ms → עיבוד (10ms) → ...
  מרווחים בפועל: 115ms, 110ms, ... (זמן העיבוד מצטבר)

עם delay until:
  Next_Release: 100ms, 200ms, 300ms, ... (זמן מוחלט)
  T=0ms → עיבוד (15ms) → delay until 100ms → T=100ms → עיבוד (10ms) → delay until 200ms
  מרווחים בפועל: 100ms, 100ms, ... (לא תלוי בזמן העיבוד)

דוגמת הרצה (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

בכל מחזור יש איחור wakeup של בערך 0.2 עד 0.3ms, אבל האיחור הזה לא עובר למחזור הבא. גם במחזור החמישי הסטייה מזמן הייחוס קטנה מ-1ms. אם היו כותבים delay Period;, האיחור הזה היה מצטבר בכל פעם, ואחרי חמישה מחזורים היה נראה הבדל. המספרים האלה הם דוגמה אחת על general-purpose Linux, ולא ערכים שמערכת hard real-time מבטיחה.

נשתמש בדפוס ה-delay until הזה בכל ה-tasks המחזוריים מכאן והלאה.

delay לעומת delay untildelay יחסי צובר drift, delay until נשען על זמן מוחלט, ואם חורגים מהמחזור delay until חוזר מיד ומזהים deadline miss.חריגת מחזור - deadline missחישוב 130msהבא = T+100msdelay until T+100ms חוזר מידמזהים איחור ומטפלים כעומס יתרdelay until - לפי זמן מוחלטחישוב 15msהבא = T+100msdelay until T+100ms → wakeup ב-100msחישוב 10msהבא = T+200ms → wakeup ב-200msמרווחים בפועל: 100ms, 100ms...delay Period - cumulative driftdelay 100ms → wakeup ב-115msT=0ms: חישוב 15msחישוב 10ms → 125msdelay 100ms → wakeup ב-225msמרווחים בפועל: 115ms, 110ms...השגיאה מצטברת עם הזמןמונע cumulative drift

6. פרופיל Ravenscar — תת-קבוצה real-time שניתנת לאימות

יכולות ה-tasking של Ada חזקות, אבל במערכות safety-critical, “חזק מדי” הופך לבעיה. יצירת tasks דינמית, משפטי select, משפטי abort וכדומה מקשים על ניתוח סטטי של WCET.

פרופיל Ravenscar הוא התשובה של Ada: הוא מצמצם את יכולות ה-tasking ל-תת-קבוצה דטרמיניסטית שניתנת לניתוח סטטי.

-- 04_ravenscar_profile.ada
-- צורה בסיסית של פרופיל Ravenscar
-- בזמן הקומפילציה מגדירים pragma Profile (Ravenscar); ב-gnat.adc
-- build: gnatchop -w 04_ravenscar_profile.ada .
--         → מתפצל ל-ravenscar_state.ads / ravenscar_state.adb / ravenscar_demo.adb
--         מכינים gnat.adc ואז 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;

מגבלות פרופיל Ravenscar:

יכולת אסורה סיבה
יצירת tasks דינמית (new או טיפוס access) הקצאת זיכרון בזמן ריצה אינה דטרמיניסטית
משפט select לא רק כמה חלופות; כל משפט ה-select מקשה על ניתוח control flow
משפט abort abort אסינכרוני יוצר חוסר חיזוי במצב
Ada.Task_Attributes התנהגות דינמית בזמן ריצה
שינוי עדיפות דינמי הנחות ניתוח התזמון משתנות בזמן ריצה
delay יחסי (delay) נוטה לייצר cumulative drift; משתמשים ב-delay until לפי זמן מוחלט
יותר מ-entry אחד לכל protected object נוספים תנאי blocking ומקרי ניתוח
סיום task ב-Ravenscar מתייחסים לכל ה-tasks כלא-מסתיימים
משפט requeue מעקב אחרי control flow מסתבך

עם המגבלות האלה, תוכנית שעומדת ב-Ravenscar מגיעה לצורה שקל יותר לעשות לה static timing analysis. זו תכונה שתקני בטיחות כמו DO-178C (תוכנת תעופה) ו-ISO 26262 (בטיחות פונקציונלית ברכב) דורשים. הרשימה למטה היא קטע מהמגבלות העיקריות; בפרופיל עצמו יש גם כללים נוספים שקשורים ל-runtime ולניתוח, כמו No_Task_Hierarchy ו-Detect_Blocking.

מגבלות פרופיל Ravenscarפרופיל Ravenscar מצמצם את יכולות ה-tasking של Ada ומחייב FIFO_Within_Priorities ו-Ceiling_Locking כדי לאפשר ניתוח תזמון סטטי.יכולות tasking מלאות של Adaפרופיל Ravenscarמגבלותמדיניות חובהאיסור על יצירת tasks דינמיתאיסור על משפט selectאיסור על משפט abortאיסור על Task_Attributesהגבלה ל-entry אחד לכל protected objectאיסור על משפט requeueאיסור על delay יחסימשתמשים ב-delay untilאיסור על שינוי עדיפות דינמיאיסור על סיום taskכל ה-tasks לא-מסתיימיםFIFO_Within_PrioritiesCeiling_Lockingמה שנהיה קל יותר:static timing analysisDO-178Cתוכנת תעופהISO 26262בטיחות פונקציונלית ברכבIEC 62304תוכנת ציוד רפואי

כדי להפעיל את פרופיל Ravenscar, כותבים בקובץ gnat.adc את השורה הבאה:

pragma Profile (Ravenscar);

7. timing events — wakeup לפי זמן, בלי polling

במערכות real-time רבות חוזרת הדרישה “כשמגיע הזמן שצוין, להעיר task בעדיפות גבוהה”. מימוש תמים היה עושה polling לטיימר. Ada נותנת מנגנון משוכלל יותר — timing events.

-- 05_timing_events.ada
-- timing events (Ada.Real_Time.Timing_Events)
-- מנגנון שמעיר task בעדיפות גבוהה בלי polling
-- build: gnatchop -w 05_timing_events.ada .
--         → מתפצל ל-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;

איך timing events עובדים:

1. Set_Handler(Timer_1, T+100ms, S.Fire'Access)  — רושמים handler לזמן מוחלט
2. עוברות T+100ms — ה-runtime קורא ל-S.Fire ב-ceiling priority
3. Fire קובע את דגל Fired ל-True — ה-barrier נפתח
4. task Reactor מתעורר מ-Wait_For_Event

הנקודה החשובה: בדוגמה הזו Ceiling_Locking מוגדר במפורש, ו-Fire הוא procedure של protected object, ולכן הוא רץ ב-ceiling priority. protected procedure שמשמש כ-handler של timing event שמים ב-protected object עם ceiling priority ברמת interrupt; כאן זה System.Interrupt_Priority'Last. כך לא נוצר priority inversion בזמן הטיפול ב-timing event.

8. תור real-time עם protected object

דפוס שחוזר במערכות real-time הוא producer-consumer. חיישן מייצר נתונים, ו-task בקרה צורך אותם — ואז צריך mutual exclusion ו-blocking על ה-buffer בצורה יעילה.

עם protected object ו-entry barrier של Ada אפשר לממש זאת כסנכרון מבוסס-barrier. בפנים ה-runtime מנהל את ה-mutual exclusion, כך שבקוד האפליקציה אין צורך לכתוב mutex או condition variable ישירות.

-- 06_protected_queue.ada
-- שיתוף נתונים real-time עם protected object
-- pipeline: Producer -> Bounded_Buffer -> Consumer
-- בזמן הקומפילציה מגדירים pragma Locking_Policy (Ceiling_Locking); ב-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;

נקודות תכנון:

  • entry Put when Count < Buffer_Size — אם ה-buffer מלא, Producer נחסם אוטומטית.
  • entry Get when Count > 0 — אם ה-buffer ריק, Consumer נחסם אוטומטית.
  • pragma Priority (System.Any_Priority'Last) — עם ceiling locking, לא נוצר priority inversion בין Producer ל-Consumer.
  • תנאי ה-barrier מוגדרים לפי המצב הפנימי של ה-protected object (Count), ובשחרור ה-lock הם מחושבים מחדש אוטומטית.

בקוד הזה אין mutex, semaphore או condition variable ברמת האפליקציה. ההמתנה הנדרשת מבוטאת ב-entry barrier של ה-protected object.

מצבי bounded bufferprotected object עם buffer מוגבל עובר בין ריק, חלקי ומלא, ו-Get או Put נחסמים לפי תנאי ה-barrier.מצב התחלתיPut (מוסיפים איבר אחד)Put / GetGet (מוציאים את האיבר האחרון)Put (ממלאים את המקום הפנוי האחרון)Get (נוצר מקום פנוי)Get נחסם (barrier Count=0)Put נחסם (barrier Count=Buffer_Size)ריק / Count=0חלקי / Count=1..Buffer_Size-1מלא / Count=Buffer_Size

אחרי Put מוצלח מחשבים מחדש את ה-barrier של מי שמחכה ב-Get, ואחרי Get מוצלח מחשבים מחדש את ה-barrier של מי שמחכה ב-Put. זה קורה בסיום פעולה מוגנת, בלי תלות באיזה מצב בתרשים נמצאים.

9. מדידת זמן ריצה — הצעד הראשון בניטור execution time

כדי להעריך schedulability של מערכת real-time צריך לדעת במדויק את זמן הריצה (CPU time) של כל task. החבילה Ada.Execution_Time נותנת זמן CPU לפי task.

-- 07_execution_time.ada
-- בקרת זמן ריצה (Execution_Time)
-- מדידת זמן CPU לכל 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 מול CPU time:

wall-clock time: Ada.Real_Time.Clock
  → הזמן שעבר בפועל. כולל זמן block ו-preemption.

CPU time (execution time): Ada.Execution_Time.Clock
  → רק הזמן שבו ה-task רץ בפועל על ה-CPU.
  → זמן block ו-preemption לא נספר.

ההבחנה הזו היא נקודת הפתיחה לניטור זמן ריצה ולבדיקת הנחות WCET. בזמן ש-Busy_Worker ממתין ב-delay until, זמן ה-CPU שלו לא גדל; הוא גדל רק בזמן חישוב אמיתי. גם בזמן delay until Clock + Milliseconds(500) של ה-main task, זמן ה-CPU אמור להיות כמעט אפס. עם זאת, מדידת CPU time אינה ערובה ל-WCET האמיתי. WCET שכולל cache, pipeline ותחרות על זיכרון דורש בנפרד ניתוח סטטי או אימות על סביבת היעד.

wall-clock time מול CPU timeAda.Real_Time.Clock מודד זמן שעבר כולל המתנה, ו-Ada.Execution_Time.Clock מודד רק זמן CPU בפועל.CPU timeכולל: חישוב בפועל בלבדCPU כולל: 120mswall-clock timeכולל: חישוב + המתנה + block + preemptionזמן שעבר כולל: 500msההפרש = זמן המתנה, block ו-preemptionCPU time צופה את עלות החישוב בפועלעוזר באימות WCET ובניטורמוציא מחוץ לחשבון המתנה, block ו-preemptionשים לבמדידה אינה ערובה ל-WCET האמיתיצריך ניתוח סטטי או אימות על היעד

10. דמו משולב — מערכת real-time רב-מחזורית

עכשיו מחברים את כל הרכיבים שלמדנו — עדיפות, Ceiling_Locking, delay until, protected objects — ובונים מערכת real-time רב-מחזורית טיפוסית.

-- 08_multiperiodic.ada
-- דמו משולב של מערכת real-time רב-מחזורית
-- task קריאת חיישן במחזור מהיר (100ms)
-- task בקרה במחזור איטי (400ms)
-- שיתוף נתונים עם 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;

מבנה המערכת:

התרשים למטה הוא דוגמת תזמון שמניחה זמני ריצה להסבר, על גבי זמני ה-release שבקוד (חיישן מהיר במחזור 100ms, בקרה איטית במחזור 400ms עם offset של 150ms). בקוד עצמו אין חישוב בקרה של 80ms, ולכן זה אינו תרשים מדידה. אם לחיישן המהיר עדיפות גבוהה יותר, ו-release שלו מגיע בזמן שהבקרה האיטית רצה, הבקרה האיטית נעצרת זמנית. לקריאות, התרשים מצייר preemption מייצג אחד לכל מחזור בקרה איטית; בפועל החיישן המהיר עושה release בכל גבול 100ms.

דוגמת תזמון רב-מחזוריתחיישן מהיר במחזור 100ms עושה preemption ללולאת בקרה איטית במחזור 400ms, לפי הנחות הסבר בלבד.בקרה איטית, מחזור 3 (release=950ms)בקרה איטית, מחזור 2 (release=550ms)בקרה איטית, מחזור 1 (release=150ms)1000-1010msחיישן מהיר #10P+3, לכן preemption950-1000msבקרה איטית #3, חלק א1010-1040msבקרה איטית #3, חלק ב600-610msחיישן מהיר #6P+3, לכן preemption550-600msבקרה איטית #2, חלק א610-640msבקרה איטית #2, חלק ב150-200msבקרה איטית #1, חלק א100-110msחיישן מהיר #1200-210msחיישן מהיר #2P+3, לכן preemption210-240msבקרה איטית #1, חלק בהנחה להסברחיישן מהיר: עיבוד 10msבקרה איטית: עיבוד 80ms

הדפוס הזה — “איסוף חיישן מהיר + לולאת בקרה איטית” — הוא מבנה טיפוסי בבקרה תעשייתית ובבקרת רובוטים.

11. איפה יכולות real-time של Ada בולטות

יכולות real-time של Ada מביאות ערך במיוחד בתחומים האלה.

תחומים שבהם Ada real-time בולטAnnex D רלוונטי לתעופה וחלל, רכבות, רכב, ציוד רפואי, בקרה תעשייתית ומערכות ביטחוניות.Ada Annex Dיכולות real-timeתעופה וחללDO-178Cרכבותמשפחת EN 50128רכבISO 26262ציוד רפואיIEC 62304בקרה תעשייתיתמשפחת IEC 61508ביטחון ומערכות high-integrityבקרת טיסהתחום יישום נפוץבקרת לוויינים וחלליותמערכות איתותבקרת רכבות אוטומטיתמועמד ל-ECU הקשור לבטיחותשימוש מוגבל וסלקטיביבתחום שבו C / MISRA-C דומיננטייםקוצבי לבמשאבות עירויבקרת רובוטיםמכונות CNCmission computersמערכות להפעלה ארוכת טווח

בתרשים, רק תחום הרכב מסומן כ”שימוש מוגבל וסלקטיבי”. הסיבה העיקרית אינה התאמת השפה עצמה, אלא גודל ה-ecosystem הקיים. תוכנה לרכב נשענת על מפרטי API סטנדרטיים כמו AUTOSAR, על קוד שמגיע מספקים, על קומפיילרים וכלי verification שעברו certification, וגם על אוכלוסיית המהנדסים — והכול בנוי סביב C ו-MISRA-C. החלפת שפה, גם אם מדובר ברכיב אחד בלבד, משמעה להכין מחדש את כלי העבודה סביב הרכיב ואת תהליכי הרכש וה-verification. לכן Ada נוטה להיות לא סטנדרט לכל הרכב, אלא בחירה סלקטיבית לרכיבים שדורשים רמת הבטחה גבוהה במיוחד, או לארגונים שכבר יש להם נכסי Ada ומערך פיתוח מתאים. לעומת זאת, בתחומים כמו תעופה וחלל או רכבות, שבהם ה-ecosystem כולו כבר נוטה לצד ה-high-integrity, המחסום הזה פשוט לא קיים.

12. נקודות זהירות ומגבלות

יכולות real-time של Ada חזקות, אבל הן אינן פתרון לכל דבר.

1. תלות בפלטפורמה:

  • המיפוי בפועל של pragma Priority תלוי בסביבת ההרצה (OS + runtime של GNAT). ב-Linux זה ממופה ל-SCHED_FIFO, אבל ב-Windows preemption מלא לא תמיד מובטח.

2. מגבלות Ravenscar:

  • יצירת tasks דינמית אסורה, ולכן צריך להצהיר על כל ה-tasks סטטית בהפעלת המערכת. זה מצמצם את חופש התכנון.

3. גבול מדידת WCET:

  • Ada.Execution_Time הוא מדידה, לא ערובה. WCET אמיתי, כולל cache miss ו-pipeline hazard, צריך לאמת בנפרד עם כלי ניתוח סטטי.

4. overhead:

  • הערכת barrier של protected object רצה אוטומטית בסיום entry, בביטול, וביציאה מה-protected object. ב-protected object שנקרא בתדירות גבוהה צריך לקחת את ה-overhead הזה בחשבון.

5. מחסום toolchain:

  • כדי לנצל במלואן את יכולות real-time של Ada צריך cross-compiler ו-runtime מתאימים. במיוחד ב-targets embedded, בסוף תלויים ב-runtime שה-vendor מספק.

13. סיכום

במאמר הזה עברנו בהדרגה על יכולות real-time של Annex D ב-Ada, בשמונה דוגמאות קוד.

יכולת מה היא נותנת
עדיפות של task תזמון preemptive מבוסס-עדיפות
Ceiling_Locking מניעת priority inversion שמובנית בשפה
delay until הרצה מחזורית בלי cumulative drift
פרופיל Ravenscar תת-קבוצת tasking שקל יותר לנתח סטטית
timing events wakeup לפי זמן, בלי polling
תור מוגן סנכרון מבוסס-barrier עם protected object
מדידת זמן ריצה ניטור CPU time לפי task
שילוב רב-מחזורי תכנון שבו tasks במחזורים שונים חיים יחד בבטחה

המהות של יכולות real-time ב-Ada היא שהן לא תוספת מאוחרת. כללי נעילה שמונעים priority inversion, ציון זמן להרצה מחזורית, וניטור execution time מסופקים כ-חלק ממפרט השפה. עצם העמידה ב-deadline עדיין מאושרת בתכנון ובניתוח, אבל החוזק הגדול הוא שה-runtime של השפה מספק את ההנחות לזה.

הצעד הבא, אם רוצים לנסות בפועל פיתוח מערכת real-time ב-Ada: מתקינים את ה-toolchain של GNAT דרך Alire, ובונים את דוגמאות המאמר ב-gnatchop + gnatmake.

ליסודות העיבוד המקבילי ב-Ada (tasks, rendezvous, protected objects), ראו את המאמר הקודם “עיבוד מקבילי בטוח ב-Ada”.

14. מקורות

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

מה זה Annex D של Ada?
אוסף יכולות למערכות real-time שתוקננו כחלק ממפרט שפת Ada. הוא כולל תזמון preemptive מבוסס-עדיפות עם FIFO_Within_Priorities, פרוטוקול Ceiling_Locking שמונע priority inversion, הרצה מחזורית לפי זמן מוחלט עם delay until, פרופיל Ravenscar, timing events, ומדידת execution time לכל task דרך Ada.Execution_Time. הייחוד הוא שזה לא ספרייה שמוסיפים אחר כך, אלא מובנה ב-runtime של השפה עצמה.
מה זה priority inversion, ואיך Ada מונעת אותו?
תופעה שבה task בעדיפות נמוכה מחזיק lock, task בעדיפות בינונית עושה לו preemption, ו-task בעדיפות גבוהה שמחכה לאותו lock נחסם בלי הגבלה. זה קרה בפועל ב-Mars Pathfinder ב-1997 וגרם לגשושית לחזור על reset. ב-Ada, פרוטוקול Ceiling_Locking הוא יכולת שפה: task שנכנס ל-protected object עולה אוטומטית ל-ceiling priority, ולכן task בעדיפות בינונית לא יכול לעשות לו preemption.
מה זה פרופיל Ravenscar?
פרופיל שמצמצם את יכולות ה-tasking של Ada לתת-קבוצה דטרמיניסטית שניתנת לניתוח סטטי, עבור מערכות safety-critical. אסור ליצור tasks דינמית, אסור select, abort, delay יחסי, requeue ועוד. הצמצום מקל על static timing analysis, ועוזר לעמוד בתכונות שתקני בטיחות כמו DO-178C (תוכנת תעופה) ו-ISO 26262 (בטיחות פונקציונלית ברכב) דורשים. ב-GNAT מפעילים אותו עם pragma Profile (Ravenscar) בקובץ gnat.adc.
למה ב-task מחזורי משתמשים ב-delay until ולא ב-delay?
כי delay עם זמן יחסי מוסיף את זמן העיבוד של כל איטרציה, והמחזור זז בהדרגה (cumulative drift). delay until קובע את זמן ה-release הבא לפי זמן מוחלט, כך שגם אם עיבוד אחד התעכב, זמני ה-release הבאים נשארים נכונים. אבל אם העיבוד כבר עבר את זמן ה-wakeup הבא, delay until חוזר כמעט מיד, ולכן צריך תכנון נפרד שמזהה זאת כ-deadline miss ומטפל בזה כעומס יתר.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג