Gem #92: Code Archetypes for Real-Time Programming - Part 2
by Marco Panunzio —University of Padua
In the previous Gem in this series we introduced the key concepts underlying our code archetypes and described the simplest of our archetypes, which realizes a cyclic task. In this Gem we show how to realize an equally simple sporadic task. In the next Gem in this series, we will depart from this level of simplicity to realize a complete archetype that overcomes some of the limitations intrinsic in these initial solutions.
Simple sporadic task
A sporadic task is a task such that any two subsequent activations of it are always separated by no less than a minimum guaranteed time span. This minimum separation is typically called minimum inter-arrival time (MIAT). In this initial archetype, the task executes a single operation at each activation, and it does so in response to a request issued by an external client.
Much like the cyclic task, our sporadic task is composed of: (i) a protected object, shared with the outside world, that external clients invoke to post their requests for execution. In the previous Gem we termed this resource an OBCS; and (ii) a thread of control that waits for incoming requests, fetches the first of them from the protected object and executes the sporadic operation that corresponds to a specific operation as provided by the OPCS.
The task structure whose code we are about to show is sketched in the figure below.
Fortunately, the Ada language is well equipped to realize that structure, and we implement that in our archetype using a protected object under the Ceiling_Locking policy for (i) and a task type for (ii).
For the moment, we illustrate a base version of the archetype for a sporadic task. In the explanation we also illustrate some of its limitations that will not be present in a more complex version of the archetype.
The specification for the simple sporadic task follows:
with System; generic with procedure Sporadic_Operation; Ceiling : System.Priority; OBCS_Size : Integer; package Simple_Sporadic_Task is procedure Put_Request; task type Thread_T (Thread_Priority : System.Priority; Interval : Integer) is pragma Priority (Thread_Priority); end Thread_T; end Simple_Sporadic_Task;
The specification defines (as for the Cyclic task that we presented in the previous Gem) a task type inside a generic package. When instantiating the package we specify the sporadic operation for the task, the Ceiling Priority for the OBCS protected object, and the size of the queue of requests of the OBCS.
Additionally, we create the procedure Put_Request, that is used by clients to post a request to the sporadic task.
The body for the package is instead:
with System_Time; with System_Types; with Ada.Real_Time; package body Simple_Sporadic_Task is Protocol : System_Types.Simple_Sporadic_OBCS (Ceiling, OBCS_Size); procedure Put_Request is begin Protocol.Put_Request; end Put_Request; task body Thread_T is use Ada.Real_Time; Next_Time : Time := System_Time.System_Start_Time + System_Time.Task_Activation_Delay; MIAT : Time_Span := Milliseconds (Interval); Release : Time; begin loop delay until Next_Time; Protocol.Get_Request (Release); Next_Time := Release + MIAT; Sporadic_Operation; end loop; end Thread_T; end Simple_Sporadic_Task;
Comparing this body to the body of the cyclic task, two major differences appear: (i) the presence of an OBCS; and (ii) a slightly modified loop structure.
As in the cyclic task, the sporadic task enters its infinite loop and suspends itself until the system-wide start time. After that: (i) it calls the entry Get_Request(Time) of the OBCS; (ii) after the execution of the entry (from which, as we show later on, it obtains a timestamp of when release actually occurred), the task executes the Sporadic_Operation (single, for now) specified at the instantiation of its generic package; (iii) it calculates the next earliest time of release (Next_Time) so as to respect the minimum separation between subsequent activations. Therefore, on the next iteration of the loop the task issues a request for absolute suspension until that time, and thus it won't probe the OBCS for execution requests until the required minimum separation has elapsed.
As a final note, when the procedure Put_Request is called, it just performs a simple indirection to an OBCS procedure with the same name. To appreciate that, we must take a look at the OBCS, which acts as the synchronization agent for the task.
The specification of the OBCS is as follows:
with System; with Ada.Real_Time; use Ada.Real_Time; package System_Types is protected type Simple_Sporadic_OBCS (C : System.Priority; Size : Integer) is pragma Priority(C); procedure Put_Request; entry Get_Request (Release_Time : out Time); private Max_Pending : Integer := Size; START_Pending : Integer := 0; Barrier : Boolean := False; end Simple_Sporadic_OBCS; end System_Types;
The OBCS declares a procedure Put_Request that is used to post requests in its queue, and a guarded entry Get_Request(Time) that is used by the thread to fetch the requests. In the private part of the declaration, the Max_Pending attribute is used to set the maximum number of pending requests that the OBCS can hold (obviously no greater than its size); the START_Pending attribute indicates the actual number of pending requests; finally the Boolean Barrier is used to control the guard of Get_Request(Time).
package body System_Types is protected body Simple_Sporadic_OBCS is procedure Update_Barrier is begin Barrier := Start_Pending > 0; end Update_Barrier; procedure Put_Request is begin if Start_Pending < Max_Pending then Start_Pending := Start_Pending + 1; end if; Update_Barrier; end Put_Request; entry Get_Request (Release_Time : out Time) when Barrier is begin Release_Time := Ada.Real_Time.Clock; Start_Pending := Start_Pending - 1; Update_Barrier; end Get_Request; end Simple_Sporadic_OBCS; end System_Types;
The body of the OBCS is quite easy to understand. When the procedure Put_Request is called, the number of pending requests (START_Pending) is increased unless the maximum number has already been reached. In that case the new request is just silently ignored.
The entry Get_Request(Time) is used by the task to probe the OBCS for pending requests. In the case where there are requests, the Barrier guard is open and the task: (i) saves the time stamp of the execution of the entry (which notionally coincides with the release of the task), that is later used to calculate the next release time; and (ii) decreases the number of pending requests.
At the end of Put_Request and Get_Request, the value of the Barrier guard is refreshed using Update_Barrier. In the event that there are no more pending requests, Barrier is set to false. For this reason, if the guard is closed when the task calls the entry, the call is blocked until a new request is posted.
The check for the request queue to be not empty is not directly used as the guard expression for the entry, so as to comply with the restriction of the Ravenscar Profile that requires guards to be simple Boolean conditions, and thus have deterministic evaluation. The OBCS has a single entry, as the profile requires, and the only task that can be enqueued on it is the task to which the OBCS belongs, thus ensuring full compliance with the Ravenscar Profile.
While the proposed structure achieves our goal of creating a sporadic task, we immediately notice two potential drawbacks: the Sporadic_Operation is parameterless, and the synchronization protocol is very, perhaps too, simple to capture real-life system needs.
For what concerns the first issue, clients of the sporadic task simply trigger new releases of the task, but cannot, for example, pass data to the task as parameters of the release request. Creating a nontrivial producer-consumer collaboration pattern with this task structure is impossible because the consumer task (our sporadic task) cannot receive any data to process.
For what concerns instead the OBCS, in this version it is a simple counter of pending requests.
In the next Gems in this series, we will illustrate how to support sporadic operations with parameters and start to realize more complex queuing policies for execution requests.