Efficacité des IPC : les signaux temps-réel

Publié par cpb
Oct 02 2011

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 signal
  • signal : le numéro que l’on indique avec les notations SIGRTMIN, SIGRTMIN+1, SIGTRMIN+2SIGRTMAX-1, SIGRTMAX
  • valeur : cette union contient deux champs superposés : sival_int et sival_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 contenir SA_SIGINFO pour que la valeur accompagnant le signal soit transmise au gestionnaire,
  • sa_mask est généralement rempli entièrement avec sigfillset() pour éviter que le handler puisse être interrompu par un autre signal.
  • sa_sigaction contient le pointeur sur la fonction handler.

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.

Une réponse

  1. V.Daanen dit :

    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

URL de trackback pour cette page