A Predictable Real Time Operating System

Document Sample
A Predictable Real Time Operating System Powered By Docstoc
					    A Predictable Real Time Operating System
                              Dr. Mantis H.M. Cheng
                                   October 28, 2003


                                           Abstract
              Conventional Real Time Operating Systems (RTOS) are designed
          with a generic architecture that supports a priority-based preemp-
          tive scheduler. Performance parameters, such as latency, jitter, band-
          width, are left to the application engineers; the RTOS provides no
          guarantee or support.
              We present an RTOS architecture that is both predictable and de-
          terminate. It is predictable in the sense that when and how often a
          task is run is guaranteed with minimal variations. It is determinate in
          the sense that the application tasks will behave exactly the same both
          in timeliness and bandwidth utilization independent of which processor
          platform is chosen.


1         Introduction
An application is real time if its correctness depends critically on its timeli-
ness. Timeliness could mean “as quickly as possible” (fast response), or “as
regularly as possible” (low jitter). Fast response is often interpreted as low
latency, which is translated into high task priority. The lower the latency of
a task’s response time, the higher its priority. Therefore, priority is inversely
proportional to latency. For periodic tasks, the shorter a task’s period, the
higher its priority.1 Therefore, priority is inversely proportional to a task’s
period. It may seem that all timeliness requirements could be translated into
task priorities.
    1
        This is Rate Monotonic Analysis.



                                              1
1.1     A Simple Sampling Problem
It is quite foreseeable that a sampling task with no latency requirement may
need to maintain a low but precise sampling rate. Precision means “as little
deviation from its sampling rate as possible”. For example, we can write this
sampling task in a typical RTOS as follows.
       thread R {
           for(;;) {
              read( sample );
              sleep( period );
           }
       }
The precision of R’s sampling rate is based entirely on the ”precision” of
“sleep”. In most systems, “sleep(T)” means “sleep at least T milliseconds”,
not at most or exactly. There could be very large variations in timing in each
sampling loop, due to the unpredictability of “sleep” jitter. In practice many
application tasks resort to the use of large buffers just to prevent underflow
or overflow as a result of “sleep” jitter. This is one of the root causes of
the large variations of data consumption or production rates. As a result,
a large buffer is necessary even when each sample’s processing time remains
constant. Although buffering could overcome data loss due to “sleep” jitter,
too large a buffer could unintentionally introduce latency, i.e., a piece of data
could stay in the buffer too long. It is a challenge to predict latency and jitter
when sleep is the only mechanism to deal with timeliness.2
    To solve our precise sampling problem, an alternative solution could be
written as follows.
       thread P {
         for(;;) {
           read( sample );
           next();
         }
       }
where P is specified to execute at a fixed rate, f cycles per second, or at a
fixed period, once every T (= 1/f ) milliseconds.3 “next” means “done with
   2
     A busy-waiting loop, i.e., a do nothing delay loop, is another. It is no better since
precious processor cycles are wasted.
   3
     We shall use T from now on to denote a thread’s period.

                                            2
current sample, wait until next cycle”. (Notice that next doesn’t specify
the delay time.) The difference between R and P is that R must specify
an explicit delay time using sleep with respect to the time when sleep
is called, while P specifies this requirement as its timing property. If each
sample requires varying amount of processing time, then the actual delay
will vary accordingly. The time to sleep cannot easily be adjusted since the
actual processing time is unknown. As a result, the overall sampling jitter
of R is worse than the sleep jitter alone. On the other hand, as long as
each data sample’s processing time is shorter than T, P’s sampling precision
is determined entirely by the precision of the underlying scheduler. It is up
to the scheduler to guarantee this timing requirement of P independent of
how long each sample processing time takes.
    It is important to emphasize at this point that the specification of exe-
cution frequency (or period) of a thread is independent of the speed of the
host processor.4 Running on a faster processor doesn’t change the basic tim-
ing requirement, its sampling rate. Once we determine the appropriate I/O
sampling rate, the behavior of a sampling thread such as P is predictable
and determinate, the same on every hosting platform. One may argue that a
“lower jitter” sleep could make R just as predictable. The fact is timeliness of
execution is the main concern of a predictable scheduler. All decisions about
timing requirements must be made efficiently by the scheduler. “sleep” is
a thread suspension mechanism designed to support an alarm function and
to introduce a relative delay (w.r.t. when it is called). Its implementation
is often done by a “sleep” queue sorted by expiration times. It is totally
independent of the scheduler, and is a crude way of supporting timely execu-
tion. For instance, doing ten times of sleep(100) is rarely equal to a single
sleep(1000). The precision of sleep is often based on the resolution of a
sleep timer and the overhead in maintaining the sleep queue, plus other un-
related issues such as relative priority of a sleeping thread, the overall “load”
of the system, etc.

1.2     Complex Timing Requirements
There are other more complex situations where a task is required to perform
several timely operations within a single period. For example,
  4
    Obviously, on a slower processor, a thread could run out of its maximum allocated
processing time if its period is too short.



                                         3
      thread Q {
          // each period is 100 msec.
          for(;;) {
             write( high );        // pull signal high
             sleep( 15 );          // wait for 15 msec
             write( low );         // pull signal low
             sleep( 25 );          // wait another 25 msec;
             read( status );
             sleep( 60 );
          }
      }

this task Q is executed at a rate of 10 Hz, or once every 100 milliseconds.
Within each cycle, it needs to generate a high and low control signal with a
duration of 15 milliseconds, and then it must wait for another 25 milliseconds
before it can read the status back. Any jitter in the control signals would
be undesirable. This task is not a simple sampling task such as P. Its timing
requirement cannot easily be expressed as just a fixed rate. We know its
“main” cycle time is 100 milliseconds. But, its internal sub-cycle is more
complex. How can we specify Q’s timing requirements? When there are
many such tasks with complex timing requirements running simultaneously,
it is a challenge to schedule all of these tasks and still maintain their stringent
timing requirements. Using sleep would be unacceptable due to its jitter.

1.3    I/O Bandwidth-Based Scheduling
Traditional schedulers in real time operating systems are either time-based
or priority-based. An example of a time-based scheduler is round-robin
where each task gets one quantum. Today more and more applications are
I/O bandwidth hungry; without any control, some tasks/applications could
consume as much bandwidth as what is available, leaving little to other
tasks/applications. A time-based or priority-based scheduler could not ad-
dress any I/O bandwidth utilization requirements. That is, a task/application
should only be allowed to consume up to certain maximum amount of I/O
bandwidth (Kilo bits per second), and no more. In other words, it is only
allowed read/process/write a maximum of I/O data per unit time. When
this limit is reached, a task is rescheduled.
    Imagine for a moment, I/O bandwidth of a system is expressed in units of

                                        4
64Kbps (or 8 Kilo bytes per second). A task that requires to read/process/write
I/O data at a rate of 56Kbps will be assigned 1 unit (or 8 KB/s). How could
the I/O data be delivered to this task? For example, it could be delivered
all 8KB at the beginning of a second or at the end, or one segment at a
time. There is an issue of segmentation, i.e., how to break up the data into
segments. This all seems very fuzzy. Look at it another way, there are the
following options:
     Option    Segment Size (KB) Period (msec) Bandwidth (Kbps)
       1               8              1000            64
       2               4              500             64
       3               2              250             64
       4               1              125             64
Each option (or combination) requires a bandwidth 64 Kbps. The conse-
quence in latency requirement is different. Option (1) specifies one segment
of 8KB per second; (2) specifies two segments of 4KB, each must be deliv-
ered within 500 milliseconds; (3) specifies 4 segments of 2KB, each must be
delivered within 250 milliseconds, etc. Basically, the smaller the segment size
(hence, the shorter the period), the more stringent the latency requirement.
    As an example, consider a simple task which copies its input to its output
without any processing:
  thread U {
       char *buf
       int size;
         for(;;) {
           size = read( in, &buf );           /* read a chunk of data */
           write( out, buf, size );           /* write a chunk of data */
       }
  }
If we create U as an I/O task with a bandwidth of 64Kbps and option (1),
then U will be scheduled once per second, and it will execute its loop once
a second with size equal to 8KB each time. But, if we choose option (2),
it will execute its loop once every 500 milliseconds with size equal to 4KB
each time. As we can see, the smaller the segment size, the more frequent
the thread runs.
    Our I/O bandwidth and latency scheduling is based on the simple obser-
vation:

                                      5
      For the same I/O bandwidth, smaller segment size gives rise to
      lower latency.

As a result, we must schedule a thread more frequent with a smaller segmen-
tation size requirement. Assuming that every read gets a segment, then one
read is allowed per segment-period, e.g., for option (4), one 1KB per 125
milliseconds. For two threads requiring the same I/O bandwidth, the one
with a smaller segment-period will be scheduled more frequent, and thus has
a lower I/O latency.
    Let the maximum of I/O bandwidth be B KB/s.5 Then, for a fixed
bandwidth requirement, lets say 8KB/s, then the minimum CPU utilization
is (8/B) ∗ 100 percent. If B is 100KB/s, then in this case it will be 8%. With
different segmentation sizes, we need to guarantee 8% of CPU utilization
per second for option (1) (or 80 milliseconds every second), or 8% per 500
milliseconds for (2) (or 40 milliseconds), or 8% per 250 milliseconds for (3) (or
20 milliseconds), etc. For option (1) the worst latency between two successive
segments will be 2 ∗ (1000 − 80) = 1840 milliseconds 6 ; for option (2), it will
be 2 ∗ (500 − 40) = 920 milliseconds, etc. Based on a 64Kb/s (or 8KB/s)
bandwidth, we arrive at the following segment sizes vs latency characteristics
(time is in milliseconds):

        Option     Segment Size (KB) Period Processing Latency
          1                8          1000     80       1840
          2                4          500      40        920
          3                2          250      20        460
          4                1          125      10        230

From an application point of view, reserving more I/O bandwidth will guar-
antee more processing time. If latency is a concern, then lowering the seg-
mentation size will reduce the overall worst case latency.

      The higher the bandwidth, the higher the CPU utilization; the
      smaller the segmentation size, the lower the latency.
  5
     The upper limit of B is the raw memory-to-memory copy rate, assuming all I/O data
transfers are done via DMA.
   6
     One segment is delivered at the beginning of a period, the other is at the end.




                                          6
1.4     Summary
For any real time systems/applications, any amount of time spent in con-
text switching or making scheduling decisions is overhead. The smaller the
overhead, the more efficient the processor utilization. Furthermore, complex
scheduling decisions may introduce unpredictable variations in timely exe-
cution. We could obtain a near “optimal” scheduling decision, but we may
have missed all critical timing requirements. It is therefore important to keep
scheduling decisions as simple and as efficient as possible. It is simple if all
difficult scheduling decisions have been made before runtime. It is efficient if
a constant scheduling overhead is all that is needed; the cost should be inde-
pendent of the total number of active threads. Complex scheduling decisions
should be avoided at runtime.
    Many traditional schedulers offer little support to guarantee I/O band-
width utilization and latency. Our ideas of specifying a task’s I/O bandwidth
and segmentation requirements are simple and novel. By coupling with an
automatic buffer management in our RTOS for all I/O data, designing real
time applications with specific latency properties will be simplified and pre-
dictable.


2       A Predictable Scheduler
We present an RTOS architecture that is both predictable and determinate. It
is predictable in the sense that when and how often a task is run is guaranteed
with minimal variations. It is determinate in the sense that the application
tasks will behave exactly the same both in timeliness and I/O bandwidth
utilization independent of which processor platform is chosen. During execu-
tion, if a timing or bandwidth requirement cannot be satisfied, the violating
task will be aborted with an error message.
    There are four scheduling levels: DEVICE, PLANNED, IO7 , and SPORADIC.
A thread is created at any one of these levels, which can never be changed
once it is assigned. The scheduling levels are prioritized: DEVICE level is the
highest, PLANNED is second, IO is third, and SPORADIC is the lowest.8 A thread
is either active or inactive. An active thread is either running, delayed or
    7
    The API for IO threads is deliberately left out. Please contact us for more details.
    8
    In principle, there is another level called IDLE, which is lower than SPORADIC. It is not
a real scheduling level in the sense that a user cannot create threads at this level.



                                             7
ready. An inactive thread is either blocked or terminated. A running thread
is the currently executing thread. A ready thread is one that is waiting for a
processor. A delayed thread is a ready thread where its execution is delayed
due to its timing requirement; it is waiting for a processor at a specific time.

      A higher level active thread always preempts a lower level active
      thread as soon as it is ready to run.

We use the term a delayed thread when we mean a ready thread that has
specific timing requirement; otherwise, we just call it a ready thread.

2.1    DEVICE Level Threads
A DEVICE level thread is designed to model a simple “sampling” task, which
reads/writes I/O devices at specific rates. Its processing time per cycle is
relatively short. Its sampling rate is stringent. A DEVICE thread may be
created or terminated at any time. Once it is created, it is executed at exactly
the specified rate, e.g., once every 50 milliseconds. Its intended usage is to
sample inputs or produce outputs at a regular rate. The rate is guaranteed
not to exceed its maximum tolerable jitter (e.g., less than 10%). To create a
DEVICE level thread, we call:

Pid   OS_CreateDevice( void (*f)(void), int argument,
                       int period, int jitter )

It will turn a function f into a thread, where arg is f’s single initial function
argument, period is its execution period e.g., 50 means once every 50 mil-
liseconds, and jitter is its maximum jitter tolerable expressed in terms of
percentage, e.g., 5 means +/-5%.
    Multiple DEVICE threads may collide at the same time instance, once every
least common multiple of their execution periods. If we have three DEVICE
threads with rates once every 20, 50, and 70 milliseconds, then they collide
once every 700 milliseconds. When they collide, we must make a decision
about which one of these threads should be executed next.They are ranked
according to their jitter requirement: the lower the jitter, the closer it is
scheduled to its specified time. In practice, we may not be able to satisfy all
jitter requirements using a simple ranking strategy. In some cases, we may
have to resort to execute some of these threads slightly ahead of their specified
time and still satisfy their jitter requirements. If the running average jitter

                                       8
of a DEVICE thread exceeds the maximum tolerable value this thread will be
terminated.9

      A DEVICE thread is always either running or delayed; it should
      never be blocked.

    Each DEVICE thread runs to completion, i.e., it runs until it explicitly
yields (calling OS Next(void)) or terminates (calling OS Terminate(void)).
(Note: OS Next(void) is the same as “next” as discussed above.) When
a DEVICE thread yields, it will be delayed until its next period begins. A
delayed DEVICE thread is not blocked; it is waiting for its next execution
cycle to begin.

      The timing requirements, periods and jitters, of DEVICE threads
      are guaranteed.

No thread can preempt a DEVICE thread, not even another DEVICE thread.
The only exception is when it is executing longer than its assigned period.
When this occurs, a DEVICE thread is terminated with an error message.
    The CPU utilization of a single DEVICE thread is its processing time
divided by its period. For example, if a DEVICE thread has a period of 50
milliseconds and it takes 5 milliseconds of processing per period, then its CPU
utilization is 5/50, or 10%. The actual processing time of a DEVICE thread
must be less than its assigned period. Throughout the course of execution,
the RTOS collects statistics about total CPU utilization based on processing
times versus periods. The combined total of CPU utilization of all DEVICE
threads should not exceed 50%.10 If so, either we have to modify their periods
to decrease their CPU utilization, or we need to redesign our application
to consider fewer DEVICE threads. To reduce the overall CPU utilization,
for example, a non-time-critical DEVICE thread could be implemented as a
SPORADIC thread (discussed below) which is then synchronized with another
DEVICE thread when processing is necessary.
    For example, a watchdog could be implemented as a DEVICE thread. We
can create it with a period of 1 second and a jitter of 20%. It will “reset”
the application if the ok status is not set within 5 seconds.
  9
     Preferably, an error message is generated so that an application programmer may
revise his overall jitter requirements.
  10
     Rule of thumb.


                                         9
  int ok = 1;
  void watchdog (void){
     int notok = 0;
     for(;;) {
        if (!ok) { ++notok; }
        else { notok = 0;}
        if (notok == 5) reset();
        ok = 0;
        OS_Next();
     }
  }
  main() {
     OS_CreateDevice( watchdog, 0, 1000, 20 );
  }

2.2    PLANNED Level Threads
A PLANNED level thread is scheduled according to a fixed cyclic scheduling
plan, specified by an array PPP[], called Periodic Process Plan. To create a
PLANNED level thread, we call:

Pid   OS_CreatePlanned( void (*f)(void), int argument, int name )

When a PLANNED thread is created, it is assigned a unique name. This name is
fixed to this thread and remains so until the thread is terminated. The PPP[]
array is a sequence of names and intervals specifying the execution order and
timing intervals of all PLANNED threads. The name of a PLANNED thread must
appear in the PPP[] array at least once, but may be more than once.
   For example, if we create three PLANNED threads with names A, B and C
out of three functions P, Q and R respectively,

  {
      OS_CreatePlanned( P, 0, A );
      OS_CreatePlanned( Q, 0, B );
      OS_CreatePlanned( R, 0, C );
  }

then, the periodic process plan PPP[] = { A, 5, B, 10, A, 10, C, 20 }
specifies that A is executing first (i.e., P), then B (i.e., Q), then A again, then

                                       10
C, then A again, and so on. The plan is cyclic; it repeats itself once the
end is reached. If P terminates, but the name A is later assigned to another
thread S (which may or may not execute the same function as P), then the
new A (i.e., S) will be executed again according to PPP[] order. In a sense,
the PPP[] specifies at least a single execution cycle of all PLANNED threads.
The plan uses only names, not the actual functions, to refer to the threads.
Different instances of the same function must be assigned a different name.
Each name may bind to a different PLANNED thread at runtime, if desired.
    In addition, each PLANNED thread is assigned a maximum CPU time per
occurrence in PPP[], a time interval immediately after each occurrence. The
name IDLE is reserved for introducing explicit CPU idle time. For example,
       PPP[]={A,5,IDLE,2,B,3,IDLE,3,A,10,C,7,IDLE,10};
means after completing A within 5 milliseconds, the processor idles for 2 mil-
liseconds, then starts B for 3 milliseconds, then idles for another 3 millisec-
onds, then starts A again for 10 milliseconds, then starts C for 7 milliseconds,
then idles for 10 milliseconds, then repeats all over again. The total cycle
time of all PLANNED threads in this case is therefore 40 milliseconds. Each
PLANNED thread is guaranteed to execute exactly according to the specified
order of PPP[] and the maximum allowable intervals. If there is no thread
associated with a name, then the name in PPP[] is skipped and the following
interval is treated as IDLE.
    A PLANNED thread runs until it yields or terminates. During execution,
it may be preempted by a ready DEVICE thread. When a PLANNED thread
yields (i.e., calling OS Next()), it is delayed until its name appears next in
the PPP[] array. Therefore, a PLANNED thread is either running or delayed,
but never blocked.
    The execution period of a PLANNED thread is not a simple sum of all
intervals since its name may appear more than once in PPP[]. But, in general,
it is a simple calculation of the period of a PLANNED thread by examining
PPP[].
    If a PLANNED thread uses up more processing time than its currently as-
signed interval, it will be terminated with a message. By monitoring the
processing time of each PLANNED thread, we can determine the total CPU
utilization of all PLANNED threads. The combined total of all DEVICE and
PLANNED threads should not exceed 70%.11 The remaining CPU time is de-
voted to the SPORADIC thread.
 11
      Another rule of thumb.

                                      11
2.3    SPORADIC Level Threads
To understand when a SPORADIC thread is executing, we must first define
when a CPU is considered to be idle. The CPU is “idle” if:

   1. all DEVICE, PLANNED and IO threads are delayed; or

   2. the IDLE thread is the currently executing SPORADIC thread.

A SPORADIC thread is either running, ready or blocked, but never delayed.12
A ready SPORADIC thread is allowed to run whenever the CPU is idle. A
ready SPORADIC thread runs at the next earliest CPU idle time; it has no
timing requirements. To create a SPORADIC thread, we call

Pid    OS_CreateSporadic(void (*f)(void), int argument, int urgency);

where urgency is a relative ranking among the SPORADIC threads. SPORADIC
threads are scheduled based on their urgency values. The higher the urgency
value, the sooner a SPORADIC thread is scheduled. A running SPORADIC
thread may sleep (i.e., calling OS Sleep(int)) or block (i.e., waiting on an
event semaphore) to give other SPORADIC threads an opportunity to execute.

2.4    Summary
The three scheduling levels are designed to address three very different time-
liness requirements:

   1. periodic execution with stringent jitter requirements;

   2. cyclic execution with complex processing timings and intervals;

   3. responsive execution but without critical timing requirements.

DEVICE level threads should be used when timing jitter cannot be tolerated.
Since they are the highest priority threads, their execution time per period
should be kept to a fixed predictable minimum. The shorter their periods, the
higher the processor utilization. It is important to balance their timeliness
and processor utilization.
  12
    When a SPORADIC thread sleeps, it is blocked, not delayed. A delayed thread is
guaranteed to be timely, while a blocked thread is not.



                                       12
    PLANNED level threads, on the other hand, should be used when processing
and timing requirements exhibit a complex dependency; but otherwise the
overall behavior is still cyclic. It is easy to tell when a periodic task cannot be
expressed simply as a sampling task. When this occurs, it is more appropriate
to deploy it as a PLANNED thread. With our periodic process plan, a designer
is ultimately responsible to specify the “scheduled timings and intervals” of
all PLANNED threads. This is to simplify our scheduling decisions at runtime
and at the same time reduce the cost to a fixed overhead.
    SPORADIC level threads correspond closely to threads in a typical RTOS,
where their thread “priority” is our notion of “urgency”. Urgent means “as
soon as possible”; it doesn’t imply any stringent timing requirements, such as
bounded response time. In our case, the responsiveness of a SPORADIC thread
depends on the periodic “load” of the applications, i.e., how many DEVICE,
PLANNED and IO threads are defined. The worst case would be maximum
contiguous execution of these timely threads, which could be estimated from
the timings specification if their total execution time is known.
    All threads are interruptible by hardware interrupts. By using DEVICE
or PLANNED threads, one is less likely to use timer interrupts. In practice,
using too many interrupts could lead to unpredictability in latency. The
application engineers have full control on timeliness and responsiveness of
the application threads. With our predictable RTOS, engineering effort is
reduced and programming with complex timing requirements becomes more
manageable.


3     RTOS API
We present the Application Programming Interface of our RTOS as imple-
mented in C. Many of the define constants may be changed from platform
to platform. All memory used by the RTOS is allocated statically. No dy-
namic memory allocation scheme is used or supported. Memory protection
is not currently enabled. Only a single hardware interval timer is needed to
maintain our predictable scheduler. A software interrupt is used to initiate a
kernel (or system) call. Any serious runtime exception will abort the RTOS
at this time.
    Every thread has a unique process id (Pid, which is an unsigned integer.
Not all Pids are valid. Only the kernel knows which Pid is valid. A failed
system call may return INVALIDPID as result.


                                        13
#define MAX_PROCESS     32   /* max. number of threads supported */
#define INVALIDPID     0xffffffff   /* id of an invalid thread */
  /* pre-defined Pid type */
typedef int Pid;

Timely threads should not use in total more than 70% of CPU time. These
limits could be adjusted per application.

#define MAX_DEVICE_CPU 50         /* utilization in percentage */
#define MAX_PLANNED_CPU 20

For a user point of view, there are 3 scheduling levels. DEVICE level is the
highest, and SPORADIC the lowest.

  /* scheduling levels */
#define SPORADIC 3      /*       preemptive prioritized aperiodic */
#define IO        2     /*       bandwidth and latency control */
#define PLANNED   1     /*       cyclic, fixed-order, periodic */
#define DEVICE    0     /*       cyclic, periodic */

Among the sporadic threads they are ranked by their urgency values. The
higher the urgency value, the sooner a sporadic thread will be scheduled.

  /* sporadic priority levels */
#define MAX_URGENCY     4 /* 0..3, 3 being the highest */

Every periodic thread has a user-assigned unique integer name, which is used
in the periodic process plan array PPP[]. IDLE is a reserved name.

 /* well-known periodic process name */
#define IDLE        0xffffffff     /* name of an IDLE process */

For the RTOS kernel to run, a user application must initialize an array, PPP[]
which may be empty. PPPLen specifies the number of elements in the array,
which must be a multiple of 2.

  /* PLANNED process scheduling plan */
extern unsigned int PPPLen;
extern unsigned int PPP[];     /* PLANNED process scheduling plan */




                                     14
At startup, the main() function is created as an initialization thread, which
runs at the highest priority and is non-preemptible. This initialization thread
will create all necessary application threads and initialize the application
global state. When it terminates, all other threads will start executing.
OS Abort() is called whenever an unrecoverable error is encountered; as a
result, the RTOS is shut down immediately. In most cases OS Abort() just
execute an infinite loop after displaying a message. In other cases, it can
perform a full hardware and software reset and then restarts the whole ap-
plication.

void OS_Abort(void);

3.1    Threads Management
Threads are created using the following API calls; they may be created dur-
ing initialization inside the main() function, or dynamically during runtime.
Different levels of threads are created using one of the following calls. The
first parameter is a pointer to a function which is created as a thread. The
second parameter is an argument to that thread, which is retrieved using
OS GetParam(). The remaining parameters are as described earlier. The
default stack size of a thread may be changed.

#define K                   1024
#define STACK_SIZE          16*K
typedef unsigned int        u32;

Pid   OS_CreateDevice(void (*f)(void), u32 arg, u32 period, u32 jitter);
Pid   OS_CreatePlanned(void (*f)(void), u32 arg, u32 name);
Pid   OS_CreateIO(void (*f)(void), u32 arg, u32 bandwidth, u32 latency);
Pid   OS_CreateSporadic(void (*f)(void), u32 arg, u32 urgency);

A thread is terminated when it calls OS Terminate(); its memory resources
are reclaimed. Since a thread is created to execute a function (i.e., the first
parameter in the Create calls above), when the function reaches its end
or when it returns, the associated thread is terminated automatically. For
DEVICE, PLANNED or IO threads, calling OS Next() means it has completed
its processing for the current period/interval; as a result, the remaining time
is idle. During the current period or interval, a timely (DEVICE or PLANNED)
thread may request to be delayed for a few milliseconds with respect to the

                                      15
beginning of the period or interval. For SPORADIC threads, calling OS Next()
is amount to yielding to another SPORADIC thread of equal urgency. The
call OS DelayUntil(n) delays a timely thread until exactly n milliseconds
after the beginning of the current period or interval, and n must be less than
the current period or interval. (Note that the thread is delayed relative to the
beginning of the period/interval, not to the time of call.) OS DelayUntil()
has no meaning for SPORADIC threads.

void OS_Terminate(void);
void OS_Next(void);
void OS_DelayUntil(u32 milliseconds);

Using our example of a complex timing thread Q discussed earlier, we can
specify it as a DEVICE thread with a period of 100 milliseconds:

      thread Q {
          // each period is 100 msec.
          for(;;) {
             write( high );           // pull signal high
             OS_DelayUntil( 15 ); // wait for 15 msec
             write( low );      // pull signal low
             OS_DelayUntil( 25 ); // wait another 25 msec;
             read( status );
             OS_Next();
          }
      }

Each thread may retrieve its initial argument by calling OS GetParam(). To
find out which level it is assigned to, a thread may call OS GetLevel().

u32    OS_GetParam(void);
u32    OS_GetLevel(void);

Only SPORADIC threads may put themselves to sleep voluntarily by calling
OS Sleep(). The sleeping period is at least as specified, but may be longer;
there is no timing guarantee since there may be no immediate CPU idle time
when it wakes up.

void OS_Sleep(u32 milliseconds);


                                      16
For all other timely threads, they may be suspended temporarily until re-
sumption. A thread is not allowed to suspend itself. A thread suspended by
another thread p must be resumed by p.
void OS_Suspend(Pid p);
void OS_Resume(Pid p);
A suspended timely thread can only be ready or delayed, but can never be
running. Its allocated period becomes idle time. When it is resumed, it
continues as a regular timely thread. Timely threads suspension/resumption
should be used only when all CPU utilization bounds are known, i.e., the
total of all timely threads is under 70%. SPORADIC threads may not be
suspended since they can be blocked, e.g., on Mutex or Semaphore, etc.

3.2     Interprocess Communication
3.2.1   Mutexes and Conditions
A Mutex is a binary semaphore for mutual exclusion purposes only, i.e., crit-
ical sections. The call IPC MutexNew() returns a new Mutex and initializes
it to be unlocked; it returns INVALIDMUTEX if none could be created. When a
thread p locks a Mutex m when m is unlocked, i.e., calling IPC MutexLock(m),
p owns m. Only p is allowed to unlock m. Other threads trying to lock m must
wait until m is unlocked. IPC MutexLock() is guaranteed to be fair; a thread
waiting on a lock will eventually gets it. (Note: Only SPORADIC threads are
allowed to use Mutexes and Conditions.)
#define MAX_MUTEX   10
#define INVALIDMUTEX -1

typedef unsigned int Mutex;

Mutex IPC_MutexNew();
void IPC_MutexLock( Mutex m );
void IPC_MutexUnLock( Mutex m );
A Condition is a waiting queue with an associated predicate. The evaluation
of the predicate is a critical section and must be enclosed by IPC MutexLock()
and IPC MutexUnLock(). After locking a Mutex m, if the evaluation of the
predicate is false, then the thread must unlock m and wait until the predicate

                                     17
is changed. This is achieved by calling IPC CondWait( c, m ) where c is
the waiting queue associated with the predicate. When the waiting thread is
later resumed from a IPC CondSignal( c ), it is guaranteed to regain the
lock of the enclosing Mutex again. IPC CondWait() always blocks the calling
thread; IPC CondSignal() never blocks. Conditions must be used together
with Mutexes, but not vice-versa.
#define MAX_COND 10
#define INVALIDCOND -1

typedef unsigned int Cond;

Cond    IPC_CondNew();
void    IPC_CondWait( Cond c, Mutex m );
void    IPC_CondSignal( Cond c );
void    IPC_CondBroadcast( Cond c );

3.2.2    Semaphores
A Semaphore is an unsigned integer with two associated indivisible opera-
tions:
   • IPC SemWait(s) waits until the semaphore s > 0, then decrements s
     by 1;
   • IPC SemSignal(s) increments the semaphore s by 1; as a result, one
     of the threads waiting on s is resumed if there is one.
The call IPC SemNew( v ) returns a new semaphore with an initial value v;
it returns INVALIDSEM if none can be created. A semaphore should be used
mainly for synchronization purposes only. For mutual exclusion, use Mutex
instead.
#define MAX_SEM 10
#define INVALIDSEM -1

typedef unsigned int Semaphore;

Semaphore IPC_SemNew( unsigned int v );
void IPC_SemWait( Semaphore s );
void IPC_SemSignal( Semaphore s );

                                    18
A special Readers/Writers semaphore permits multiple readers to read con-
currently. All writers must wait while readers are reading. When the last
reader has done reading, a writer is then allowed to write. When a writer is
writing, all readers and writers must wait. When a writer is done, another
reader is (or readers are) allowed to read, other writers must wait. That is,
readers take precedence over writers.
   The call IPC RWSemNew() returns a new Readers/Writers semaphore; it
returns INVALIDRWSEM if none can be created.

   • IPC SemRead( s ) is called when a reader wants to initiate a read
     operation; the calling thread waits until s is free or readable;

   • IPC SemDoneRead( s ) is called when a read has completed its reading;

   • IPC SemWrite( s ) is called when a writer wants to initiate a write
     operation; the calling thread waits until s is free;

   • IPC SemDoneWrite( s ) is called when a writer has completed its writ-
     ing.

A Readers/Writers semaphore is free if no thread is reading or writing. When
a RW semaphore is being read, it is readable; it becomes free again when the
last reader has completed its reading. When a RW semaphore is being writ-
ten, it is occupied; it becomes free when the writer has completed its writing.
When multiple readers and writers are waiting while a RW semaphore is oc-
cupied, the readers will be resumed first when the RW semaphore becomes
free again.

#define MAX_RWSEM 10
#define INVALIDRWSEM -1

typedef unsigned int RWSemaphore;

RWSemaphore IPC_NewRWSem();
void IPC_SemRead( RWSemaphore s );
void IPC_SemDoneRead( RWSemaphore s );
void IPC_SemWrite( RWSemaphore s );
void IPC_SemDoneWrite( RWSemaphore s );



                                      19
3.2.3   Events
An Event is an abstraction of a maskable hardware interrupt. An interrupt
signals an event and resumes an event handler. The interrupt is essentially
masked when there is no waiting handler. When a handler is expecting an
interrupt, it calls IPC EventWait( e ), where the event e is associated with
the interrupt. More than one handler may be waiting on the same event.
When an interrupt occurs, it calls IPC EventBroadcast( e ) which resumes
all waiting handlers on e, or IPC EventSignal( e ) which resumes a single
handler. An Event has no memory (unlike a binary semaphore); signaling an
event when there is no waiting handler is a noop. The call IPC EventNew()
returns a new event or INVALIDEVENT if none can be created.

#define MAX_EVENT 10
#define INVALIDEVENT -1

typedef unsigned int Event;

Event IPC_EventNew();
void IPC_EventWait( Event e );
void IPC_EventSignal( Event e );
void IPC_EventBroadcast( Event e );

  /* for H/W interrupt handlers only! */
void IPC_AsyncEventSignal( Event e );
void IPC_AsyncEventBroadcast( Event e );

The IPC AsyncEventSignal() and IPC AsyncEventBroadcast() are opti-
mized versions of IPC EventSignal() and IPC EventBroadcast(); they are
designed specifically to be interrupt safe and interrupt latency efficient.

3.2.4   Summary
All DEVICE, PLANNED and IO threads are not allowed to use blocking IPC
calls, since they have stringent timing requirements. It is important to keep
this in mind; otherwise, there will be priority inversion problems which will
destroy all timeliness guarantee. But, timely threads are allowed to use non-
blocking IPC calls, such as OS EventSignal(), OS EventBroadcast(), or
OS SemSignal(). Using Events, a timely thread may trigger other threads

                                     20
on a regular basis, thus achieving a soft timer. By separating the synchro-
nization and timing requirements can we then hope to achieve a predictable
application.


4    A Simple Example
Using the examples discussed in the introduction, we create four threads, one
DEVICE, one PLANNED and two SPORADIC. The DEVICE thread is P which has a
sampling rate of 30 milliseconds and jitter tolerance of +/- 1%. The PLANNED
thread is Q which follows the specified periodic process plan. Notice how the
individual interval in PPPMax[] matches with how Q is scheduled. (Note:
The execution of P may affect slightly the timing of Q. If it is not acceptable,
then we can incorporate P into the periodic process plan with Q. As a result,
there will be less timing conflict between them.) Finally, two instances of
R are created as SPORADIC threads, which do not have any stringent timing
requirements except they need to dosomething regularly. The first R has
a sleeping period of 150 milliseconds, the second of 250 milliseconds. The
first is more urgent than the second one. They only perform their repeated
operations 1000 times and then stop.
     main() {
       OS_CreateDevice( P, 0, 30, 1 ); /* period = 30 msec; jitter = 1% */
       OS_CreatePlanned( Q, 0, Qt );
       OS_CreateSporadic( R, 150, 3 );
       OS_CreateSporadic( R, 250, 2 );
       OS_Start(); /* boot the RTOS */
     }

     void P() {
       for(;;) {
         read( sample );
         OS_Next();
       }
     }

     #define Qt 1 /* name of thread Q */
     unsigned int PPPLen = 8;
     unsigned int PPP = { Qt, 15, Qt, 25, Qt, 10, IDLE, 50 };

                                      21
void Q() {
    /* each cycle takes 100 msec. */
    for(;;) {
       write( high );      /* pull signal high */
       OS_Next();         /* end of 1st interval */
       write( low );       /* pull signal low */
       OS_Next();         /* end of 2nd interval */
       read( status );     /* read status */
       OS_Next();         /* end of 3rd interval */
    }
}

void R() {
    unsigned int period = OS_GetParam();
    for(i = 0; i < 1000; ++i) {
       dosomething();
       OS_Sleep( period );
    }
    OS_Terminate();
}




                          22