Recherche de site Web

Créer une minuterie sous Linux


Un didacticiel montrant comment créer un minuteur d'intervalle compatible POSIX.

Le timing de certains événements est une tâche courante pour un développeur. Les scénarios courants pour les minuteries sont les chiens de garde, l'exécution cyclique de tâches ou la planification d'événements pour une heure spécifique. Dans cet article, je montre comment créer un minuteur d'intervalle compatible POSIX à l'aide de timer_create(...).

Vous pouvez télécharger le code source des exemples suivants depuis GitHub.

Préparer Qt Creator

J'ai utilisé Qt Creator comme IDE pour cet exemple. Pour exécuter et déboguer l'exemple de code dans Qt Creator, clonez le référentiel GitHub, ouvrez Qt Creator, accédez à Fichier -> Ouvrir un fichier ou un projet... et choisissez CMakeLists.txt :

Ouvrir un projet dans Qt Creator (CC-BY-SA 4.0)

Après avoir sélectionné la chaîne d'outils, cliquez sur Configurer le projet. Le projet contient trois exemples indépendants (nous n'en couvrirons que deux dans cet article). Avec le menu marqué en vert, basculez entre les configurations pour chaque exemple et activez Exécuter dans le terminal pour chacune d'entre elles (voir la marque jaune ci-dessous). L'exemple actuellement actif pour la construction et le débogage peut être sélectionné via le bouton Debug dans le coin inférieur gauche (voir la marque orange ci-dessous) :

Configuration du projet (CC-BY-SA 4.0)

Minuterie d'enfilage

Jetons un coup d'œil à l'exemple simple_threading_timer.c. C'est le plus simple : il montre comment un minuteur d'intervalle est créé, qui appelle la fonction expired à l'expiration. A chaque expiration, un nouveau thread est créé dans lequel la fonction expiration est appelée.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

void expired(union sigval timer_data);

pid_t gettid(void);

struct t_eventData{
    int myData;
};

int main()
{
    int res = 0;
    timer_t timerId = 0;

    struct t_eventData eventData = { .myData = 0 };


    /*  sigevent specifies behaviour on expiration  */
    struct sigevent sev = { 0 };

    /* specify start delay and interval
     * it_value and it_interval must not be zero */

    struct itimerspec its = {   .it_value.tv_sec  = 1,
                                .it_value.tv_nsec = 0,
                                .it_interval.tv_sec  = 1,
                                .it_interval.tv_nsec = 0
                            };

    printf("Simple Threading Timer - thread-id: %d\n", gettid());

    sev.sigev_notify = SIGEV_THREAD;
    sev.sigev_notify_function = &expired;
    sev.sigev_value.sival_ptr = &eventData;


    /* create timer */
    res = timer_create(CLOCK_REALTIME, &sev, &timerId);


    if (res != 0){
        fprintf(stderr, "Error timer_create: %s\n", strerror(errno));
        exit(-1);
    }

    /* start timer */
    res = timer_settime(timerId, 0, &its, NULL);

    if (res != 0){
        fprintf(stderr, "Error timer_settime: %s\n", strerror(errno));
        exit(-1);
    }

    printf("Press ETNER Key to Exit\n");
    while(getchar()!='\n'){}
    return 0;
}


void expired(union sigval timer_data){
    struct t_eventData *data = timer_data.sival_ptr;
    printf("Timer fired %d - thread-id: %d\n", ++data->myData, gettid());
}

L’avantage de cette approche est son faible encombrement, en termes de code et de débogage simple. L'inconvénient est la surcharge supplémentaire due à la création d'un nouveau thread à l'expiration et, par conséquent, le comportement moins déterministe.

Minuterie de signal d'interruption

Une autre possibilité d'être averti d'un temporisateur expiré est basée sur un signal du noyau. Au lieu de créer un nouveau thread à chaque fois que le timer expire, le noyau envoie un signal au processus, le processus est interrompu et le gestionnaire de signal correspondant est appelé.

Comme l'action par défaut lors de la réception d'un signal est de terminer le processus (voir la page de manuel du signal), nous devons préparer Qt Creator à l'avance afin qu'un débogage correct soit possible.

Le comportement par défaut de Qt Creator lorsque le débogué reçoit un signal est :

  • Interrompez l'exécution et passez au contexte du débogueur.
  • Afficher une fenêtre pop-up qui informe l'utilisateur de la réception d'un signal.

Ces deux actions ne sont pas souhaitées car la réception d'un signal fait partie de notre application.

Qt Creator utilise GDB en arrière-plan. Afin d'empêcher GDB d'arrêter l'exécution lorsque le processus reçoit un signal, accédez à Outils -> Options, sélectionnez Débogueur et accédez à Locales et expressions. Ajoutez l'expression suivante à la personnalisation de l'aide au débogage :

handle SIG34 nostop pass

Sig 34 pas d'arrêt avec erreur (CC-BY-SA 4.0)

Vous pouvez trouver plus d'informations sur la gestion du signal GDB dans la documentation GDB.

Ensuite, nous souhaitons supprimer la fenêtre pop-up qui nous avertit à chaque fois qu'un signal est reçu lorsque nous nous arrêtons dans le gestionnaire de signal :

Boîte contextuelle Signal 34 (CC-BY-SA 4.0)

Pour ce faire, accédez à l'onglet GDB et décochez la case cochée :

Fenêtres de signal de minuterie (CC-BY-SA 4.0)

Vous pouvez maintenant déboguer correctement le signal_interrupt_timer. La mise en œuvre réelle du temporisateur de signal est un peu plus complexe :

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

#define UNUSED(x) (void)(x)

static void handler(int sig, siginfo_t *si, void *uc);
pid_t gettid(void);

struct t_eventData{
    int myData;
};

int main()
{
    int res = 0;
    timer_t timerId = 0;


    struct sigevent sev = { 0 };
    struct t_eventData eventData = { .myData = 0 };

    /* specifies the action when receiving a signal */
    struct sigaction sa = { 0 };

    /* specify start delay and interval */
    struct itimerspec its = {   .it_value.tv_sec  = 1,
                                .it_value.tv_nsec = 0,
                                .it_interval.tv_sec  = 1,
                                .it_interval.tv_nsec = 0
                            };

    printf("Signal Interrupt Timer - thread-id: %d\n", gettid());

    sev.sigev_notify = SIGEV_SIGNAL; // Linux-specific
    sev.sigev_signo = SIGRTMIN;
    sev.sigev_value.sival_ptr = &eventData;

    /* create timer */
    res = timer_create(CLOCK_REALTIME, &sev, &timerId);

    if ( res != 0){
        fprintf(stderr, "Error timer_create: %s\n", strerror(errno));
        exit(-1);
    }

    /* specifz signal and handler */
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = handler;

    /* Initialize signal */
    sigemptyset(&sa.sa_mask);

    printf("Establishing handler for signal %d\n", SIGRTMIN);

    /* Register signal handler */
    if (sigaction(SIGRTMIN, &sa, NULL) == -1){
        fprintf(stderr, "Error sigaction: %s\n", strerror(errno));
        exit(-1);
    }

    /* start timer */
    res = timer_settime(timerId, 0, &its, NULL);

    if ( res != 0){
        fprintf(stderr, "Error timer_settime: %s\n", strerror(errno));
        exit(-1);
    }

    printf("Press ENTER to Exit\n");
    while(getchar()!='\n'){}
    return 0;
}



static void
handler(int sig, siginfo_t *si, void *uc)
{
    UNUSED(sig);
    UNUSED(uc);
    struct t_eventData *data = (struct t_eventData *) si->_sifields._rt.si_sigval.sival_ptr;
    printf("Timer fired %d - thread-id: %d\n", ++data->myData, gettid());
}

Contrairement au minuteur de thread, nous devons initialiser le signal et enregistrer un gestionnaire de signal. Cette approche est plus performante car elle n'entraînera pas la création de threads supplémentaires. Pour cette raison, l’exécution du gestionnaire de signaux est également plus déterministe. L'inconvénient est clairement l'effort de configuration supplémentaire pour déboguer correctement.

Résumé

Les deux méthodes décrites dans cet article sont des implémentations de minuteries proches du noyau. Même si la fonction timer_create(...) fait partie de la spécification POSIX, il n'est pas possible de compiler l'exemple de code sur un système FreeBSD en raison de petites différences dans les structures de données. Outre cet inconvénient, une telle implémentation vous offre un contrôle précis pour les applications de synchronisation à usage général.

Articles connexes: