โปรแกรมมิงระบบเรียลไทม์ด้วย Ada — ลำดับความสำคัญ คาบ และการควบคุมเวลาประมวลผลในทางปฏิบัติ

· อัปเดตเมื่อ: · · 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

แผนที่ความรู้โปรแกรมมิงระบบเรียลไทม์ของ Adaแผนภาพที่แสดงว่า Ada Annex D ฝังการ dispatch ตามลำดับความสำคัญ, Ceiling_Locking, delay until, โปรไฟล์ Ravenscar, timing event และการวัดเวลาประมวลผล ลงบน task กับ protected object อย่างไร และ Ceiling_Locking กัน priority inversion อย่างที่เกิดกับ Mars Pathfinder ได้อย่างไรใช้ใช้ใช้ใช้ใช้ใช้ต้องมีต้องมีต้องมีป้องกันอาจก่อให้เกิดต้องมีต้องมีต้องมีกำหนดค่าด้วยอาจก่อให้เกิดป้องกันไม่แนะนำให้ใช้กับใช้ใช้ต้องมีAnnex D (มาตรฐานระบบเรียลไทม์ของ Ada)โปรโตคอล Ceiling_LockingFIFO_Within_Prioritiesdelay until (หน่วงถึงเวลาสัมบูรณ์)โปรไฟล์ Ravenscartiming event (Ada.Real_Time.Timing_Events)Ada.Execution_Time (การวัดเวลาประมวลผล)task ของ Ada (การประมวลผลพร้อมกัน)อ็อบเจ็กต์ป้องกัน (protected object)GNATการกลับลำดับความสำคัญ (priority inversion)อุบัติเหตุการกลับลำดับความสำคัญของ Mars Pathfinderdelay แบบสัมพัทธ์ (คำสั่ง delay)ดริฟต์สะสมของการทำงานเป็นคาบ

ในแผนภาพ เส้นทึบหมายถึงความสัมพันธ์ที่ถือเสมอ และเส้นประหมายถึงความสัมพันธ์ที่มีเงื่อนไข (เงื่อนไขอยู่ในการอธิบายของแต่ละความสัมพันธ์ในหน้าละเอียด) รายการความสัมพันธ์ทั้งหมด (รวม 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 ให้ โมเดลการรันที่คาดเดาได้ ในระดับภาษา ซึ่งทำให้วิเคราะห์นั้นง่ายขึ้น

ข้อกำหนดระบบเรียลไทม์กับการวิเคราะห์ schedulabilityแผนภาพที่แสดงว่า hard real-time, soft real-time, deadline, period, WCET, jitter และกลไกของ Ada Annex D ไหลเข้าสู่การวิเคราะห์ schedulability อย่างไรข้อกำหนดเรียลไทม์พลาด deadline = ความล้มเหลวร้ายแรงพลาดเป็นครั้งคราวได้Deadlineเวลาสัมบูรณ์ที่ต้องเสร็จPeriodช่วงทำซ้ำWCETเวลาประมวลผลกรณีเลวสุดJitterความแกว่งของคาบHard real-timeควบคุมการบินถุงลมนิรภัยเครื่องกระตุ้นหัวใจSoft real-timeสตรีมวิดีโอเกมUIAda Annex Dกลไกที่ค้ำจุนความคาดเดาได้FIFO_Within_PrioritiesCeiling_Lockingdelay untilการวิเคราะห์ schedulabilityเงื่อนไขจำเป็น: WCET &lt;= deadlineความเพียงพอตรวจด้วย response-time analysis

หนึ่งในปรากฏการณ์ที่อันตรายที่สุดในระบบเรียลไทม์คือ priority inversion ปัญหานี้เกิดจริงกับ Mars Pathfinder ปี 1997 และเป็นสาเหตุที่ยานสำรวจรีเซ็ตซ้ำ

ลำดับเหตุการณ์ของ priority inversionแผนภาพลำดับที่แสดง task ลำดับความสำคัญต่ำถือ lock แล้วถูก preempt โดย task ลำดับความสำคัญกลาง ทำให้ task ลำดับความสำคัญสูงถูกบล็อกไม่มีกำหนดทรัพยากรที่ใช้ร่วมTask ลำดับความสำคัญกลางTask ลำดับความสำคัญสูงTask ลำดับความสำคัญต่ำSchedulerทรัพยากรที่ใช้ร่วมTask ลำดับความสำคัญกลางTask ลำดับความสำคัญสูงTask ลำดับความสำคัญต่ำSchedulerกำลังรันใน critical sectionH ปลุก จึงให้ scheduler หยุด Lบล็อกเพราะรอ lock! (L ยังถืออยู่)H รอ lock จึงให้ L รันต่อเดินต่อเพื่อจะปล่อย lock...M ปลุก จึงให้ scheduler หยุด LL ปล่อย lock ไม่ได้M รันต่อ (ทั้ง H และ L ขยับไม่ได้)[priority inversion] ฝ่ายสูงถูกบล็อกไม่มีกำหนดได้ lockพยายามได้ lock

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 แก่แต่ละ task Priority'Last สูงสุด Priority'First ต่ำสุด
  • ตัว task ในเดโมนี้ไม่ใช่การคำนวณหนัก แต่รอจนถึงเวลาที่กำหนดด้วย delay until สิ่งที่อยากยืนยันคือ เมื่อพร้อมรันพร้อมกัน task ลำดับความสำคัญสูงได้โอกาสรันก่อน
  • แผนภาพนี้แสดงด้าน preemption ระหว่างลำดับความสำคัญต่างกันของ FIFO_Within_Priorities หากจะยืนยันลำดับ FIFO ภายในลำดับความสำคัญเดียวกัน ต้องมีตัวอย่างอีกชุดที่วางหลาย task ที่ลำดับความสำคัญเท่ากัน
  • ในระบบจริงมักออกแบบลำดับความสำคัญแบบสัมพัทธ์โดยยึด System.Default_Priority เป็นฐาน
การสาธิต FIFO_Within_Prioritiesแผนภาพลำดับที่แสดง scheduler เลือก task ลำดับความสำคัญสูงก่อน แล้วให้ task ลำดับความสำคัญต่ำรันระหว่างที่ฝ่ายสูงถูกบล็อกด้วย delay untilTask ลำดับความสำคัญต่ำ(Priority=First)Task ลำดับความสำคัญสูง(Priority=Last)Main taskSchedulerTask ลำดับความสำคัญต่ำ(Priority=First)Task ลำดับความสำคัญสูง(Priority=Last)Main taskSchedulerT=0ms: ทั้งสอง task เป็น runnableพิมพ์ล็อกเริ่มต้นพิมพ์ล็อกเริ่มต้นT=100ms: HP ปลุกพิมพ์ล็อกเสร็จงานT=500ms: LP ปลุกพิมพ์ล็อกเสร็จงาน(T=800ms) เมนจบสร้าง taskสร้าง taskเลือก HP ซึ่งลำดับความสำคัญสูงสุดบล็อกด้วย delay until T+100msรัน LP ต่อบล็อกด้วย delay until T+500msรัน HPรัน LP

ตัวอย่างการรัน (GNAT 13.3.0 / Ubuntu 24.04, x86-64):

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

สังเกตว่าล็อกเริ่มต้นของสอง 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:

  1. ตั้ง ceiling priority ให้ protected object ด้วย pragma Priority (Ceiling)
  2. task ใดเข้า protected object จะ ถูกยกไป ceiling priority อัตโนมัติตอนเข้า
  3. ด้วยเหตุนี้ task ลำดับความสำคัญกลางจึง preempt task ที่กำลังใช้ protected object ไม่ได้
  4. เมื่อออกจาก protected object ลำดับความสำคัญกลับสู่ค่าเดิม

แผนภาพด้านล่างไม่ใช่เทรซเวลาแบบตรงตัวของโค้ดตัวอย่างทันทีก่อนหน้า แต่เป็นแผนภาพแนวคิดว่าแบบแผน priority inversion ในแผนภาพที่ 2 ถูก Ceiling_Locking กดได้อย่างไร

Ceiling_Locking ป้องกัน priority inversionแผนภาพลำดับที่แสดง task ที่เข้า protected object ถูกยกไป ceiling priority จึงไม่ถูก preempt โดย task ลำดับความสำคัญกลางProtected object(ceiling priority=30)Task ลำดับความสำคัญสูง(ลำดับความสำคัญ=30)Task ลำดับความสำคัญกลาง(ลำดับความสำคัญ=20)Task ลำดับความสำคัญต่ำ(ลำดับความสำคัญ=10)SchedulerProtected object(ceiling priority=30)Task ลำดับความสำคัญสูง(ลำดับความสำคัญ=30)Task ลำดับความสำคัญกลาง(ลำดับความสำคัญ=20)Task ลำดับความสำคัญต่ำ(ลำดับความสำคัญ=10)Schedulerผู้เรียกที่ active priority > ceiling ได้ Program_ErrorH(30) ในภาพเท่ากับ ceiling(30) จึงเข้าได้ลำดับความสำคัญขณะรันถูกยกเป็น 30M ปลุกL กำลังรันที่ ceiling 30M(20) preempt ไม่ได้H ปลุกH(30) ผ่านการตรวจ ceilingแต่รอเพราะ L กำลังใช้ POลำดับความสำคัญกลับเป็น 10หลังปล่อย PO แล้วรัน HH(30) = ceiling(30) จึงเข้าได้หลังข้อขัดแย้งคลี่คลายเข้า protected operationทำงานของ operationออกจาก protected operationเข้า protected operationออกจาก protected operation

หลักออกแบบ: 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 ทุกตัวจากนี้ไป

ความต่างระหว่าง delay กับ delay untilแผนภาพที่เปรียบเทียบ cumulative drift ของ delay แบบสัมพัทธ์ กับ delay until ที่ยึดเวลาสัมบูรณ์ และกรณี deadline miss เมื่อเกินคาบเกินคาบ - deadline missคำนวณ 130msครั้งถัดไป = T+100msdelay until T+100ms กลับทันทีตรวจความล่าช้าแล้วจัดการเป็นโอเวอร์โหลดdelay until - ยึดเวลาสัมบูรณ์คำนวณ 15msครั้งถัดไป = T+100msdelay until T+100ms → ปลุกที่ 100msคำนวณ 10msครั้งถัดไป = T+200ms → ปลุกที่ 200msช่วงจริง: 100ms, 100ms...delay Period - cumulative driftdelay 100ms → ปลุกที่ 115msT=0ms: คำนวณ 15msคำนวณ 10ms → 125msdelay 100ms → ปลุกที่ 225msช่วงจริง: 115ms, 110ms...ความเพี้ยนสะสมตามเวลากัน cumulative drift

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

ข้อจำกัดและนโยบายของโปรไฟล์ Ravenscarแผนภาพที่แสดงว่าโปรไฟล์ Ravenscar จำกัด tasking ของ Ada ให้วิเคราะห์ timing แบบ static ได้ง่ายขึ้น และสัมพันธ์กับมาตรฐานความปลอดภัยฟีเจอร์ task ของ Ada ทั้งชุดโปรไฟล์ Ravenscarข้อจำกัดนโยบายที่บังคับห้ามสร้าง task แบบไดนามิกห้ามคำสั่ง selectห้ามคำสั่ง abortห้าม Task_Attributesจำกัด 1 entry ต่อ protected objectห้ามคำสั่ง requeueห้าม delay แบบสัมพัทธ์ใช้ delay untilห้ามเปลี่ยนลำดับความสำคัญแบบไดนามิกห้ามการจบของ taskทุก task ไม่จบFIFO_Within_PrioritiesCeiling_Lockingสิ่งที่ทำได้ง่ายขึ้น:การวิเคราะห์ timing แบบ staticDO-178Cซอฟต์แวร์อากาศยานISO 26262ความปลอดภัยเชิงฟังก์ชันของยานยนต์IEC 62304ซอฟต์แวร์อุปกรณ์การแพทย์

หากจะเปิดโปรไฟล์ 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

สถานะของ bounded buffer แบบ protected objectแผนภาพสถานะ Empty, Partial, Full ของคิว และเงื่อนไข barrier ที่บล็อก Put หรือ Getสถานะเริ่มต้นPut (เพิ่ม 1 element)Put / GetGet (เอา element สุดท้ายออก)Put (เติมช่องว่างสุดท้าย)Get (เกิดช่องว่าง)Get ถูกบล็อก (barrier Count=0)Put ถูกบล็อก (barrier Count=Buffer_Size)ว่าง / Count=0มีบางส่วน / Count=1..Buffer_Size-1เต็ม / Count=Buffer_Size

เมื่อ Put สำเร็จ barrier ของฝ่ายรอ Get ถูกประเมินใหม่ และเมื่อ Get สำเร็จ barrier ของฝ่ายรอ Put ถูกประเมินใหม่ การประเมินนี้เกิดตอน 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 หรือการยืนยันบนเป้าหมาย

ความต่างระหว่าง wall-clock time กับ CPU timeแผนภาพที่แสดงว่า wall-clock รวมเวลารอและถูก preempt ส่วน CPU time นับเฉพาะเวลาที่ task รันบน CPU จริงCPU timeรายการ: การคำนวณจริงเท่านั้นCPU time รวม: 120msWall-clock timeรายการ: คำนวณ + รอ + บล็อก + preemptเวลารวมที่ผ่านไป: 500msส่วนต่าง = เวลารอ บล็อก และถูก preemptCPU time สังเกตต้นทุนการคำนวณจริงช่วยตรวจและเฝ้า WCETตัดเวลารอ บล็อก และถูก preempt ออกข้อควรระวังการวัดไม่ได้รับประกัน WCET ที่แท้จริงยังต้องมี 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

ตารางเวลาของระบบเรียลไทม์หลายคาบแผนภาพสมมติที่แสดง sensor ความเร็วสูงคาบ 100ms แทรกเข้าไปในรอบควบคุมความเร็วต่ำคาบ 400msควบคุมช้า คาบ 3 (release=950ms)ควบคุมช้า คาบ 2 (release=550ms)ควบคุมช้า คาบ 1 (release=150ms)1000-1010msเซ็นเซอร์เร็ว #10P+3 จึง preempt950-1000msควบคุมช้า #3 ช่วงแรก1010-1040msควบคุมช้า #3 ช่วงหลัง600-610msเซ็นเซอร์เร็ว #6P+3 จึง preempt550-600msควบคุมช้า #2 ช่วงแรก610-640msควบคุมช้า #2 ช่วงหลัง150-200msควบคุมช้า #1 ช่วงแรก100-110msเซ็นเซอร์เร็ว #1200-210msเซ็นเซอร์เร็ว #2P+3 จึง preempt210-240msควบคุมช้า #1 ช่วงหลังสมมติเพื่ออธิบายเซ็นเซอร์เร็ว: งาน 10msควบคุมช้า: งาน 80ms

แพทเทิร์นนี้คือโครงฉบับ «เก็บเซ็นเซอร์เร็ว + ลูปควบคุมช้า» ที่พบบ่อยในระบบควบคุมอุตสาหกรรมและการควบคุมหุ่นยนต์

11. จุดที่ฟีเจอร์เรียลไทม์ของ Ada โดดเด่น

ฟีเจอร์เรียลไทม์ของ Ada แสดงคุณค่าชัดเป็นพิเศษในโดเมนต่อไปนี้

โดเมนที่ Ada Annex D โดดเด่นแผนภาพที่แสดงการประยุกต์ใช้ Ada Annex D ในอากาศยาน รถไฟ ยานยนต์ อุปกรณ์การแพทย์ ควบคุมอุตสาหกรรม และการป้องกันAda Annex Dฟีเจอร์เรียลไทม์อากาศยานและอวกาศDO-178Cรถไฟตระกูล EN 50128ยานยนต์ISO 26262อุปกรณ์การแพทย์IEC 62304ควบคุมอุตสาหกรรมตระกูล IEC 61508ระบบป้องกันและความน่าเชื่อถือสูงควบคุมการบินโดเมนที่มีผลงานมากควบคุมดาวเทียมและยานอวกาศระบบสัญญาณควบคุมรถไฟอัตโนมัติตัวเลือกสำหรับ ECU ที่เกี่ยวความปลอดภัยการใช้แบบจำกัดและเลือกสรรในโดเมนที่ C / MISRA-C เป็นหลักเครื่องกระตุ้นหัวใจปั๊มสารน้ำควบคุมหุ่นยนต์เครื่องจักรกล NCคอมพิวเตอร์ภารกิจระบบที่ใช้งานระยะยาว

เหตุที่ในภาพมีเพียงยานยนต์ที่เขียนว่า «การใช้แบบจำกัดและเลือกสรร» ไม่ได้มาจากว่าภาษาเหมาะหรือไม่เหมาะเป็นหลัก แต่มาจากขนาดของระบบนิเวศที่มีอยู่แล้ว ซอฟต์แวร์ในรถกองข้อกำหนด 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. อ้างอิง

บทความล่าสุดที่มีแท็กเดียวกัน เพื่อเจาะลึกหัวข้อใกล้เคียง

เชิงลึกของการจำลองเสมือนบน 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...

หน้าเหล่านี้วางหัวข้อไว้ในบริบทที่กว้างขึ้นของบริการและการตัดสินใจ

คำถามที่พบบ่อย

คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้

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 และจัดการเป็นภาวะโอเวอร์โหลด

โปรไฟล์ผู้เขียน

หน้าแนะนำผู้เขียนบทความ

Go Komura

ผู้แทนของ KomuraSoft LLC

เชี่ยวชาญด้านการพัฒนาซอฟต์แวร์ Windows ที่ปรึกษาเทคนิค และการตรวจสอบบั๊ก โดยเฉพาะโปรเจกต์ที่มีระบบเดิมและบั๊กที่ทำซ้ำได้ยาก

ลิงก์สาธารณะ

กลับไปยังบล็อก