[Blog] How to ensure tasks’ timing properties: ASTERIOS vs POSIX-like RTOS

Previous blog posts introduced the principles behind the PsyC programming language and most of those behind the execution runtime used by ASTERIOS RTK. Here are these principles with a link to each dedicated blog post:

These concepts and principles are, of course, not the only ones allowing to design a real-time system. In fact, most of them are implemented using asynchronous real-time operating-systems and plain C as programming language, either by writing them by hand or by generating them from a higher description model. Thus, in this blog post, I will show how POSIX can be used to implement basic real-time applications and will point out the differences, advantages and disadvantages in comparison to ASTERIOS RTK regarding:

  • Determinism: how to ensure tasks’ timing properties and communications?
  • Implementation complexity: what’s easier with an API when it comes to implementation? What are the drawbacks in comparison to a dedicated programming model like Psy?

For each example, a POSIX implementation will be proposed and discussed, and finally the equivalent PsyC implementation will be shown.

Why POSIX?

Almost all RTOS rely not directly on POSIX but on dedicated C APIs which in turn rely on POSIX. Thus, instead of picking one RTOS API in particular, we chose to compare ASTERIOS technology with the common factor of most of the RTOS on the market: POSIX.

« Hello World »

Let’s start with the mother of all examples when it comes to software programming: the « Hello world » example.
Our time-triggered « Hello world » application shall be composed of a single periodic task printing the message « Hello World » every second with a tolerance on the task’s jitter of +/- 1 second:

Hello World design

Figure 1. Time-triggered « Hello World » timeline

POSIX

For our POSIX-based real-time “Hello World”, we need a POSIX process, a periodic POSIX timer and a POSIX signal. The design is as follows: the process will wait for a user signal, then print “Hello World” once, then wait again for the signal and so on. The signal will be emitted with a period of 1 second using a timer configured accordingly.

Starting by the POSIX version, let’s define the code of the task:

/* (1) */
#include <signal.h>
#include <stdio.h>
/* (2) */
#define TICK (SIGRTMIN)
/* (3) */
int main(void) {
  /* (4) */
  sigset_t waiting_signals;
  sigemptyset(&waiting_signals);
  sigaddset(&waiting_signals, TICK);
  /* (5) */
  handle_deadline_missed();
  /* (6) */
  initialize_periodic_timer();
  /* (7) */
  while (1) {
    /* (8) */
    int received_signal;
    sigwait(&waiting_signals, &received_signal);
    /* (9) */
    printf("Hello World\n");
  }
  return 0; /* Never reached */
}

  1. C headers inclusion: signal.h defines POSIX signals API and stdio.h defines the printf prototype used by the main task;
  2. Definition of a user signal used to synchronize tasks on a period;
  3. Definition of our main task. We use the main function here because only one task is necessary in this example;
  4. The task is configured to wait for the TICK signal, the system will redirect this signal to the current process when it is emitted;
  5. If the TICK signal is emitted while the task is still printing « Hello World » (and thus hasn’t finished its E.A.), it’s a deadline missed error that we need to detect and react to. This function is defined afterward and is responsible for configuring the system to let the application capture any pending TICK signal not waited for by the main task. Note that this behavior is native in ASTERIOS RTK, any ASTERIOS task that misses its deadline is detected by the system and the kernel error management is responsible for dealing with the error according to a user configuration;
  6. Function that initializes the POSIX timer to send the TICK signal every 1 second. It is defined below;
  7. Main task’s periodic treatment;
  8. Wait for the TICK signal. This ensures that the main task is executed according to the period of emission of the TICK signal;
  9. Print « Hello World », then loop and wait again for the signal.

Here is the code that initializes the POSIX timer:

/* (1) */
#include <time.h>
void initialize_periodic_timer(void) {
  clockid_t clock;
  sigevent_t sig_config;
  struct itimerspec timer_config;
  timer_t timer;
  /* (2) */
  sig_config.sigev_notify = SIGEV_SIGNAL;
  sig_config.sigev_signo = TICK;
  timer_create(CLOCK_REALTIME, &sig_config, &timer);
  /* (3) */
  timer_config.it_value.tv_sec = 1;
  timer_config.it_value.tv_nsec = 0;
  timer_config.it_interval.tv_sec = timer_config.it_value.tv_sec;
  timer_config.it_interval.tv_nsec = timer_config.it_value.tv_nsec;
  timer_settime(timer, 0, &timer_config, NULL);
}

  1. C headers inclusion: time.h defines POSIX timer API;
  2. Creation of a POSIX timer configured to signal the current process by emitting the TICK signal;
  3. Configuration of the periodicity of our created timer. Here the timer is configured to trigger every 1 second and be automatically reloaded.

Finally, here is the code of the function handle_deadline_missed() which configures the application to detect deadline missed errors:

/* (1) */
static void deadline_missed() {
  printf("error: deadline missed!\n");
}
void handle_deadline_missed(void) {
  struct sigaction config;
  /* (2) */
  config.sa_handler = &deadline_missed;
  config.sa_flags = 0;
  config.sa_restorer = NULL;
  sigemptyset(&config.sa_mask);
  sigaction(TICK, &config, &config);
}

  1. Definition of the error handler function which prints an error message;
  2. Configure the application to catch pending TICK signals not already captured using the handler function deadline_missed.

What we can say here is that the POSIX API requires a lot of configuration code to implement a simple periodic task, as opposed to ASTERIOS RTK where the PsyC code remains focused on the functional part while most of the configuration is managed by the ASTERIOS tool chain.

A second point to mention concerns the guarantee that the designed application corresponds to the initial specification. Indeed, designing any application using POSIX API needs to be done fully-dynamically. Tasks, timers, signals, execution periods, etc… All are configured at run-time. This prevents the possibility of validating parts of the system off-line, like the fact that the scheduling or the communication patterns are designed as expected.

Last but not least, the real-time behavior of the application is dependent on the latency of the underlying operating system. This latency can be lowered down on many systems (PREEMPT-RT for linux for example) until it fits the application real-time expectations, but it requires potentially long iterations of integration and testing.

ASTERIOS

Now that we have examined in details how to write this application using POSIX API, let’s see how to do this within the ASTERIOS development environment.

ASTERIOS is the development environment developed by KRONO-SAFE. It is composed of:

  • A graphical IDE which allows users to edit, debug and maintain PsyC code projects;
  • A compilation toolchain that takes PsyC code as input and produces executable files;
  • An RTK, a real-time microkernel that supports the PsyC runtime interface and enables PsyC programs to be executed on different single- or multi-core hardware or on our simulator;

As you can see, the PsyC language is the input of the ASTERIOS tools and ASTERIOS RTK, and can thus be compared to the POSIX C API for the design and the implementation of real-time critical applications.

Here is the PsyC code needed to implement an application that conforms to the “Hello World” specification. These few lines of code are equivalent to the 3 previous POSIX-based code blocks:

/* (1) */
#include <asterios.h>
#include <stdio.h>
/* (2) */
agent hello (uses realtime, starttime 1, defaultclock ast_realtime_s) {
    /* (3) */
    body start {
        /* (4) */
        printf("Hello World\n");
        /* (5) */
        advance 1;
    }
}

  1. C headers inclusion: asterios.h defines the realtime source and clocks that are based on multiple of microsecond. This header defines the following base clocks:
    • ast_realtime_us: PsyC clock of period 1 microsecond;
    • ast_realtime_ms: PsyC clock of period 1 millisecond;
    • ast_realtime_s: PsyC clock of period 1 second.
  2. Definition of our main task. Its an agent named hello which is configured to use the realtime source and the clock ast_realtime_s as default clock;
  3. Each PsyC agent needs to be defined with at least a start body. With the lines 1 and 2, the hello task is configured to start after the first tick of the clock ast_realtime_s, by executing its body start;
  4. Print « Hello World »;
  5. Advance to, e.g. terminate its current treatment and wait for the next tick of the agent’s default clock, which is ast_realtime_s.

So, it’s obvious that fewer lines of code were necessary to implement the same specification. The PsyC being a parallel synchronous programming language, concepts like tasks and temporal synchronizations are embedded within it, as opposed to POSIX which is nothing but a general-purpose C programming API. When it comes to real-time critical application design, the PsyC lets the developer focus on what matters, which is the functional part of the application (e.g. the application logic), and provides the appropriate concepts, controls structures and tools.

By using POSIX, you need to deal with more general-purpose concepts like threads, signals, timers, etc. and make them fit the role you want them to play. This requires more choices to be made by the developer and more code to be implemented, leading to a higher probability of introducing bugs in the program, and requiring more tests to be written to validate parts that are not functional (is the timer correctly configured, let’s say to automatically reload itself after each trigger for example ?).

Finally, the tasks scheduling being computed offline with ASTERIOS RTK, you are able to bring proofs that your application conforms to the initial specification before running it. If for example you introduce a bug in the above program by using ast_realtime_ms instead of ast_realtime_s and you tell the compiler that one body iteration needed at least 200 milliseconds to be fully executed, then it will reject it with an error telling that there is not enough time to execute an iteration taking at least 200 milliseconds in a 1 millisecond interval.

In the example code below, hello-wrong.psy is a modified version of hello.psy with the addition of the keyword timebudget linked to the advance. In this particular example, it tells the compiler that the code executed in the start body is given a time budget of ITERATION, which is set to 200 milliseconds in the .bgt file.

/* (1) */
#include <asterios.h>
#include <stdio.h>
/* (2) */
agent hello (uses realtime, defaultclock ast_realtime_s) {
    /* (3) */
    body start {
        /* (4) */
        printf("Hello World\n");
        /* (5) */
        timebudget ITERATION, advance 1;
    }
}

BGT file
output

As you see, psyko (our compiler is named psyko, for PsyC Kompiler) rejected the program because it has detected that the timing constraints of the agent hello are not matching the declared time budget of execution. Either the budget needs to be lowered down, or the deadline relaxed.

Adding a parallel task to our « Hello World » example

Now that we treated the time-triggered design approach both with POSIX and ASTERIOS RTK on the simplest example « Hello World », let’s now talk about parallelism by adding another simple task in our application. Here is the specification: an additional task shall print « Welcome to Time-Triggered World ! » within a periodic interval of 2 seconds.

Here is a graphical representation of the specification update:

Hello World programming

Figure 2. Parallel update to hello world

POSIX

For the POSIX version, we now need to use either threads or processes for our tasks, and our timer needs to manage multiple timing signals sending periods (every second and every 2 seconds).

Using threads seems to be easier because POSIX pthread API provides a function to send a signal to a given thread only, and if that thread doesn’t catch the signal, the underneath process is able to catch it.

Let’s write our tasks using threads. First of all, here are the definition of the task’s functions:

void* hello() {
  sigset_t waiting_signals;
  sigemptyset(&waiting_signals);
  sigaddset(&waiting_signals, TICK);
  while (1) {
    int received_signal;
    sigwait(&waiting_signals, &received_signal);
    printf("Hello World\n");
  }
  return NULL;
}

void* hello2() {
  sigset_t waiting_signals;
  sigemptyset(&waiting_signals);
  sigaddset(&waiting_signals, TICK);
  while (1) {
    int received_signal;
    sigwait(&waiting_signals, &received_signal);
    printf("Welcome to Time-Triggered World !\n");
  }
  return NULL;
}


Then, we need to start the threads:

/* (1) */
#include <pthread.h>
/* (2) */
#define NUM_TASKS (2)
/* (3) */
pthread_t threads[NUM_TASKS];
int periods[NUM_TASKS];
/* (4) */
int main() {
  /* (5) */
  pthread_create(&threads[0], NULL, &hello, 0);
  pthread_create(&threads[1], NULL, &hello2, 0);
  
  /* (6) */
  periods[0] = 1;
  periods[1] = 2;
  
  handle_deadline_missed();
  initialize_periodic_timer();
  
  while (1);
  return 0;
}

  1. Inclusion of the pthread header file which defines the POSIX threads API;
  2. Define the number of tasks present in the application;
  3. Define two arrays of the size of the number of tasks present in the application, the first storing the handles to the created threads and the second storing the periods of the associated task;
  4. Application’s entry point, where all is configured. Note that the main function no longer implements a task of the specified system;
  5. Creation of two threads to execute the two tasks;
  6. Register the tasks’ periods for future, by the timer most certainly.

Finally, we need to modify the timer to send signals to the appropriate threads at the appropriate date. We can do this in multiple ways, with a single timer ticking at a period equals to the Greatest Common Divisor (GCD) of the task’s periods, and comparing the period of each task with the current timer time before sending the signal. We could also use one timer per task with different period to lower the computation time necessary at the cost of increased latency (due to the fact that the system may not trigger the timers « at the same time »).

I’ll let you with that explanation and won’t right an example code here; I think you get the general idea anyway.

ASTERIOS

In PsyC, all we need to do is to add a new agent that looks very similar to the first one. The only change is its default clock which needs to be synchronized on a period of 2 seconds instead of 1.

Here is the code:

#include <asterios.h>
#include <stdio.h>

/* (1) */
agent hello (uses realtime, defaultclock ast_realtime_s) {
  body start {
    printf("Hello World\n");
    advance 1;
  }
}

/* (2) */
agent hello2 (uses realtime, defaultclock ast_realtime_s) {
  body start {
    printf("Welcome to Time-Triggered World !\n");
    /* (3) */
    advance 2;
  }
}

  1. Definition of our previous hello agent;
  2. Definition of our additional task which will run in parallel;
  3. The additional task advances to the next 2 seconds at each iteration. That’s the only difference between both of them, aside from the printed message of course.

You can see that expressing parallelism is very straightforward in PsyC, and the specification requirements based on timing constraints can be written almost as-is in PsyC. Fewer code where necessary to the implementation by using ASTERIOS. One can also note that the design of the POSIX version had to be updated since the first version was designed to handle only one task, whereas the design of the ASTERIOS version didn’t change at all. Adding a second agent similar to the first one was the only thing updated.

Conclusion

Albert Einstein said about physics: « The formulation of the problem is often more essential than its solution, which may be merely a matter of mathematical or experimental skill« . Talking about computer science, it could be translated to: « The formulation of the problem is often more essential than its solution, which may be merely a matter of algorithmic or coding skill« .

The POSIX API is great because it’s a huge toolbox that can address most of the problems encountered in computer science. It is designed for that purpose. Most real-time operating systems use a super-set API based on POSIX to let programmers create periodic tasks for example and abstract all the gory details. Still, the base concepts used are those of POSIX, and these base concepts don’t target real-time critical problems.

ASTERIOS, with its parallel and synchronous language PsyC, lets users formulate the problem (e.g. the software design) with concepts that are appropriate to real-time time-triggered problems:

  • Agents, which are parallel threads of execution synchronized on Clock ticks;
  • Clocks, which are “periodic timers” based on Sources;
  • Sources, which are abstraction of any hardware component that is able to send ticks, like a hardware timer, the teeth of a crankshaft, etc.;
  • Communication means to let parallel tasks communicate in a deterministic manner.

That’s all it takes to express arbitrarily complicated real-time parallel applications.

The PsyC is a simple yet powerful programming language. It’s quite easy to learn how to program in PsyC and it is also quite easy to read PsyC code and understand the functional specification that is implemented.

The examples presented in this article show the expressiveness of the PsyC language regarding non-communicating periodic tasks, but the PsyC can do more, like describing tasks with non-periodic patterns (synchronizations conditioned by if statements) and allowing parallel tasks to communicate in a deterministic, lock-free, wait-free manner.

In this blog post, we talk about multitasks application design and compared ASTERIOS RTK to POSIX-based systems. It could also be interesting to compare the ASTERIOS RTK synchronous, deterministic approach with the POSIX asynchronous, dynamic one concerning tasks’ communication, and highlights the main advantages that ASTERIOS RTK brings to avoid deadlocks, starving, priority inheritance and parallel communications debugging nightmares that most asynchronous system users already know of. In a future post maybe… stay tuned !

About the author

Vincent CAMUS

Vincent CAMUS is an engineering manager at KRONO-SAFE with 8 years of experience in software. He was initially part of the team that wrote the PsyC language compiler and the real-time kernel of ASTERIOS. He later took the lead of this team. Vincent currently manage the development of the ASTERIOS Developer product, our PsyC language development environment composed of a compiler, a debugger, a simulator and a GUI editor.