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.
flowchart LR
accTitle: knowledge map: תכנות מערכות real-time ב-Ada
accDescr: תרשים שמראה איך Annex D של Ada משלב על גבי tasks ו-protected objects dispatch מבוסס-עדיפות, Ceiling_Locking, delay until, פרופיל Ravenscar, timing events ומדידת execution time, ואיך Ceiling_Locking מונע priority inversion מהסוג שקרה ב-Mars Pathfinder.
ada_annex_d["Annex D (Real-Time Systems Annex)"]
ceiling_locking["Ceiling_Locking"]
fifo_within_priorities["FIFO_Within_Priorities"]
ada_delay_until["delay until (לפי זמן מוחלט)"]
ravenscar_profile["Ravenscar profile"]
ada_timing_events["Ada.Real_Time.Timing_Events"]
ada_execution_time["Ada.Execution_Time"]
ada_task["Ada task"]
protected_object["protected object"]
gnat["GNAT"]
priority_inversion["priority inversion"]
mars_pathfinder_priority_inversion["תקרית priority inversion ב-Mars Pathfinder"]
ada_relative_delay["delay יחסי"]
cumulative_drift["cumulative drift בביצוע מחזורי"]
ada_annex_d -->|"משתמש ב"| fifo_within_priorities
ada_annex_d -->|"משתמש ב"| ceiling_locking
ada_annex_d -->|"משתמש ב"| ada_delay_until
ada_annex_d -->|"משתמש ב"| ravenscar_profile
ada_annex_d -->|"משתמש ב"| ada_timing_events
ada_annex_d -->|"משתמש ב"| ada_execution_time
ada_annex_d -->|"דורש"| ada_task
ada_annex_d -.->|"דורש"| protected_object
ada_annex_d -.->|"דורש"| gnat
ceiling_locking -->|"מונע"| priority_inversion
priority_inversion -->|"עלול לגרום ל"| mars_pathfinder_priority_inversion
ravenscar_profile -->|"דורש"| ada_task
ravenscar_profile -->|"דורש"| fifo_within_priorities
ravenscar_profile -->|"דורש"| ceiling_locking
ravenscar_profile -.->|"מוגדר ב"| gnat
ada_relative_delay -->|"עלול לגרום ל"| cumulative_drift
ada_delay_until -->|"מונע"| cumulative_drift
ada_relative_delay -->|"לא מומלץ ל"| ravenscar_profile
ada_timing_events -->|"משתמש ב"| ceiling_locking
ada_timing_events -->|"משתמש ב"| protected_object
ada_execution_time -->|"דורש"| ada_task
ב-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, ברמת השפה, כך שהניתוח הזה נהיה קל יותר.
flowchart LR
accTitle: מושגי יסוד במערכת real-time
accDescr: תרשים שמקשר hard real-time ו-soft real-time, deadline, period, WCET ו-jitter לניתוח schedulability, ומציג את מנגנוני Annex D שתומכים ב-predictability.
HRT["hard real-time"] -->|אי-עמידה ב-deadline = כשל קטלני| Examples["בקרת טיסה<br/>כריות אוויר<br/>קוצב לב"]
SRT["soft real-time"] -->|חריגה נדירה מותרת| Examples2["סטרימינג<br/>משחקים<br/>UI"]
Ada["Ada Annex D<br/>מנגנונים שתומכים ב-predictability"] --> Mechanism["FIFO_Within_Priorities<br/>Ceiling_Locking<br/>delay until"]
subgraph Requirements["דרישות real-time"]
D["deadline<br/>זמן מוחלט לסיום"]
P["period<br/>מרווח חזרה"]
W["WCET<br/>זמן ריצה גרוע ביותר"]
J["jitter<br/>שונות במחזור"]
end
D --> Analysis["ניתוח schedulability"]
P --> Analysis
W --> Analysis
J --> Analysis
HRT --> Analysis
SRT --> Analysis
Mechanism --> Analysis
Analysis --> Constraint["תנאי הכרחי: WCET <= deadline<br/>תנאי מספיק נבדק בניתוח response time"]
אחת התופעות המסוכנות ביותר במערכת real-time היא priority inversion. הבעיה קרה בפועל ב-Mars Pathfinder ב-1997, וגרמה לגשושית לחזור על reset.
sequenceDiagram
accTitle: איך נוצר priority inversion
accDescr: task בעדיפות נמוכה מחזיק lock, task בעדיפות בינונית עושה לו preemption, ו-task בעדיפות גבוהה נחסם בלי הגבלה.
participant S as scheduler
participant L as task בעדיפות נמוכה
participant H as task בעדיפות גבוהה
participant M as task בעדיפות בינונית
participant R as משאב משותף
L->>R: תופס lock
activate L
Note over L: בתוך critical section
Note over S,L: H מתעורר, וה-scheduler עוצר את L
deactivate L
activate H
H->>R: מנסה לתפוס lock
Note over H: נחסם בהמתנה ל-lock (L עדיין מחזיק)
deactivate H
Note over S,L: H ממתין ל-lock, ולכן L ממשיך
activate L
Note over L: ממשיך לקראת שחרור ה-lock...
Note over S,L: M מתעורר, וה-scheduler עוצר את L
deactivate L
activate M
Note over L: L לא יכול לשחרר את ה-lock
Note over M: M ממשיך לרוץ (גם H וגם L תקועים)
Note over H: [priority inversion] עדיפות גבוהה חסומה בלי הגבלה
deactivate M
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.
sequenceDiagram
accTitle: FIFO_Within_Priorities בין עדיפויות שונות
accDescr: כששני tasks רצים יחד, ה-scheduler בוחר קודם את ה-task בעדיפות הגבוהה, ואחרי delay until הוא חוזר להריץ לפי זמני ה-wakeup.
participant S as scheduler
participant Main as main task
participant HP as task בעדיפות גבוהה<br/>(Priority=Last)
participant LP as task בעדיפות נמוכה<br/>(Priority=First)
Main->>HP: יצירת task
Main->>LP: יצירת task
Note over HP,LP: T=0ms: שני ה-tasks במצב runnable
S->>HP: בוחר את HP, העדיפות הגבוהה ביותר
activate HP
Note over HP: מדפיס לוג התחלה
HP->>S: block עם delay until T+100ms
deactivate HP
S->>LP: מריץ את LP הבא
activate LP
Note over LP: מדפיס לוג התחלה
LP->>S: block עם delay until T+500ms
deactivate LP
Note over S: T=100ms: HP מתעורר
S->>HP: מריץ את HP
activate HP
Note over HP: מדפיס לוג סיום
deactivate HP
Note over S: T=500ms: LP מתעורר
S->>LP: מריץ את LP
activate LP
Note over LP: מדפיס לוג סיום
deactivate LP
Note over Main: (T=800ms) main מסתיים
דוגמת הרצה (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 עובד:
- מגדירים ceiling priority על ה-protected object עם
pragma Priority (Ceiling). - כל task שנכנס ל-protected object עולה אוטומטית ל-ceiling priority בכניסה.
- לכן task בעדיפות בינונית לא יכול לעשות preemption ל-task שנמצא כרגע בתוך ה-protected object.
- ביציאה מה-protected object, העדיפות חוזרת למקורית.
התרשים למטה אינו trace מדויק של זמני הקוד שקדם לו. הוא תרשים מושגי שמראה איך דפוס ה-priority inversion מתרשים 2 נחסם עם Ceiling_Locking.
sequenceDiagram
accTitle: Ceiling_Locking מונע priority inversion
accDescr: task שנכנס ל-protected object עולה ל-ceiling priority, ולכן task בעדיפות בינונית לא יכול לעשות לו preemption.
participant S as scheduler
participant L as task בעדיפות נמוכה<br/>(עדיפות=10)
participant M as task בעדיפות בינונית<br/>(עדיפות=20)
participant H as task בעדיפות גבוהה<br/>(עדיפות=30)
participant PO as protected object<br/>(ceiling priority=30)
Note over PO: קורא עם active priority > ceiling priority מקבל Program_Error<br/>H(30) שווה ל-ceiling (30), ולכן מותר להיכנס
L->>PO: נכנס לפעולה מוגנת
activate L
Note over L,PO: עדיפות הריצה עולה ל-30
Note over S: M מתעורר
Note over S,L: L רץ ב-ceiling priority 30<br/>M(20) לא יכול לעשות preemption
Note over S: H מתעורר
Note over S,H: H(30) עובר את בדיקת ה-ceiling<br/>אבל ממתין כי L משתמש ב-PO
L->>PO: מבצע את הפעולה
L->>PO: יוצא מהפעולה המוגנת
deactivate L
Note over L: העדיפות חוזרת ל-10
Note over S,H: אחרי שחרור PO מריצים את H
activate H
H->>PO: נכנס לפעולה מוגנת
Note over H,PO: H(30) = ceiling (30), ולכן אפשר להיכנס אחרי שהתחרות נגמרה
H->>PO: יוצא מהפעולה המוגנת
deactivate H
כלל תכנון: 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 המחזוריים מכאן והלאה.
flowchart TB
accTitle: delay לעומת delay until
accDescr: delay יחסי צובר drift, delay until נשען על זמן מוחלט, ואם חורגים מהמחזור delay until חוזר מיד ומזהים deadline miss.
subgraph Bad["delay Period - cumulative drift"]
B1["T=0ms: חישוב 15ms"] --> B2["delay 100ms → wakeup ב-115ms"]
B2 --> B3["חישוב 10ms → 125ms"]
B3 --> B4["delay 100ms → wakeup ב-225ms"]
B4 --> B5["מרווחים בפועל: 115ms, 110ms..."]
end
subgraph Good["delay until - לפי זמן מוחלט"]
G1["הבא = T+100ms"] --> G2["חישוב 15ms"]
G2 --> G3["delay until T+100ms → wakeup ב-100ms"]
G3 --> G4["חישוב 10ms"]
G4 --> G5["הבא = T+200ms → wakeup ב-200ms"]
G5 --> G6["מרווחים בפועל: 100ms, 100ms..."]
end
subgraph Overrun["חריגת מחזור - deadline miss"]
O1["הבא = T+100ms"] --> O2["חישוב 130ms"]
O2 --> O3["delay until T+100ms חוזר מיד"]
O3 --> O4["מזהים איחור ומטפלים כעומס יתר"]
end
Bad --> Drift["השגיאה מצטברת עם הזמן"]
Good --> Stable["מונע cumulative drift"]
Good --> Overrun
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.
flowchart TB
accTitle: מגבלות פרופיל Ravenscar
accDescr: פרופיל Ravenscar מצמצם את יכולות ה-tasking של Ada ומחייב FIFO_Within_Priorities ו-Ceiling_Locking כדי לאפשר ניתוח תזמון סטטי.
Full["יכולות tasking מלאות של Ada"] --> Profile["פרופיל Ravenscar"]
Profile --> Restrict["מגבלות"]
Profile --> Policy["מדיניות חובה"]
Restrict --> R1["איסור על יצירת tasks דינמית"]
Restrict --> R2["איסור על משפט select"]
Restrict --> R3["איסור על משפט abort"]
Restrict --> R4["איסור על Task_Attributes"]
Restrict --> R5["הגבלה ל-entry אחד לכל protected object"]
Restrict --> R6["איסור על משפט requeue"]
Restrict --> R7["איסור על delay יחסי<br/>משתמשים ב-delay until"]
Restrict --> R8["איסור על שינוי עדיפות דינמי"]
Restrict --> R9["איסור על סיום task<br/>כל ה-tasks לא-מסתיימים"]
Policy --> P1["FIFO_Within_Priorities"]
Policy --> P2["Ceiling_Locking"]
Restrict --> Benefit["מה שנהיה קל יותר:<br/>static timing analysis"]
Policy --> Benefit
Benefit --> DO178["DO-178C<br/>תוכנת תעופה"]
Benefit --> ISO26262["ISO 26262<br/>בטיחות פונקציונלית ברכב"]
Benefit --> IEC62304["IEC 62304<br/>תוכנת ציוד רפואי"]
כדי להפעיל את פרופיל 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.
stateDiagram-v2
accTitle: מצבי bounded buffer
accDescr: protected object עם buffer מוגבל עובר בין ריק, חלקי ומלא, ו-Get או Put נחסמים לפי תנאי ה-barrier.
Empty: ריק / Count=0
Partial: חלקי / Count=1..Buffer_Size-1
Full: מלא / Count=Buffer_Size
[*] --> Empty: מצב התחלתי
Empty --> Partial: Put (מוסיפים איבר אחד)
Partial --> Partial: Put / Get
Partial --> Empty: Get (מוציאים את האיבר האחרון)
Partial --> Full: Put (ממלאים את המקום הפנוי האחרון)
Full --> Partial: Get (נוצר מקום פנוי)
Empty --> Empty: Get נחסם (barrier Count=0)
Full --> Full: Put נחסם (barrier 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 ותחרות על זיכרון דורש בנפרד ניתוח סטטי או אימות על סביבת היעד.
flowchart LR
accTitle: wall-clock time מול CPU time
accDescr: Ada.Real_Time.Clock מודד זמן שעבר כולל המתנה, ו-Ada.Execution_Time.Clock מודד רק זמן CPU בפועל.
subgraph Wall["wall-clock time"]
W1["זמן שעבר כולל: 500ms"] --> W2["כולל: חישוב + המתנה + block + preemption"]
end
subgraph CPU["CPU time"]
C1["CPU כולל: 120ms"] --> C2["כולל: חישוב בפועל בלבד"]
end
Wall --> Diff["ההפרש = זמן המתנה, block ו-preemption"]
CPU --> Diff
Diff --> Insight["CPU time צופה את עלות החישוב בפועל<br/>עוזר באימות WCET ובניטור<br/>מוציא מחוץ לחשבון המתנה, block ו-preemption"]
Insight --> Caveat["שים לב<br/>מדידה אינה ערובה ל-WCET האמיתי<br/>צריך ניתוח סטטי או אימות על היעד"]
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.
flowchart TB
accTitle: דוגמת תזמון רב-מחזורית
accDescr: חיישן מהיר במחזור 100ms עושה preemption ללולאת בקרה איטית במחזור 400ms, לפי הנחות הסבר בלבד.
Assumption["הנחה להסבר<br/>חיישן מהיר: עיבוד 10ms<br/>בקרה איטית: עיבוד 80ms"]
subgraph Cycle1["בקרה איטית, מחזור 1 (release=150ms)"]
direction LR
C1F1["100-110ms<br/>חיישן מהיר #1"] --> C1S1["150-200ms<br/>בקרה איטית #1, חלק א"]
C1S1 --> C1F2["200-210ms<br/>חיישן מהיר #2<br/>P+3, לכן preemption"]
C1F2 --> C1S2["210-240ms<br/>בקרה איטית #1, חלק ב"]
end
subgraph Cycle2["בקרה איטית, מחזור 2 (release=550ms)"]
direction LR
C2S1["550-600ms<br/>בקרה איטית #2, חלק א"] --> C2F6["600-610ms<br/>חיישן מהיר #6<br/>P+3, לכן preemption"]
C2F6 --> C2S2["610-640ms<br/>בקרה איטית #2, חלק ב"]
end
subgraph Cycle3["בקרה איטית, מחזור 3 (release=950ms)"]
direction LR
C3S1["950-1000ms<br/>בקרה איטית #3, חלק א"] --> C3F10["1000-1010ms<br/>חיישן מהיר #10<br/>P+3, לכן preemption"]
C3F10 --> C3S2["1010-1040ms<br/>בקרה איטית #3, חלק ב"]
end
Assumption --> C1F1
C1S2 --> C2S1
C2S2 --> C3S1
הדפוס הזה — “איסוף חיישן מהיר + לולאת בקרה איטית” — הוא מבנה טיפוסי בבקרה תעשייתית ובבקרת רובוטים.
11. איפה יכולות real-time של Ada בולטות
יכולות real-time של Ada מביאות ערך במיוחד בתחומים האלה.
flowchart TB
accTitle: תחומים שבהם Ada real-time בולט
accDescr: Annex D רלוונטי לתעופה וחלל, רכבות, רכב, ציוד רפואי, בקרה תעשייתית ומערכות ביטחוניות.
Ada["Ada Annex D<br/>יכולות real-time"] --> Aero["תעופה וחלל<br/>DO-178C"]
Ada --> Rail["רכבות<br/>משפחת EN 50128"]
Ada --> Auto["רכב<br/>ISO 26262"]
Ada --> Medical["ציוד רפואי<br/>IEC 62304"]
Ada --> Industrial["בקרה תעשייתית<br/>משפחת IEC 61508"]
Ada --> Defense["ביטחון ומערכות high-integrity"]
Aero --> A1["בקרת טיסה<br/>תחום יישום נפוץ"]
Aero --> A2["בקרת לוויינים וחלליות"]
Rail --> R1["מערכות איתות"]
Rail --> R2["בקרת רכבות אוטומטית"]
Auto --> Au1["מועמד ל-ECU הקשור לבטיחות"]
Auto --> Au2["שימוש מוגבל וסלקטיבי<br/>בתחום שבו C / MISRA-C דומיננטיים"]
Medical --> M1["קוצבי לב"]
Medical --> M2["משאבות עירוי"]
Industrial --> I1["בקרת רובוטים"]
Industrial --> I2["מכונות CNC"]
Defense --> D1["mission computers"]
Defense --> D2["מערכות להפעלה ארוכת טווח"]
בתרשים, רק תחום הרכב מסומן כ”שימוש מוגבל וסלקטיבי”. הסיבה העיקרית אינה התאמת השפה עצמה, אלא גודל ה-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. מקורות
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
Concurrency בטוח ב-Ada — מדריך מעשי ל-task ול-protected object
מאמר מבוא ל-concurrency שמובנה בשפת Ada עצמה: task ו-protected object. נעבור על rendezvous (entry/accept), selective accept, mutual exclu...
Generic programming ב-Ada — חוזה בטיפוסים ו-reuse ב-zero cost
סקירה שיטתית של generic programming ב-Ada: generic subprograms, generic packages, formal subprograms, קטגוריות טיפוס, ועקרונות עיצוב מעשי...
Windows Virtualization Internals (חלק 3) — VM שעולה תוך שניות: WSL2, Windows Sandbox ו-containers
למה WSL2 ו-Windows Sandbox עולים בשניות ומרגישים קלים? המאמר מסביר את המנגנונים, מ-dynamic base image ו-direct map דרך הקצאת זיכרון דינמי...
Windows Virtualization Internals (חלק 2) — זיכרון שאפילו ה-kernel לא רואה: VBS, HVCI ו-Credential Guard
בהתקנה נקייה על חומרה תואמת, VBS מופעל כברירת מחדל ומשתמש ב-hypervisor וב-SLAT כדי ליצור בידוד חזק מה-kernel. המאמר מסביר את המבנה של VTL...
Windows Virtualization Internals (חלק 1) — איפה Windows שלכם באמת רץ: hypervisor ו-partitions
כשמפעילים Hyper-V, Windows המארח עצמו רץ מעל ה-hypervisor כ-root partition. המאמר מסביר את יסודות הווירטואליזציה דרך התפקידים של VT-x, SL...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מה זה 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 ומטפל בזה כעומס יתר.