โปรแกรมมิงระบบเรียลไทม์ด้วย Ada — ลำดับความสำคัญ คาบ และการควบคุมเวลาประมวลผลในทางปฏิบัติ
· อัปเดตเมื่อ: · Go Komura · Ada, RealTime, Ravenscar, CeilingLocking, Tasking, Scheduling, PriorityInversion, ProgrammingLanguage, เรียลไทม์, ความน่าเชื่อถือสูง
1. บทนำ — ความสัมพันธ์ลึกระหว่าง Ada กับเรียลไทม์
บทความก่อนหน้า «การทำงานแบบ concurrent ที่ปลอดภัยใน Ada» อธิบายพื้นฐานการทำงานแบบ concurrent ที่ปลอดภัยด้วย task และ protected object ของ Ada คราวนี้เราเดินต่อบนเส้นนั้น เข้าสู่โดเมนที่ข้อจำกัดเข้มกว่า — ระบบเรียลไทม์
ในระบบเรียลไทม์ ความ «ถูกต้อง» ไม่ได้หมายแค่ผลคำนวณที่ถูกทางตรรกะ แต่รวมถึง ได้ผลนั้นภายในกำหนดเวลา ด้วย คำตอบที่ถูกแต่มาช้า 1 มิลลิวินาที อันตรายพอ ๆ กับคำตอบที่ผิด
Ada ตอบข้อกำหนดนี้ด้วยกลุ่มฟีเจอร์เรียลไทม์ที่ครอบคลุม ซึ่งถูกมาตรฐานไว้เป็น Annex D (Real-Time Systems) ของข้อกำหนดภาษา ไม่ใช่ «ต่อท้ายด้วยไลบรารี» แต่เป็น การรับประกันเรียลไทม์ที่ฝังใน language runtime เอง
ฟีเจอร์เรียลไทม์ของ Ada (Annex D):
- ลำดับความสำคัญของ task และการ preempt (FIFO_Within_Priorities)
- โปรโตคอล Ceiling_Locking (กัน priority inversion)
- การทำงานแบบคาบด้วยเวลาสัมบูรณ์ผ่าน delay until
- โปรไฟล์ Ravenscar (subset สำหรับงานที่ความปลอดภัยสำคัญ)
- timing event (ปลุกตามเวลาโดยไม่ต้อง poll)
- การเฝ้าเวลาประมวลผล (Ada.Execution_Time)
- การจัดตารางหลายคาบ
บทความนี้ไล่ฟีเจอร์เหล่านี้ทีละขั้นด้วยตัวอย่างโค้ดเชิงปฏิบัติ 8 ชุด แต่ละ snippet ใช้เป็นตัวอย่างเดี่ยวได้ แต่ตัวอย่าง 04/05 ที่มีหลายหน่วยคอมไพล์ ให้แยกด้วย gnatchop แล้วค่อย gnatmake
ผู้อ่านเป้าหมายและความรู้พื้นฐาน: สมมติว่าอ่านพื้นฐาน task, rendezvous และ protected object จากบทความก่อนแล้ว มุ่งนักพัฒนาที่สนใจซอฟต์แวร์ควบคุมของอุปกรณ์ฝังตัวหรือระบบความน่าเชื่อถือสูง และไม่ใช่บทนำไวยากรณ์ Ada เอง
สภาพแวดล้อมที่ใช้ตรวจยืนยัน: ยืนยันแล้วว่าตัวอย่างทั้ง 8 ในบทความนี้สร้างได้ด้วย GNAT 13.3.0 (Ubuntu 24.04, x86-64) ผลรันที่ลงในเนื้อหา (บทที่ 3 และบทที่ 5) ก็เก็บจากสภาพแวดล้อมเดียวกัน ลำดับความสำคัญและการ preempt จะมีผลจริงอย่างไรขึ้นกับ OS และ GNAT runtime (บทที่ 12) ดังนั้นรายละเอียดของผลลัพธ์ย่อมต่างตามสภาพแวดล้อม
อนึ่ง ชิ้นโค้ดในบทความนี้เผยแพร่บน GitHub เป็นชุดอ้างอิงที่จัดเป็นไฟล์ตามบท
ada-real-time-systems - komurasoft-blog-samples (GitHub)
แผนที่ความรู้ของบทความนี้
ข้อกำหนดภาษา Ada ภาค Annex D ฝังลงใน language runtime เองตั้งแต่ FIFO_Within_Priorities ตามลำดับความสำคัญของ task, โปรโตคอล Ceiling_Locking ที่กำหนด ceiling priority ให้ protected object อัตโนมัติ, การทำงานแบบคาบด้วย delay until ที่กัน cumulative drift, โปรไฟล์ Ravenscar ที่วิเคราะห์แบบ static ได้ง่าย, timing event ที่ไม่ต้อง poll ไปจนถึงการวัดเวลาประมวลผลต่อ task Ceiling_Locking กัน priority inversion ที่เกิดจริงกับ Mars Pathfinder ปี 1997 จนยานสำรวจรีเซ็ตซ้ำ โดยยก task ที่เข้า protected object ไป ceiling priority อัตโนมัติ โปรไฟล์ Ravenscar ห้าม delay แบบสัมพัทธ์และคำสั่ง select เป็นต้น เพื่อจำกัด task ของ Ada ให้เหลือ subset ที่วิเคราะห์ timing แบบ static ได้ง่าย และใน GNAT เปิดใช้โดยเขียน pragma ในไฟล์ gnat.adc อย่างไรก็ตามการแม็ปลำดับความสำคัญจริงขึ้นกับ OS และ GNAT runtime
flowchart LR
accTitle: แผนที่ความรู้โปรแกรมมิงระบบเรียลไทม์ของ Ada
accDescr: แผนภาพที่แสดงว่า Ada Annex D ฝังการ dispatch ตามลำดับความสำคัญ, Ceiling_Locking, delay until, โปรไฟล์ Ravenscar, timing event และการวัดเวลาประมวลผล ลงบน task กับ protected object อย่างไร และ Ceiling_Locking กัน priority inversion อย่างที่เกิดกับ Mars Pathfinder ได้อย่างไร
ada_annex_d["Annex D (มาตรฐานระบบเรียลไทม์ของ Ada)"]
ceiling_locking["โปรโตคอล Ceiling_Locking"]
fifo_within_priorities["FIFO_Within_Priorities"]
ada_delay_until["delay until (หน่วงถึงเวลาสัมบูรณ์)"]
ravenscar_profile["โปรไฟล์ Ravenscar"]
ada_timing_events["timing event (Ada.Real_Time.Timing_Events)"]
ada_execution_time["Ada.Execution_Time (การวัดเวลาประมวลผล)"]
ada_task["task ของ Ada (การประมวลผลพร้อมกัน)"]
protected_object["อ็อบเจ็กต์ป้องกัน (protected object)"]
gnat["GNAT"]
priority_inversion["การกลับลำดับความสำคัญ (priority inversion)"]
mars_pathfinder_priority_inversion["อุบัติเหตุการกลับลำดับความสำคัญของ Mars Pathfinder"]
ada_relative_delay["delay แบบสัมพัทธ์ (คำสั่ง delay)"]
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
ในแผนภาพ เส้นทึบหมายถึงความสัมพันธ์ที่ถือเสมอ และเส้นประหมายถึงความสัมพันธ์ที่มีเงื่อนไข (เงื่อนไขอยู่ในการอธิบายของแต่ละความสัมพันธ์ในหน้าละเอียด) รายการความสัมพันธ์ทั้งหมด (รวม 21 รายการ พร้อมหลักฐานและระดับความเชื่อมั่น) และนิยามของแนวคิดหลักรวบรวมไว้ที่ หน้าละเอียดของแผนที่ความรู้ (เป็นภาษาญี่ปุ่น) ข้อมูล: JSON-LD / Turtle
2. ระบบเรียลไทม์คืออะไร
เริ่มจากจัดระเบียบศัพท์ก่อน
| แนวคิด | คำอธิบาย |
|---|---|
| Hard real-time | พลาด deadline หมายถึงความล้มเหลวร้ายแรงของระบบ (ควบคุมการบิน, ถุงลมนิรภัย, เครื่องกระตุ้นหัวใจ) |
| Soft real-time | พลาด deadline ไม่พึงประสงค์ แต่พลาดเป็นครั้งคราวพอรับได้ (สตรีมวิดีโอ, เกม) |
| Deadline | เวลาสัมบูรณ์ที่ task ต้องเสร็จ |
| Period (คาบ) | ช่วงเวลาที่ task ถูกปลุกซ้ำ |
| WCET (Worst-Case Execution Time) | เวลาประมวลผลกรณีเลวสุดของ task |
| Jitter | ความแกว่งของการทำงานแบบคาบ |
| Schedulability | คุณสมบัติที่ว่า «ชุด task นั้นรันโดยรักษา deadline ทุกตัวได้หรือไม่» งานตรวจบนกระดาษคือการวิเคราะห์ schedulability (เช่น response-time analysis) |
| โปรไฟล์ Ravenscar | กติกาที่จำกัดฟีเจอร์ task ของ Ada ให้เหลือ subset ที่วิเคราะห์แบบ static ได้ง่าย ชื่อมาจากหมู่บ้าน Ravenscar ในสหราชอาณาจักรที่เป็นที่ประชุมกำหนดกติกา (บทที่ 6) |
ในการออกแบบระบบเรียลไทม์ เงื่อนไขจำเป็นสำคัญคือแต่ละ task เป็นจริงตาม «WCET <= deadline» แต่แค่นั้นยังไม่รับประกันว่าระบบทั้งก้อนจะทันกำหนด ยังต้องมี response-time analysis ที่รวม blocking time, การกำหนดลำดับความสำคัญ, jitter, อินเทอร์รัปต์ และพฤติกรรมของ runtime กับ OS แยกต่างหาก ในทางปฏิบัติมักตั้งเป้า WCET < deadline เพื่อเหลือระยะเผื่อ ฟีเจอร์เรียลไทม์ของ Ada ให้ โมเดลการรันที่คาดเดาได้ ในระดับภาษา ซึ่งทำให้วิเคราะห์นั้นง่ายขึ้น
flowchart LR
accTitle: ข้อกำหนดระบบเรียลไทม์กับการวิเคราะห์ schedulability
accDescr: แผนภาพที่แสดงว่า hard real-time, soft real-time, deadline, period, WCET, jitter และกลไกของ Ada Annex D ไหลเข้าสู่การวิเคราะห์ schedulability อย่างไร
HRT[Hard real-time] -->|พลาด deadline = ความล้มเหลวร้ายแรง| Examples[ควบคุมการบิน<br/>ถุงลมนิรภัย<br/>เครื่องกระตุ้นหัวใจ]
SRT[Soft real-time] -->|พลาดเป็นครั้งคราวได้| Examples2[สตรีมวิดีโอ<br/>เกม<br/>UI]
Ada[Ada Annex D<br/>กลไกที่ค้ำจุนความคาดเดาได้] --> Mechanism[FIFO_Within_Priorities<br/>Ceiling_Locking<br/>delay until]
subgraph Requirements[ข้อกำหนดเรียลไทม์]
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 analysis]
หนึ่งในปรากฏการณ์ที่อันตรายที่สุดในระบบเรียลไทม์คือ priority inversion ปัญหานี้เกิดจริงกับ Mars Pathfinder ปี 1997 และเป็นสาเหตุที่ยานสำรวจรีเซ็ตซ้ำ
sequenceDiagram
accTitle: ลำดับเหตุการณ์ของ priority inversion
accDescr: แผนภาพลำดับที่แสดง task ลำดับความสำคัญต่ำถือ lock แล้วถูก preempt โดย task ลำดับความสำคัญกลาง ทำให้ 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 ลำดับความสำคัญต่ำถูก preempt โดย task ลำดับความสำคัญกลางขณะยังถือ lock อยู่ ทำให้ task ลำดับความสำคัญสูงถูกบล็อกไม่มีกำหนด มาตรการที่ Mars Pathfinder ทำจริงคือเปิด priority inheritance ของ VxWorks แต่ Ada จัดการปัญหาตระกูลเดียวกันด้วยวิธีอีกแบบ คือ Ceiling_Locking ซึ่งให้มาเป็นฟีเจอร์ของภาษา
3. พื้นฐานลำดับความสำคัญของ task — FIFO_Within_Priorities
FIFO_Within_Priorities คือนโยบาย dispatch ตามลำดับความสำคัญมาตรฐานที่ Ada Annex D ให้ระบุได้ หากไม่ระบุนโยบาย พฤติกรรมเริ่มต้นเป็น implementation-defined แต่ GNAT ใช้ตระกูลนโยบายนี้บนหลายเป้าหมาย ภายในลำดับความสำคัญเดียวกันรันแบบ FIFO (เข้าก่อนออกก่อน) และ task ลำดับความสำคัญสูงกว่าจะ preempt (แย่ง) task ลำดับความสำคัญต่ำกว่า
-- 01_task_priority.ada
-- รูปแบบพื้นฐานของลำดับความสำคัญของ task และ FIFO_Within_Priorities
-- configuration pragma ต้องอยู่ก่อน 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;
จุดที่ควรจำ:
pragma Priorityให้ลำดับความสำคัญแบบ static แก่แต่ละ taskPriority'LastสูงสุดPriority'Firstต่ำสุด- ตัว task ในเดโมนี้ไม่ใช่การคำนวณหนัก แต่รอจนถึงเวลาที่กำหนดด้วย
delay untilสิ่งที่อยากยืนยันคือ เมื่อพร้อมรันพร้อมกัน task ลำดับความสำคัญสูงได้โอกาสรันก่อน - แผนภาพนี้แสดงด้าน preemption ระหว่างลำดับความสำคัญต่างกันของ
FIFO_Within_Prioritiesหากจะยืนยันลำดับ FIFO ภายในลำดับความสำคัญเดียวกัน ต้องมีตัวอย่างอีกชุดที่วางหลาย task ที่ลำดับความสำคัญเท่ากัน - ในระบบจริงมักออกแบบลำดับความสำคัญแบบสัมพัทธ์โดยยึด
System.Default_Priorityเป็นฐาน
sequenceDiagram
accTitle: การสาธิต FIFO_Within_Priorities
accDescr: แผนภาพลำดับที่แสดง scheduler เลือก task ลำดับความสำคัญสูงก่อน แล้วให้ task ลำดับความสำคัญต่ำรันระหว่างที่ฝ่ายสูงถูกบล็อกด้วย delay until
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: ทั้งสอง task เป็น runnable
S->>HP: เลือก HP ซึ่งลำดับความสำคัญสูงสุด
activate HP
Note over HP: พิมพ์ล็อกเริ่มต้น
HP->>S: บล็อกด้วย delay until T+100ms
deactivate HP
S->>LP: รัน LP ต่อ
activate LP
Note over LP: พิมพ์ล็อกเริ่มต้น
LP->>S: บล็อกด้วย 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) เมนจบ
ตัวอย่างการรัน (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
สังเกตว่าล็อกเริ่มต้นของสอง task ออกมาก่อนบรรทัดหัวข้อของเมน เพราะ task ที่ประกาศในส่วนประกาศจะถูก activate ก่อนเข้าตัวของซับโปรแกรมที่ครอบอยู่ จึงเรียงแบบนี้ได้ และ ลำดับสองบรรทัดล็อกเริ่มต้นนี้อาจสลับกันได้ทุกรอบที่รัน ตามบทที่ 12 การที่ pragma Priority สะท้อนสู่การจัดตารางจริงอย่างไรขึ้นกับ OS และ GNAT runtime ดังนั้นบน Linux ทั่วไปจึงไม่รับประกันลำดับสตาร์ตตามลำดับความสำคัญ สิ่งที่สังเกตได้อย่างเสถียรในตัวอย่างนี้คือลำดับการเสร็จงานที่กำหนดด้วย delay until ที่ 0.1 วินาที / 0.5 วินาที
ช่วงลำดับความสำคัญของ Ada (ค่าเริ่มต้นของ GNAT):
Priority'First = 0 (ต่ำสุด)
Priority'Last = 30 (สูงสุด แต่ขึ้นกับ OS)
4. Ceiling_Locking — ภาษาป้องกัน priority inversion ให้เอง
หนึ่งในปัญหาที่ยุ่งที่สุดในระบบเรียลไทม์คือ priority inversion ปรากฏการณ์ที่ task ลำดับความสำคัญสูงรอ lock ที่ task ลำดับความสำคัญต่ำถืออยู่ แล้ว task ลำดับความสำคัญต่ำนั้นถูก preempt โดย task ลำดับความสำคัญกลาง ทำให้ฝ่ายสูงถูก บล็อกไม่มีกำหนด
Ada จัดการปัญหานี้ด้วย โปรโตคอล Ceiling_Locking ที่ฝังใน protected object โดยตรง
-- 02_ceiling_locking.ada
-- กัน priority inversion ด้วยโปรโตคอล Ceiling_Locking
-- configuration pragma ต้องอยู่ก่อน 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;
กลไกของ Ceiling_Locking:
- ตั้ง ceiling priority ให้ protected object ด้วย
pragma Priority (Ceiling) - task ใดเข้า protected object จะ ถูกยกไป ceiling priority อัตโนมัติตอนเข้า
- ด้วยเหตุนี้ task ลำดับความสำคัญกลางจึง preempt task ที่กำลังใช้ protected object ไม่ได้
- เมื่อออกจาก protected object ลำดับความสำคัญกลับสู่ค่าเดิม
แผนภาพด้านล่างไม่ใช่เทรซเวลาแบบตรงตัวของโค้ดตัวอย่างทันทีก่อนหน้า แต่เป็นแผนภาพแนวคิดว่าแบบแผน priority inversion ในแผนภาพที่ 2 ถูก Ceiling_Locking กดได้อย่างไร
sequenceDiagram
accTitle: Ceiling_Locking ป้องกัน priority inversion
accDescr: แผนภาพลำดับที่แสดง task ที่เข้า protected object ถูกยกไป ceiling priority จึงไม่ถูก preempt โดย task ลำดับความสำคัญกลาง
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 ได้ Program_Error<br/>H(30) ในภาพเท่ากับ ceiling(30) จึงเข้าได้
L->>PO: เข้า protected operation
activate L
Note over L,PO: ลำดับความสำคัญขณะรันถูกยกเป็น 30
Note over S: M ปลุก
Note over S,L: L กำลังรันที่ ceiling 30<br/>M(20) preempt ไม่ได้
Note over S: H ปลุก
Note over S,H: H(30) ผ่านการตรวจ ceiling<br/>แต่รอเพราะ L กำลังใช้ PO
L->>PO: ทำงานของ operation
L->>PO: ออกจาก protected operation
deactivate L
Note over L: ลำดับความสำคัญกลับเป็น 10
Note over S,H: หลังปล่อย PO แล้วรัน H
activate H
H->>PO: เข้า protected operation
Note over H,PO: H(30) = ceiling(30) จึงเข้าได้หลังข้อขัดแย้งคลี่คลาย
H->>PO: ออกจาก protected operation
deactivate H
หลักออกแบบ: ceiling priority ของ protected object ต้องตั้ง ไม่ต่ำกว่าลำดับความสำคัญสูงสุดของทุก task ที่ใช้มัน หากทำลายกฎนี้ แล้ว task ที่ active priority สูงกว่า ceiling เรียก protected operation Ada ตรวจความผิดพลาดของดีไซน์ได้ด้วย
Program_Error
ใน C หากจะได้ผลเทียบเท่า ต้องตั้งแอตทริบิวต์ PTHREAD_PRIO_PROTECT ของ pthread mutex เอง แต่ใน Ada นี่คือฟีเจอร์มาตรฐานของภาษา
5. delay until — รัน periodic task โดยไม่ให้ drift
แพทเทิร์นพื้นฐานของระบบเรียลไทม์คือ periodic task ใน task ที่รันซ้ำเป็นช่วงคงที่ การกันข้อผิดพลาดของจังหวะที่สะสม (drift) สำคัญมาก
delay until ของ Ada แก้ปัญหานี้อย่างสง่างาม
-- 03_periodic_task.ada
-- periodic 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; |
ยึดเวลาสัมบูรณ์ ดังนั้นแม้รอบหนึ่งจะช้า เวลาปลุกครั้งถัดไปยังคงถูกต้อง |
อย่างไรก็ตาม delay until ไม่ได้รับประกันให้อัตโนมัติว่าเวลาประมวลผลจะอยู่ในคาบ หากงานเลยเวลาปลุกครั้งถัดไป 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
แต่ละรอบมีดีเลย์ตอนปลุกราว 0.2–0.3 มิลลิวินาที แต่ ดีเลย์นั้นไม่ได้ถูกยกไปคาบถัดไป อ่านออกได้ แม้รอบที่ 5 ค่าเพี้ยนจากเวลาฐานก็ยังต่ำกว่า 1 มิลลิวินาที หากเขียนด้วย delay Period; ดีเลย์นี้จะกองทุกรอบ และหลัง 5 รอบจะเห็นส่วนต่างชัด อนึ่งตัวเลขนี้เป็นตัวอย่างบน Linux ทั่วไป ไม่ใช่ค่าที่รับประกันในสภาพแวดล้อม hard real-time
แพทเทิร์น delay until นี้ใช้กับ periodic task ทุกตัวจากนี้ไป
flowchart TB
accTitle: ความต่างระหว่าง delay กับ delay until
accDescr: แผนภาพที่เปรียบเทียบ cumulative drift ของ delay แบบสัมพัทธ์ กับ delay until ที่ยึดเวลาสัมบูรณ์ และกรณี deadline miss เมื่อเกินคาบ
subgraph Bad["delay Period - cumulative drift"]
B1[T=0ms: คำนวณ 15ms] --> B2[delay 100ms → ปลุกที่ 115ms]
B2 --> B3[คำนวณ 10ms → 125ms]
B3 --> B4[delay 100ms → ปลุกที่ 225ms]
B4 --> B5[ช่วงจริง: 115ms, 110ms...]
end
subgraph Good["delay until - ยึดเวลาสัมบูรณ์"]
G1[ครั้งถัดไป = T+100ms] --> G2[คำนวณ 15ms]
G2 --> G3[delay until T+100ms → ปลุกที่ 100ms]
G3 --> G4[คำนวณ 10ms]
G4 --> G5[ครั้งถัดไป = T+200ms → ปลุกที่ 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 — subset เรียลไทม์ที่พิสูจน์ได้
ฟีเจอร์ task ของ Ada ทรงพลัง แต่ในระบบที่ความปลอดภัยสำคัญมาก ความ «ทรงพลังเกินไป» กลายเป็นปัญหา การสร้าง task แบบไดนามิก, คำสั่ง select, คำสั่ง abort ทำให้วิเคราะห์เวลาประมวลผลกรณีเลวสุดแบบ static ยาก
โปรไฟล์ Ravenscar คือคำตอบที่ Ada ให้ต่อปัญหานี้ โดยจำกัดฟีเจอร์ task ให้เหลือ subset ที่วิเคราะห์แบบ static ได้และเป็น deterministic
-- 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:
| ฟีเจอร์ที่ห้าม | เหตุผล |
|---|---|
การสร้าง task แบบไดนามิก (new หรือ access type) |
การจัดสรรหน่วยความจำตอนรันไม่เป็น deterministic |
คำสั่ง select |
ไม่ใช่แค่ทางเลือกหลายทาง ทั้งคำสั่ง select ทำให้วิเคราะห์ control flow ยาก |
คำสั่ง abort |
การตัดแบบอะซิงโครนัสทำให้สถานะคาดเดาไม่ได้ |
Ada.Task_Attributes |
พฤติกรรมไดนามิกตอนรัน |
| การเปลี่ยนลำดับความสำคัญแบบไดนามิก | สมมติฐานของการวิเคราะห์การจัดตารางเปลี่ยนตอนรัน |
delay แบบสัมพัทธ์ (delay) |
ก่อ cumulative drift ได้ง่าย จึงใช้ delay until แบบเวลาสัมบูรณ์ |
| หลาย entry ต่อ protected object | เงื่อนไขบล็อกและกรณีที่ต้องวิเคราะห์เพิ่มขึ้น |
| การจบของ task | Ravenscar ถือว่าทุก task ไม่จบ |
คำสั่ง requeue |
การตาม control flow ซับซ้อนขึ้น |
ด้วยข้อจำกัดนี้ โปรแกรมที่ตาม Ravenscar จะอยู่ในรูปที่ วิเคราะห์ timing แบบ static ได้ง่ายขึ้น นี่คือคุณสมบัติที่มาตรฐานความปลอดภัยอย่าง DO-178C (ซอฟต์แวร์อากาศยาน) และ ISO 26262 (ความปลอดภัยเชิงฟังก์ชันของยานยนต์) ต้องการ รายการด้านล่างเป็นสารสกัดของข้อจำกัดหลัก โปรไฟล์จริงยังมีกฎเพิ่มเรื่อง runtime และการวิเคราะห์ได้ เช่น No_Task_Hierarchy และ Detect_Blocking
flowchart TB
accTitle: ข้อจำกัดและนโยบายของโปรไฟล์ Ravenscar
accDescr: แผนภาพที่แสดงว่าโปรไฟล์ Ravenscar จำกัด tasking ของ Ada ให้วิเคราะห์ timing แบบ static ได้ง่ายขึ้น และสัมพันธ์กับมาตรฐานความปลอดภัย
Full[ฟีเจอร์ task ของ Ada ทั้งชุด] --> Profile[โปรไฟล์ Ravenscar]
Profile --> Restrict[ข้อจำกัด]
Profile --> Policy[นโยบายที่บังคับ]
Restrict --> R1[ห้ามสร้าง task แบบไดนามิก]
Restrict --> R2[ห้ามคำสั่ง select]
Restrict --> R3[ห้ามคำสั่ง abort]
Restrict --> R4[ห้าม Task_Attributes]
Restrict --> R5[จำกัด 1 entry ต่อ protected object]
Restrict --> R6[ห้ามคำสั่ง requeue]
Restrict --> R7[ห้าม delay แบบสัมพัทธ์<br/>ใช้ delay until]
Restrict --> R8[ห้ามเปลี่ยนลำดับความสำคัญแบบไดนามิก]
Restrict --> R9[ห้ามการจบของ task<br/>ทุก task ไม่จบ]
Policy --> P1[FIFO_Within_Priorities]
Policy --> P2[Ceiling_Locking]
Restrict --> Benefit[สิ่งที่ทำได้ง่ายขึ้น:<br/>การวิเคราะห์ timing แบบ static]
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 event — ปลุกตามเวลาโดยไม่ต้อง poll
ระบบเรียลไทม์จำนวนมากมีข้อกำหนดซ้ำ ๆ ว่า «เมื่อถึงเวลาที่กำหนด ให้ปลุก task ลำดับความสำคัญสูง» การทำแบบตรงไปตรงมาคือ poll ตัวจับเวลา แต่ Ada มีกลไกที่ประณีตกว่า — timing event
-- 05_timing_events.ada
-- Timing event (Ada.Real_Time.Timing_Events)
-- กลไกที่ปลุก task ลำดับความสำคัญสูงโดยไม่ต้อง poll
-- 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 event:
1. Set_Handler(Timer_1, T+100ms, S.Fire'Access) — ลงทะเบียนแฮนด์เลอร์ที่เวลาสัมบูรณ์
2. ครบ T+100ms — runtime เรียก S.Fire ที่ **ceiling priority**
3. Fire ตั้งแฟล็ก Fired เป็น True — barrier เปิด
4. task Reactor ปลุกจาก Wait_For_Event
สิ่งสำคัญคือตัวอย่างนี้ระบุ Ceiling_Locking ชัด และเพราะแฮนด์เลอร์ Fire เป็นโพรซีเดอร์ของ protected object จึง รันที่ ceiling priority โพรซีเดอร์แบบ protected ที่ใช้เป็นแฮนด์เลอร์ของ timing event ให้อยู่ใน protected object ที่มี ceiling ระดับอินเทอร์รัปต์ ในที่นี้คือ System.Interrupt_Priority'Last ด้วยเหตุนี้จึงไม่เกิด priority inversion ระหว่างประมวลผล timing event
8. คิวเรียลไทม์ด้วย protected object
แพทเทิร์นที่พบบ่อยในระบบเรียลไทม์คือ producer-consumer เซ็นเซอร์สร้างข้อมูล แล้ว task ควบคุมบริโภค — ตอนนั้นต้องทำ mutual exclusion ของบัฟเฟอร์และการบล็อกให้มีประสิทธิภาพ
หากใช้ protected object และ entry barrier ของ Ada จะลงเป็นซิงค์แบบ barrier ได้ ภายใน runtime จัดการ mutual exclusion ให้ จึงไม่ต้องเขียน mutex หรือ condition variable ตรงในโค้ดแอปพลิเคชัน
-- 06_protected_queue.ada
-- แชร์ข้อมูลเรียลไทม์ด้วย protected object
-- ไปป์ไลน์: 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— ถ้าบัฟเฟอร์เต็ม Producer จะถูก บล็อกอัตโนมัติentry Get when Count > 0— ถ้าบัฟเฟอร์ว่าง Consumer จะถูก บล็อกอัตโนมัติpragma Priority (System.Any_Priority'Last)— ด้วย ceiling locking จะ ไม่เกิด priority inversion ระหว่าง Producer กับ Consumer- เงื่อนไข barrier นิยามจากสถานะภายในของ protected object (
Count) และถูกประเมินใหม่เองตอนปล่อยล็อก
โค้ดนี้ไม่มี mutex, semaphore หรือ condition variable ฝั่งแอปพลิเคชัน การรอที่ต้องใช้แสดงด้วย entry barrier ของ protected object
stateDiagram-v2
accTitle: สถานะของ bounded buffer แบบ protected object
accDescr: แผนภาพสถานะ Empty, Partial, Full ของคิว และเงื่อนไข barrier ที่บล็อก Put หรือ Get
Empty: ว่าง / Count=0
Partial: มีบางส่วน / Count=1..Buffer_Size-1
Full: เต็ม / Count=Buffer_Size
[*] --> Empty: สถานะเริ่มต้น
Empty --> Partial: Put (เพิ่ม 1 element)
Partial --> Partial: Put / Get
Partial --> Empty: Get (เอา element สุดท้ายออก)
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 ถูกประเมินใหม่ การประเมินนี้เกิดตอน protected operation จบ ไม่ว่าแผนภาพจะอยู่ในสถานะใด
9. การวัดเวลาประมวลผล — ก้าวแรกของการเฝ้า execution time
หากจะประเมินว่าจัดตารางระบบเรียลไทม์ได้หรือไม่ ต้องรู้ เวลาประมวลผล (CPU time) ของแต่ละ task ให้แม่น แพ็กเกจ Ada.Execution_Time ของ Ada ให้เวลาที่ใช้ 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
→ เวลาที่ผ่านไปจริง รวมช่วงที่ถูกบล็อกหรือถูก preempt
CPU time (execution time): Ada.Execution_Time.Clock
→ เฉพาะเวลาที่ task นั้นรันบน CPU จริง
→ ช่วงที่ถูกบล็อกหรือถูก preempt ไม่ถูกนับ
ความต่างนี้เป็นจุดตั้งต้นของการเฝ้าเวลาประมวลผลและการตรวจสมมติฐาน WCET ระหว่างที่ Busy_Worker รอด้วย delay until CPU time ไม่เพิ่ม และเพิ่มเฉพาะตอนคำนวณจริง ช่วง delay until Clock + Milliseconds(500) ของเมน task ก็ควรเห็น CPU time เกือบศูนย์เช่นกัน แต่การวัด CPU time ไม่ได้รับประกัน WCET ที่แท้จริง WCET ที่รวม cache, pipeline และ memory contention ยังต้องตรวจแยกด้วย static analysis หรือการยืนยันบนเป้าหมาย
flowchart LR
accTitle: ความต่างระหว่าง wall-clock time กับ CPU time
accDescr: แผนภาพที่แสดงว่า wall-clock รวมเวลารอและถูก preempt ส่วน CPU time นับเฉพาะเวลาที่ task รันบน CPU จริง
subgraph Wall[Wall-clock time]
W1[เวลารวมที่ผ่านไป: 500ms] --> W2[รายการ: คำนวณ + รอ + บล็อก + preempt]
end
subgraph CPU[CPU time]
C1[CPU time รวม: 120ms] --> C2[รายการ: การคำนวณจริงเท่านั้น]
end
Wall --> Diff[ส่วนต่าง = เวลารอ บล็อก และถูก preempt]
CPU --> Diff
Diff --> Insight[CPU time สังเกตต้นทุนการคำนวณจริง<br/>ช่วยตรวจและเฝ้า WCET<br/>ตัดเวลารอ บล็อก และถูก preempt ออก]
Insight --> Caveat[ข้อควรระวัง<br/>การวัดไม่ได้รับประกัน WCET ที่แท้จริง<br/>ยังต้องมี static analysis หรือการยืนยันบนเป้าหมาย]
10. เดโมรวม — ระบบเรียลไทม์หลายคาบ
ต่อไปรวมทุกองค์ประกอบที่เรียนมา — ลำดับความสำคัญ, Ceiling_Locking, delay until, protected object — แล้วสร้างระบบเรียลไทม์หลายคาบแบบฉบับ
-- 08_multiperiodic.ada
-- เดโมรวมระบบเรียลไทม์หลายคาบ
-- 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 ที่ออฟเซ็ต 150ms) โค้ดเองไม่มีงานควบคุม 80ms จึงไม่ใช่แผนภาพที่วัดจากของจริง หากเซ็นเซอร์เร็วมีลำดับความสำคัญสูงกว่า เมื่อ release ของเซ็นเซอร์เร็วมาถึงระหว่างที่ควบคุมช้ารันอยู่ ฝ่ายควบคุมช้าจะถูกหยุดชั่วคราว ในภาพวาดการถูกตัดที่เป็นตัวแทนเพียงครั้งเดียวต่อคาบควบคุมช้าเพื่อให้อ่านง่าย แต่จริง ๆ แล้วเซ็นเซอร์เร็วจะ release ที่ทุกขอบ 100ms
flowchart TB
accTitle: ตารางเวลาของระบบเรียลไทม์หลายคาบ
accDescr: แผนภาพสมมติที่แสดง sensor ความเร็วสูงคาบ 100ms แทรกเข้าไปในรอบควบคุมความเร็วต่ำคาบ 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 จึง preempt"]
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 จึง preempt"]
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 จึง preempt"]
C3F10 --> C3S2["1010-1040ms<br/>ควบคุมช้า #3 ช่วงหลัง"]
end
Assumption --> C1F1
C1S2 --> C2S1
C2S2 --> C3S1
แพทเทิร์นนี้คือโครงฉบับ «เก็บเซ็นเซอร์เร็ว + ลูปควบคุมช้า» ที่พบบ่อยในระบบควบคุมอุตสาหกรรมและการควบคุมหุ่นยนต์
11. จุดที่ฟีเจอร์เรียลไทม์ของ Ada โดดเด่น
ฟีเจอร์เรียลไทม์ของ Ada แสดงคุณค่าชัดเป็นพิเศษในโดเมนต่อไปนี้
flowchart TB
accTitle: โดเมนที่ Ada Annex D โดดเด่น
accDescr: แผนภาพที่แสดงการประยุกต์ใช้ Ada Annex D ในอากาศยาน รถไฟ ยานยนต์ อุปกรณ์การแพทย์ ควบคุมอุตสาหกรรม และการป้องกัน
Ada[Ada Annex D<br/>ฟีเจอร์เรียลไทม์] --> 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[ระบบป้องกันและความน่าเชื่อถือสูง]
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[เครื่องจักรกล NC]
Defense --> D1[คอมพิวเตอร์ภารกิจ]
Defense --> D2[ระบบที่ใช้งานระยะยาว]
เหตุที่ในภาพมีเพียงยานยนต์ที่เขียนว่า «การใช้แบบจำกัดและเลือกสรร» ไม่ได้มาจากว่าภาษาเหมาะหรือไม่เหมาะเป็นหลัก แต่มาจากขนาดของระบบนิเวศที่มีอยู่แล้ว ซอฟต์แวร์ในรถกองข้อกำหนด API มาตรฐานอุตสาหกรรมอย่าง AUTOSAR, โค้ดจากซัพพลายเออร์, คอมไพเลอร์และเครื่องมือตรวจที่ยืนยันแล้ว รวมถึงประชากรวิศวกร ล้วนกองบนสมมติฐานของ C และ MISRA-C การเปลี่ยนภาษา แม้จะเป็นแค่คอมโพเนนต์เดียวที่สนใจ หมายถึงต้องจัดชุดเครื่องมือรอบข้างกับกระบวนการจัดซื้อและตรวจยืนยันใหม่ทั้งก้อน ด้วยเหตุนี้ Ada จึงมักอยู่ในตำแหน่งที่ไม่ใช่มาตรฐานทั้งคัน แต่ถูกเลือกใช้แบบเจาะจงในบางคอมโพเนนต์ที่ต้องการการรับประกันสูงเป็นพิเศษ หรือในองค์กรที่มีสินทรัพย์และระบบพัฒนา Ada อยู่แล้ว ในทางกลับกัน โดเมนอย่างอากาศยานหรือรถไฟที่ระบบนิเวศทั้งก้อนเอนไปทางความน่าเชื่อถือสูงอยู่แล้ว กำแพงนี้ไม่มีแต่แรก
12. ข้อควรระวังและขีดจำกัด
ฟีเจอร์เรียลไทม์ของ Ada ทรงพลัง แต่ไม่ใช่ยาครอบจักรวาล
1. ขึ้นกับแพลตฟอร์ม:
- การแมปจริงของ
pragma Priorityขึ้นกับสภาพแวดล้อมรัน (OS + GNAT runtime) บน Linux ถูกแมปไปSCHED_FIFOแต่บน Windows อาจไม่รับประกัน preemption เต็มรูปแบบ
2. ข้อจำกัดของ Ravenscar:
- ห้ามสร้าง task แบบไดนามิก จึงต้องประกาศ task ทั้งหมดแบบ static ตอนสตาร์ตระบบ นี่จำกัดอิสระของดีไซน์
3. ขีดจำกัดของการวัด WCET:
Ada.Execution_Timeคือ การวัด ไม่ใช่ การรับประกัน WCET ที่แท้จริงซึ่งรวม cache miss และ pipeline hazard ต้องตรวจแยกด้วยเครื่องมือ static analysis
4. โอเวอร์เฮด:
- การประเมิน barrier ของ protected object รันอัตโนมัติตอน entry จบ, ถูกยกเลิก และตอนออกจาก protected object สำหรับ protected object ที่ถูกเรียกถี่ ต้องคิดโอเวอร์เฮดนี้ด้วย
5. กำแพงของทูลเชน:
- หากจะใช้ฟีเจอร์เรียลไทม์ของ Ada ให้เต็มที่ ต้องมีครอสคอมไพเลอร์และ runtime ที่เหมาะ โดยเฉพาะบนเป้าหมายฝังตัว จะพึ่ง runtime ที่ผู้ขายจัดให้
13. สรุป
บทความนี้ไล่ฟีเจอร์เรียลไทม์ที่ Ada Annex D ให้มาทีละขั้นด้วยตัวอย่างโค้ด 8 ชุด
| ฟีเจอร์ | คุณค่าที่ให้ |
|---|---|
| ลำดับความสำคัญของ task | การจัดตารางตามลำดับความสำคัญแบบ preemptive |
| Ceiling_Locking | กัน priority inversion ที่ฝังในภาษา |
delay until |
การทำงานแบบคาบที่กัน cumulative drift |
| โปรไฟล์ Ravenscar | subset ของ task ที่วิเคราะห์แบบ static ได้ง่าย |
| Timing event | ปลุกตามเวลาโดยไม่ต้อง poll |
| คิวแบบ protected | ซิงค์ฐาน barrier ของ protected object |
| การวัดเวลาประมวลผล | เฝ้า CPU time ต่อ task |
| รวมหลายคาบ | ดีไซน์ที่ให้ task คาบต่างกันอยู่ร่วมกันอย่างปลอดภัย |
แก่นของฟีเจอร์เรียลไทม์ของ Ada คือ «ไม่ได้ต่อท้ายภายหลัง» กติกาล็อกที่กด priority inversion, การระบุเวลาสำหรับการทำงานแบบคาบ, และการเฝ้าเวลาประมวลผล ถูกให้มาเป็น ส่วนหนึ่งของข้อกำหนดภาษา แน่นอนว่าการทัน deadline เองยังต้องยืนยันด้วยดีไซน์และการวิเคราะห์ แต่จุดแข็งใหญ่คือ language runtime จัดสมมติฐานสำหรับงานนั้นให้พร้อม
ขั้นต่อไป หากจะลองพัฒนาระบบเรียลไทม์ด้วย Ada จริง ให้ติดตั้งทูลเชน GNAT ด้วย Alire แล้วลองสร้างโค้ดตัวอย่างในบทความนี้ด้วย gnatchop + gnatmake
อนึ่ง พื้นฐานการทำงานแบบ concurrent ของ Ada (task, rendezvous, protected object) ดูบทความก่อนหน้า «การทำงานแบบ concurrent ที่ปลอดภัยใน Ada»
14. อ้างอิง
บทความที่เกี่ยวข้อง
บทความล่าสุดที่มีแท็กเดียวกัน เพื่อเจาะลึกหัวข้อใกล้เคียง
การทำงานแบบ concurrent ที่ปลอดภัยใน Ada ── คู่มือปฏิบัติเรื่อง task และ protected object
บทความแนะนำ concurrency ที่ฝังในภาษา Ada เอง ได้แก่ task และ protected object จัดระเบียบตั้งแต่ rendezvous (entry/accept), selective acce...
Generic programming ใน Ada — เขียนสัญญาด้วยชนิดข้อมูล เพื่อนำกลับมาใช้ใหม่แบบ zero-cost
อธิบาย generic programming ของ Ada อย่างเป็นระบบ ตั้งแต่ generic subprogram, generic package, formal subprogram, type category ไปจนถึงแนว...
เชิงลึกของการจำลองเสมือนบน Windows (ตอนที่ 3) — เครื่องเสมือนที่บูตในไม่กี่วินาที: ทำไม WSL2, Windows Sandbox และคอนเทนเนอร์จึงเบา
ทำไม WSL2 และ Windows Sandbox จึงเริ่มในไม่กี่วินาทีและรู้สึกเบา? บทความนี้อธิบายกลไก ตั้งแต่ dynamic base image และ direct map ไปจนถึงกา...
เชิงลึกของการจำลองเสมือนบน Windows (ตอนที่ 2) — หน่วยความจำที่แม้เคอร์เนลก็มองไม่เห็น: กลไกของ VBS, HVCI และ Credential Guard
เมื่อติดตั้งใหม่บนฮาร์ดแวร์ที่รองรับ VBS จะเปิดตามค่าเริ่มต้น และใช้ hypervisor กับ SLAT สร้างการแยกที่แข็งกว่าเคอร์เนล บทความนี้อธิบายโค...
เชิงลึกของการจำลองเสมือนบน Windows (ตอนที่ 1) — Windows ของคุณรันอยู่ที่ไหนจริง ๆ? Hypervisor และพาร์ติชัน
เมื่อเปิด Hyper-V แล้ว Windows ฝั่งโฮสต์เองก็รันบน hypervisor ในฐานะ root partition บทความนี้อธิบายรากฐานของการจำลองเสมือนผ่านบทบาทของ VT...
หัวข้อที่เกี่ยวข้อง
หน้าเหล่านี้วางหัวข้อไว้ในบริบทที่กว้างขึ้นของบริการและการตัดสินใจ
หัวข้อเทคนิคของ Windows
ประตูสู่การพัฒนา Windows การตรวจสอบบั๊ก และการใช้ประโยชน์จากสินทรัพย์เดิม
คำถามที่พบบ่อย
คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้
- Annex D ของ Ada คืออะไร
- กลุ่มฟีเจอร์สำหรับระบบเรียลไทม์ที่ถูกมาตรฐานไว้เป็นส่วนหนึ่งของข้อกำหนดภาษา Ada ครอบคลุมการจัดตารางแบบ preemptive ตามลำดับความสำคัญด้วย FIFO_Within_Priorities, โปรโตคอล Ceiling_Locking ที่กัน priority inversion, การทำงานแบบคาบด้วยเวลาสัมบูรณ์ผ่าน delay until, โปรไฟล์ Ravenscar, timing event และการวัดเวลาประมวลผลต่อ task ด้วย Ada.Execution_Time จุดเด่นคือไม่ใช่ไลบรารีที่ต่อท้ายภายหลัง แต่ฝังอยู่ใน language runtime เอง
- priority inversion คืออะไร และ Ada กันอย่างไร
- ปรากฏการณ์ที่ task ลำดับความสำคัญต่ำถูก preempt โดย task ลำดับความสำคัญกลางขณะยังถือ lock อยู่ ทำให้ task ลำดับความสำคัญสูงที่รอ lock นั้นถูกบล็อกไม่มีกำหนด เกิดขึ้นจริงกับ Mars Pathfinder ปี 1997 และเป็นสาเหตุที่ยานสำรวจรีเซ็ตซ้ำ Ada ให้โปรโตคอล Ceiling_Locking เป็นฟีเจอร์ของภาษา เมื่อ task เข้า protected object จะถูกยกไป ceiling priority อัตโนมัติ จึงกันไม่ให้ task ลำดับความสำคัญกลาง preempt ได้
- โปรไฟล์ Ravenscar คืออะไร
- โปรไฟล์ที่จำกัดฟีเจอร์ task ของ Ada ให้เหลือ subset ที่วิเคราะห์แบบ static ได้และเป็น deterministic สำหรับระบบที่ความปลอดภัยสำคัญมาก ห้ามการสร้าง task แบบไดนามิก, คำสั่ง select, คำสั่ง abort, delay แบบสัมพัทธ์, คำสั่ง requeue เป็นต้น ข้อจำกัดนี้ทำให้วิเคราะห์ timing แบบ static ได้ง่ายขึ้น และช่วยให้เข้าใกล้คุณสมบัติที่มาตรฐานความปลอดภัยอย่าง DO-178C (ซอฟต์แวร์อากาศยาน) และ ISO 26262 (ความปลอดภัยเชิงฟังก์ชันของยานยนต์) ต้องการ ใน GNAT เปิดใช้โดยเขียน pragma Profile (Ravenscar) ในไฟล์ gnat.adc
- ทำไม periodic task ต้องใช้ delay until ไม่ใช้ delay
- เพราะ delay ที่ระบุเวลาแบบสัมพัทธ์จะบวกเวลาประมวลผลของแต่ละรอบเข้าไป ทำให้คาบค่อย ๆ เพี้ยน (cumulative drift) ส่วน delay until กำหนดเวลาปลุกครั้งถัดไปจากเวลาสัมบูรณ์ ดังนั้นแม้รอบหนึ่งจะช้า เวลาปลุกรอบถัดไปยังคงถูกต้อง อย่างไรก็ตามถ้างานเลยเวลาปลุกครั้งถัดไป delay until จะกลับเกือบทันที จึงต้องมีดีไซน์แยกที่ตรวจว่าเป็น deadline miss และจัดการเป็นภาวะโอเวอร์โหลด