Nous avions examiné dans l’article précédent de cette série un premier mécanisme de communication entre processus : les files de messages Posix. Nous avions observé qu’il était facile de transmettre un message de quelques kilo-octets entre deux processus, en un temps allant de 8 microsecondes si le processeur était déjà actif à 15 microsecondes si les messages étaient suffisament espacés pour laisser au processeur le temps de s’endormir et de prendre un temps de réveil non négligeable.
Nous allons à présent observer les possibilités liées aux signaux temps-réel Posix.1b.
Signaux temps-réel
Les signaux temps-réel introduits par la norme Posix.1b (et qui n’ont de temps-réel que leur nom) ne sont ni plus rapides ni plus prioritaires que les signaux classiques Unix. Ils sont toutefois un peu plus fiables et peuvent nous offrir un moyen assez méconnu de communiquer entre des processus.
Les principales différences entre signaux classiques et signaux temps-réel sont les suivantes.
- Les signaux temps-réel ne sont jamais émis spontanément par le système, ils sont à la disposition du développeur applicatif. Ils doit y en avoir au moins 8 (sous Linux ils sont 32) dans l’intervalle
[SIGRTMIN, SIGRTMAX]
. - Lorsque la délivrance d’un signal classique est bloquée temporairement par un processus, et que plusieurs occurrences de ce signal arrivent, un seul sera délivré lorsque le processus le débloquera. Les signaux temps-réel en attente sont mis en file.
- Si plusieurs signaux temps-réel doivent être délivrés à un processus, ils lui arrivent par ordre croissant. Avec les signaux classiques, il n’y a aucun ordre normalisé.
- Enfin, un signal temps-réel peut être accompagné d’un entier (ou d’un pointeur mais ça ne sert pratiquement jamais), à la différence d’un signal classique qui ne sert que de notification.
On peut donc utiliser les signaux temps-réel lorsque le message à transmettre est très court (idéalement la taille d’un entier).
Émission
On envoie un signal temps-réel avec l’appel sigqueue()
#include <signal.h> int sigqueue(pid_t pid, int signal, const union sigval valeur);
dont les arguments sont
pid
: la cible visée par le signalsignal
: le numéro que l’on indique avec les notationsSIGRTMIN
,SIGRTMIN+1
,SIGTRMIN+2
…SIGRTMAX-1
,SIGRTMAX
valeur
: cette union contient deux champs superposés :sival_int
etsival_ptr
qui transmettent respectivement un entier ou un pointeur.
Réception
Pour recevoir un signal temps-réel il faudra écrire un gestionnaire légèrement différent de ceux utilisés pour les signaux classiques (afin de pouvoir récuperer la valeur qui l’accompagne).
void handler(int numero, siginfo_t * info, void * unused);
La structure obtenue dans le paramètre info
contient dans son champs si_value
l’union sigval
remplie lors de l’émission. Pour en savoir plus, consultez la page de manuel sigaction(2)
.
Le dernier paramètre, bien qu’inutile est obligatoire ; c’est une question de cohérence de prototype du handler.
L’installation du gestionnaire se fait avec :
#include <signal.h> int sigaction(int numero, const struct sigaction *action, struct sigaction *precedent);
Les champs importants de la structure sigaction
sont les suivants.
sa_flags
doit contenirSA_SIGINFO
pour que la valeur accompagnant le signal soit transmise au gestionnaire,sa_mask
est généralement rempli entièrement avecsigfillset()
pour éviter que le handler puisse être interrompu par un autre signal.sa_sigaction
contient le pointeur sur la fonctionhandler
.
Transfert rare
Nous allons employer un petit programme qui se scinde en deux processus. Le fils envoie un signal temps réel à son père toutes les secondes, accompagné de la valeur des microsecondes de l’heure qu’il vient de lire. Le père lit l’heure courante et celle transmise par son fils, puis affiche par comparaison la durée écoulée. Les fichiers sources et le Makefile
sont dans cette archive.
transfert-signaux-01.c : #include #include #include #include #include static void processus_fils(void); static void processus_pere(void); static void handler(int, siginfo_t *, void *); int main(int argc,char * argv[]) { pid_t pid; pid = fork(); if (pid < 0) { perror("fork"); exit(EXIT_FAILURE); } if (pid == 0) { processus_fils(); } else { processus_pere(); } return EXIT_SUCCESS; } static void processus_fils(void) { struct timeval heure; union sigval value; sleep(2); // Pour etre sur que le pere ait installe son handler while (1) { gettimeofday(& heure, NULL); value.sival_int = heure.tv_usec; sigqueue(getppid(), SIGRTMIN, value); sleep(1); } } static void processus_pere(void) { struct sigaction action; action.sa_flags = SA_SIGINFO; action.sa_sigaction = handler; sigfillset(& action.sa_mask); sigaction(SIGRTMIN, & action, NULL); while (1) pause(); } static void handler(int unused, siginfo_t * info, void * unused_ptr) { struct timeval heure; gettimeofday(& heure, NULL); heure.tv_usec -= info->si_value.sival_int; if (heure.tv_usec < 0) heure.tv_usec += 1000000; fprintf(stdout, "Duree = %ld micro-secondesn", heure.tv_usec); }
Testons notre programme :
$ ./transfert-signaux-01 Duree = 33 micro-secondes Duree = 8 micro-secondes Duree = 18 micro-secondes Duree = 13 micro-secondes Duree = 11 micro-secondes Duree = 10 micro-secondes Duree = 9 micro-secondes Duree = 13 micro-secondes Duree = 17 micro-secondes Duree = 17 micro-secondes Duree = 12 micro-secondes Duree = 17 micro-secondes Duree = 11 micro-secondes Duree = 10 micro-secondes Duree = 17 micro-secondes Duree = 14 micro-secondes Duree = 17 micro-secondes Duree = 16 micro-secondes Duree = 15 micro-secondes Duree = 16 micro-secondes Duree = 20 micro-secondes Duree = 6 micro-secondes Duree = 6 micro-secondes Duree = 14 micro-secondes Duree = 7 micro-secondes (Contrôle-C) $
Transfert à débit élevé
L’inconvénient avec les signaux, est qu’il n’y a aucune garantie que le processus cible ait bien reçu le signal, à moins d’intégrer un mécanisme d’acquittement à un niveau plus élevé. Nous allons donc programmer une partie de ping-pong où chaque processus renvoie un signal à son partenaire dès qu’il en a lui-même reçu un. Nous afficherons les statistiques de durées toutes les secondes.
transfert-signaux-02.c: #include #include #include #include #include static void processus_fils(void); static void processus_pere(void); static void handler_pere(int, siginfo_t *, void *); static void handler_fils(int, siginfo_t *, void *); int main(int argc,char * argv[]) { pid_t pid; pid = fork(); if (pid < 0) { perror("fork"); exit(EXIT_FAILURE); } if (pid == 0) { processus_fils(); } else { processus_pere(); } return EXIT_SUCCESS; } static void processus_fils(void) { struct sigaction action; action.sa_flags = SA_SIGINFO; action.sa_sigaction = handler_fils; sigfillset(& action.sa_mask); sigaction(SIGRTMIN, & action, NULL); sleep(2); // Pour etre sur que le pere ait installe son handler raise(SIGRTMIN); // pour amorcer le ping-pong while (1) pause(); } static void handler_fils(int unused, siginfo_t * info, void * unused_ptr) { union sigval value; struct timeval heure; gettimeofday(& heure, NULL); value.sival_int = heure.tv_usec; sigqueue(getppid(), SIGRTMIN, value); } static void processus_pere(void) { struct sigaction action; action.sa_flags = SA_SIGINFO; action.sa_sigaction = handler_pere; sigfillset(& action.sa_mask); sigaction(SIGRTMIN, & action, NULL); while (1) pause(); } static void handler_pere(int unused, siginfo_t * info, void * unused_ptr) { long int duree; static long int duree_max = 0; static long int duree_min = -1; static long int somme_durees = 0; static long int nb_recus = 0; static struct timeval debut = { 0, 0}; struct timeval heure; gettimeofday(& heure, NULL); duree = heure.tv_usec - info->si_value.sival_int; if (duree < 0) duree += 1000000; if (duree_max < duree) duree_max = duree; if ((duree_min == -1) || (duree_min > duree)) duree_min = duree; somme_durees += duree; nb_recus ++; if (heure.tv_sec != debut.tv_sec) { fprintf(stdout, "min =%3ld max =%3ld recus = %6ld moy=%5.1fn", duree_min, duree_max, nb_recus, ((float) somme_durees) / nb_recus); nb_recus = 0; duree_min = -1; duree_max = 0; somme_durees = 0; debut = heure; } sigqueue(info->si_pid, SIGRTMIN, (const union sigval) 0); }
Les résultats sont intéressants :
$ ./transfert-signaux-02 min = 22 max = 22 recus = 1 moy= 22.0 min = 3 max = 12 recus = 88491 moy= 3.9 min = 3 max = 15 recus = 120337 moy= 4.1 min = 3 max = 15 recus = 120674 moy= 4.0 min = 3 max = 15 recus = 120715 moy= 4.0 min = 3 max = 18 recus = 121055 moy= 4.0 min = 3 max = 16 recus = 120594 moy= 4.1 min = 3 max = 13 recus = 120779 moy= 4.0 min = 3 max = 12 recus = 120779 moy= 4.0 min = 3 max = 20 recus = 121118 moy= 4.0 min = 3 max = 17 recus = 121028 moy= 4.0 min = 3 max = 20 recus = 121122 moy= 4.0 min = 3 max = 20 recus = 120719 moy= 4.0 min = 3 max = 23 recus = 121064 moy= 4.0 min = 3 max = 15 recus = 120781 moy= 4.0 min = 3 max = 15 recus = 121103 moy= 4.0 min = 3 max = 16 recus = 120611 moy= 4.0 min = 3 max = 41 recus = 120611 moy= 4.0 min = 3 max = 34 recus = 122327 moy= 4.0 (Contrôle-C) $
Nous voyons que lorsque le processeur reste éveillé, le temps de passage d’un signal d’un processus à l’autre est très rapide, en moyenne 4 microsecondes !
Conclusion
La quantité d’information transportée par les signaux temps-réel est très légère (un entier), mais cela peut suffire dans certains cas. L’intérêt est également la notion de notification asynchrone qu’induisent par définition les signaux. Le processus cible peut être en train de réaliser des calculs, il sera averti dans un délai bref de l’occurrence d’un événement par un autre processus. Il existe encore d’autres mécanismes IPC, que nous examinerons prochainement.
Bonjour,
j’ai execute les 2 programmes de test sur un cible AAEON processeur x86-64 bits celeron dual core.
J’obtiens des resultats « deroutants »:
* programme 1: temps moyen 20µs
* programme 1: temps moyen 20µs avec une grande dispersion de temps (le temps max peut monter jusqu’a 700µs)..
J’ai observé ce comportement ‘bizarre’ sur d’autres applis et je me demande si cela peut venir d’une mauvaise configuration du noyau (distribution ArchLinux, noyau 4.9.39 patché avec le patch PREEMPT-RT adapté et tick (HZ)=1000
Qu’en pensez-vous ?
Merci
Vincent