La Real-time Specification for Java (RTSJ)

Dépréciations

Quelques parties de la spécification RTSJ précédente sont dépréciées avec RTSJ 2.0. Parmi elles, notons la classe NoHeapRealtimeThread (NHRT), en particulier, qui est remplacée par d'autres combinaisons de services, de même que quelques éléments des interfaces au système d'exploitation (surtout du fait que POSIX ne les supporte plus).

Il existe une spécification d'une plateforme de développement systèmes TR et embarqués pour Java, nommée Real-time Specification for Java (RTSJ). La plus récente incarnation de cette plateforme date de 2024 : https://www.aicas.com/cms/en/rtsj

Extraits choisis

Quelques extraits choisis de la spécification RTSJ 2.0 suivent.

Ordonnancement

« In a realtime system, the system tries to schedule the most critical task that is ready to run first. This task runs either until it is finished, or it needs to wait for some event or data, or a more critical task is released or a more critical task becomes schedulable after waiting for its event or data.

Realtime scheduling is commonly done with a priority preemptive scheduler, where tasks that have short deadlines are given higher priority than tasks that have longer deadlines. The programmer is responsible for encoding some notion of task importance to priorities. The goal is to see that all tasks nish within their deadlines. Scheduling analysis, such as Rate Monotonic Analysis, can be used to help determine this. »

Il est sous-entendu de par l'introduction que l'approche privilégiée pour l'ordonnancement des tâches TR au sens strict du terme est basée sur le respect des échéances, ce qui laisse entendre un biais pour l'ordonnancement sur la base de priorités dynamiques, probablement  de type EDF. Il semble raisonnable de présumer qu'il en va toutefois de la responsabilité des programmeuses et des programmeurs d'accorder aux tâches les plus pressantes la priorité la plus haute.

En pratique, l'ordonnancement minimal pour supporter RTSJ 2.0 est un ordonnancement à priorité fixe (FPS) de 28 niveaux de priorité, les dix plus basses correspondant aux priorités standards (non-TR). Les classes dérivées de Schedulable (les tâches TR, quels que soient les types effectifs) offrent toutefois de l'information plus précise pour permettre à un ordonnanceur plus sophistiqué de prendre le relais. RTSJ permet de remplacer l'ordonnanceur de base par un autre implémentant un algorithme de notre choix.

Transfert de contrôle asynchrone

« Many event-driven computer systems that tightly interact with external physical systems (e.g., humans, machines, control processes, etc.) may require mode changes in their computational behavior as a result of significant changes in the non-computer real-world system. It simplifies the architecture of a system when a task can be programmatically terminated when an external physical system change causes its computation to be superfluous. Without this facility, a thread or set of threads have to be coded so that their computational behavior anticipates all of the possible transitions among possible states of the external system. When the external system makes a state transition, the changes in computation behavior can be managed by an oracle that terminates a set of threads required for the old state of the external system, and invokes a new set of threads appropriate for the new state of the external system. Since the possible state transitions of the external system are encoded in only the oracle and not in each thread, the overall system design is simpler.

There is a second requirement for a mechanism to terminate some computation, where a potentially unbounded computation needs to be done in a bounded period of time. In this case, if that computation can be executed with an algorithm that is iterative, and produces successively re ned results, the system could abandon the computation early and still have usable results. The RTSJ supports aborting a computation by signalling from another thread, or the passage of time, with a feature termed Asynchronous Transfer of Control (ATC). »

Le transfert de contrôle asynchrone (ATC) englobe tous les mécanismes de transfert de contrôle situés hors du comportement « normal » du programme, incluant entre autres les exceptions et les interruptions. Les exceptions de RTSJ peuvent d'ailleurs être créées et véhiculées par des mécanismes différents de ceux de la JVM standard (en particulier la classe AsynchronouslyInterruptedException).

Les méthodes susceptibles de subir un ATC doivent être identifiées en tant que telles. L'exception InterruptedException de la JVM standard sert à titre de signal ATC.

Certains blocs, en particulier les blocs synchronized et les constructions static, sont tels qu'ils doivent être menés à terme pour éviter un interblocage. Conséquemment, tout ATC rencontré en chemin ne sera traité qu'à la fin du bloc.

L'interruption d'un fil d'exécution TR demande un traitement soigneux de la part des programmeuses et des programmeurs, évidemment.

Gestion de la mémoire

« The Java language is designed around automatic memory management, in particular garbage collection. Unfortunately, though garbage collection is a functional safety and security feature, conventional garbage collectors interrupt the normal flow of control in a program. Therefore, garbage-collected memory heaps had been considered an obstacle to realtime programming due to the potential for unpredictable latencies introduced by the garbage collector.

Though convention collectors still have these drawbacks, there are now realtime collectors that can be used for hard realtime application. Still, the RTSJ provides an alternative to garbage collection for systems which require it, either because they do not have a garbage collector or deterministic garbage collector, or require heap partitioning for some other reason. Extensions to the memory model, which support memory management in a manner that does not interfere with the ability of realtime code to provide deterministic behavior, are provided to support these alternatives.

This goal is accomplished by providing memory areas for the allocation of objects outside of the garbage-collected heap for both short-lived and long-lived objects. In order to provide additional separation between the garbage collector and schedulables which do not require its services, a schedulable can be marked no-heap to indicate that it never accesses the heap. »

Les auteurs de la spécification RTSJ 2.0 sont d'avis qu'il existe des moteurs de collecte d'ordures appropriés pour des systèmes TR stricts, mais préconisent tout de même des mécanismes alternatifs.

En particulier, pour éviter des fuites de mémoire, RTSJ propose des zones de mémoire réclamées en bloc, de manière pleinement déterministe, et choisies par les programmeuses et les programmeurs. Ces zones échappent au moteur de collecte d'ordures en étant prises ailleurs, un peu à l'image des variables locales placées sur la pile. Le moteur de collecte d'ordures peut lire dans ces zones (au cas où elles contiendraient des références sur des objets placés sur le tas) mais ne les réclamera pas lui-même.

Pour éviter les accidents, il est possible de marquer certains fils d'exécution comme étant de type No Heap et d'éviter qu'un tel fil ne fasse un new inattendu.

Pour ce qui est de l'accès direct à de la mémoire physique, RTSJ offre des mécanismes sous la forme de :

Cependant, RTSJ exige que les accès à ces zones de mémoire soient faits avec des instructions « normales » de la JVM. L'accès à des zones mémoires qui exigeraient des instructions que la JVM n'offre pas n'est pas supporté par cette spécification.

La sémantique d'accès, les temps d'accès et les temps d'allocation dans chaque cas sont définis à même la spécification RTSJ.

Modularité

« The original RTSJ specification was conceived, with the exception of some optional features, as a monolith specification. This has inhibited the adoption of the RTSJ beyond the hard realtime community, because some of the features were considered to have an overly negative impact on overall JVM performance. Version 2.0 addresses this by breaking the specification into modules[...]

Modules provide a means of grouping like functionality together in a way that promotes maximal adoption for various implementation classes. A conventional JVM could simply implement the Base Module, without providing any realtime guarantees at all, to provide programmers with the benefits of features such as asynchronous event programming as an alternative to conventional threading. A hard realtime implementation could implement all modules to provide the maximal flexibility and functionality to the realtime programmer. Both would benefit from easier migration of code to realtime systems. Every RTSJ implementation shall provide the Base Module functionality, but all other modules are optional. The optional modules are the Device Module and the Alternate Memory Management module »

À l'image de ce vers quoi semblait tendre à l'époque Java 9, la RTSJ 2.0 se veut modulaire. Il semble en effet que l'un des obstacles à l'adoption de la version précédente de cette spécification est qu'elle était présentée comme un « tout ou rien », ce qui a refroidi les ardeurs de bien des implémenteurs.

Synchronisation

L'héritage de priorités doit être implémenté par toute implémentation de RTSJ 2.0. Le protocole de plafonnage de priorité est optionnel.

Périphériques et interruptions

La spécification RTSJ vise entre autres à permettre de déployer des programmes Java TR en tant que modules du noyau d'un système d'exploitation, ou pour réagir à l'occurrence d'un événement tel qu'une interruption. Une partie de la spécification porte sur les outils pour y arriver, et les considérations propres à la latence lors d'une réaction à une interruption y sont documentées.

Système d'exploitation

Enfin, RTSJ offre une interface pour accéder de manière relativement directe aux services du système d'exploitation, mais ces services sont essentiellement construits en fonction des systèmes POSIX.

Petits exemples

Je n'ai pas eu accès à une implémentation de RTSJ récemment, mais voici tout de même quelques mini exemples pour vous donner un aperçu de ce à quoi peut ressembler un programme TR en Java.

Programme Java TR minimaliste

Un programme Java TR minimaliste avec RTSJ serait :

import javax.realtime.RealtimeThread;
public class CoucouTR extends RealtimeThread {
   public void run() {
      System.out.println("J'aime mon prof!");
   }
   public static void main(String [] args) {
      CoucouTR coucou = new CoucouTR();
      coucou.start();
      try {
         coucou.join();
      } catch (InterruptedException ie) {
      } 
   } 
}

Notez que ce fil d'exécution est considéré apériodique car il n'est pas assujetti à une contrainte de constance, donc il n'a pas de période de démarrage fixée a priori.

Ce programme fonctionne du fait que le join() suspend le code appelant jusqu'à complétion du fil d'exécution coucou, et du fait que le fil coucou s'arrêtera de lui-même. En effet, une fois coucou lancé, ce dernier étant un fil TR strict et RTSJ étant une véritable plateforme TR, coucou prendra la main et main(), qui se loge dans un fil d'exécution « normal », ne pourra plus s'exécuter.

Programme Java TR minimaliste... et dysfonctionnel

Toutefois, le programme suivant ne fonctionne pas :

import javax.realtime.RealtimeThread;
public class CoucouTR extends RealtimeThread {
   volatile boolean meurs;
   public CoucouTR() {
      meurs = false;
   }
   public void finExécution() {
      meurs = true;
   }
   public void run() {
      while(!meurs) { // oups!
         System.out.println("J'aime mon prof!");
      }
   }
   public static void main(String [] args) {
      CoucouTR coucou = new CoucouTR();
      coucou.start();
      try {
         coucou.finExécution(); // oups!
         coucou.join();
      } catch (InterruptedException ie) {
      } 
   } 
}

En effet, dans ce cas, coucou ne sera arrêté que lorsque main() aura appelé sa méthode finExécution()... or puisque coucou est un fil d'exécution TR et main() est dans un fil d'exécution normal, il se trouve que main() n'aura jamais l'occasion de s'exécuter.

Exemple de fil d'exécution périodique (contrainte de constance)

 Enfin, un exemple de fil d'exécution TR périodique serait le TiPoints ci-dessous. Notez que cette version s'interrompt après un nombre d'affichages déterminé à la construction, plutôt que d'attendre qu'une touche ait été pressée :

import javax.realtime.PeriodicParameters;
import javax.realtime.RealtimeThread;
import javax.realtime.RelativeTime;
public class TiPoints extends RealtimeThread {
   int nbIt;
   public TiPoints(PeriodicParameters pp, int n) { // on suppose n >= 0 pour simplifier le tout
      super(null, pp);
      nbIt = n;
   }
   public void run() {
      for (int i = 0; i < nbIt; ++i) {
         System.out.print(".");
         waitForNextPeriod();
      }
   }
   public static void main(String [] args) {
      PeriodicParameters pp = new PeriodicParameters(new RelativeTime(1000, 0)); // chaque 1000 ms
      TiPoints tiPts = new TiPoints(pp, 10); 
      tiPts.start();
      try {
         tiPts.join();
      } catch (InterruptedException ie) {
      }
   }
}

Ceci montre d'ailleurs l'intérêt de programmer avec RTSJ, soit l'existence d'outils formalisant les pratiques propres aux systèmes TR (ici, la périodicité avec exécution à taux constant).

Lectures complémentaires

Quelques liens pour enrichir le propos :


Valid XHTML 1.0 Transitional

CSS Valide !