Forum Programmation.c probleme pour faire un timer précis de l'ordre de la milliseconde

Posté par  . Licence CC By‑SA.
Étiquettes : aucune
2
26
jan.
2022

bonjour à tous,
je souhaite faire un timer via timerfd_create qui reveille mon programme toutes les 1 milliseconde (ms). j'aimerais une erreur que de 10% soit pas plus de 1.1ms. Mon programme à un timer moyen de 1ms (super :) ) mais a parfois des piques à 1,2 ms voir 1,4ms :(

Je l'ai passé en fifo priorité 99, j'ai viré toutes les interruptions venant de mon cpu8 afin d'empecher que mon processus soit interrompu (via /proc/irq/smp_affinity), j'ai bien entendu forcé mon programme via la commande taskset a etre que sur le cpu8, j'ai mis tous mes cpus en mode performance, j'ai mis la variable /proc/sys/kernel/sched_rt_runtime_us = -1
et pourtant rien à faire j'ai toujours des piques avec une erreur pouvant aller jusqu'a plus de 20%.

Je demande des idées, merci d'avance :)

PS : je précise avoir un bon PC donc il devrait tenir la charge

Je vous montre mon code ca pourra peut etre aidé:

    #include <stdio.h>
    #include <sys/timerfd.h>
    #include <errno.h>
    #include <string.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/mman.h>

    #define COUNT 10001
    #define PERIODE 1000000 //1ms

    int main(int argc, char const *argv[])
    {
        int timerfd;
        if( (timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC )) < 0)
        {
            printf("error timerfd : %s\n", strerror(errno));
            return -1;
        }

        struct timespec initTimer;
        if( clock_gettime(CLOCK_MONOTONIC, &initTimer ) < 0 )
        {
            printf("error clock_gettime : %s\n", strerror(errno));
            return -1;
        }

        struct itimerspec setTimer ;
        setTimer.it_interval.tv_sec = 0;
        setTimer.it_interval.tv_nsec = PERIODE;
        setTimer.it_value.tv_sec = initTimer.tv_sec ;
        setTimer.it_value.tv_nsec = 0;

        if( timerfd_settime(timerfd, TFD_TIMER_ABSTIME,  &setTimer, NULL ) < 0)
        {
            printf("error timerfd_settime : %s\n", strerror(errno));
            return -1;
        }


        struct timespec now, previous;
        long somme = 0;
        long peak = PERIODE, base = PERIODE;
        long tabTimer[COUNT];
        __uint64_t ret;

        if( mlockall( MCL_CURRENT | MCL_FUTURE ) < 0)
        {
            printf("error mlockall : %s\n", strerror(errno));
            return -1;
        }

        if (clock_gettime(CLOCK_MONOTONIC, &previous ) < 0 )
        {
            printf("error clock_gettime : %s\n", strerror(errno));
            return -1;
        }

        for (size_t i = 0; i < COUNT; i++)
        {        

            read( timerfd, &ret, sizeof(__uint64_t));

            clock_gettime(CLOCK_MONOTONIC, &now );
            tabTimer[i] = (now.tv_sec - previous.tv_sec) * 1000000000 + now.tv_nsec - previous.tv_nsec;
            previous = now;

        }

        //we write results
        for (size_t i = 1; i < COUNT; i++)
        {        
            somme += tabTimer[i];
            if( tabTimer[i] > peak)
            {
                peak = tabTimer[i];
            }
            if( tabTimer[i] < base)
            {
                base = tabTimer[i];
            }
        }

        printf("average en ns = %ld, peak = %ld, base = %ld\n", somme/(COUNT - 1), peak, base);

        return 0;
    }
  • # mauvaise hardware, mauvais os ?

    Posté par  . Évalué à 5. Dernière modification le 26 janvier 2022 à 22:21.

    Pour obtenir ce que tu veux, il te faudrait peut etre te tourner vers une extensio real time pour linux (rtlinux, xenomai, … ), avec peut etre un hardware mieux adapté ?

    Ou alors partir vers un is dédié au temps réel.

    C'est pour quoi faire ?

  • # Vérifie que tu es vraiment en priorité haute

    Posté par  . Évalué à 4. Dernière modification le 26 janvier 2022 à 23:13.

    Sans jouer avec les affinités ni rien :

    ~$ ./a.out
    average en ns = 999989, peak = 1236804, base = 815934
    ~$ chrt 99 ./a.out
    average en ns = 999920, peak = 1061961, base = 143937
    

    J'avais conclu que c'est parce que je suis dans le groupe audio qui a le droit de faire du RT (cf /etc/security/limits.conf), mais ça fonctionne avec un autre user qui n'y est pas :-/.

    Tu peux aussi vérifier la prio avec /proc/PID/limits :

    ~$ cat /proc/`pidof a.out`/limits
    Limit                     Soft Limit           Hard Limit           Units
    Max cpu time              unlimited            unlimited            seconds
    Max file size             unlimited            unlimited            bytes
    Max data size             unlimited            unlimited            bytes
    Max stack size            8388608              unlimited            bytes
    Max core file size        unlimited            unlimited            bytes
    Max resident set          unlimited            unlimited            bytes
    Max processes             31337                31337                processes
    Max open files            1024                 524288               files
    Max locked memory         8388608              8388608              bytes
    Max address space         unlimited            unlimited            bytes
    Max file locks            unlimited            unlimited            locks
    Max pending signals       31337                31337                signals
    Max msgqueue size         819200               819200               bytes
    Max nice priority         0                    0
    Max realtime priority     99                   99
    Max realtime timeout      unlimited            unlimited            us
    

    Je suis d'accord avec totof que si tu veux aller plus loin que jouer un peu avec les timers, il faudra sans doute un kernel spécifique, voire du matériel. Bon, la ms c'est pas trop demander non plus ;).

    (moi là c'est sur un vieux laptop)

    • [^] # Re: Vérifie que tu es vraiment en priorité haute

      Posté par  . Évalué à 3.

      Bon, la ms c'est pas trop demander non plus ;).

      Le problème c'est pas la ms, mais la marge d'erreur qui peut être assez fluctuante sur un noyau Linux, et sur un CPU de type x86.

      Il y a probablement des choses à tuner côté configuration noyau avant de passer par des modules spécifiques temps réel, mais tout ça dépend du besoin, d'ou ma question.

  • # J'ai été confronté au même problème.

    Posté par  . Évalué à 2.

    La solution que j'avais trouvé c'était le le sleep pendant le temps qu'il faut - une marge de sécurité + l'attente active pour les derniers instant.
    Il me semble que j'avais mis en place un truc pour ajuster le temps d'attente active.

    Sinon, comme alternative entre sleep et l'attente active il y a yield, je ne sais pas ce que ça donne.

  • # Set affinity ?

    Posté par  . Évalué à 3.

    Hello,
    Tu n’indiques pas comment tu lances ton programme.
    En mode temps réel sur un noyau standard, la seule chose plus prioritaire, c’est le noyau.

    Une solution pourrait-être de forcer ton processus sur un cœur, et d’interdire ce cœur au noyau.

    Sinon, le réveil un peu plus tôt puis boucle active peut-être viable, et vraiment précis.

  • # Paramètre de timerfd_create

    Posté par  . Évalué à 4.

    Re,
    Dans le man timerfd_create,
    il y a ce passage :

           CLOCK_REALTIME
                  A settable system-wide real-time clock.
    
           CLOCK_MONOTONIC
                  A nonsettable monotonically increasing clock that measures time from some unspecified point in the past that does not change after system startup.
    
    

    As-tu essayé avec REALTIME ?

    • [^] # Re: Paramètre de timerfd_create

      Posté par  . Évalué à 2.

      Le get_clocktime aussi a le paramètre CLOCK_REALTIME.

      Sinon, tu peux utiliser la fonction setitimer. Par contre, c'est un peu plus complexe, il faut intercepter le signal SIGALARM, dans la fonction callback, tu libères un sémaphore (mutex). Et dans ton code, tu bloques le sémaphores.

      Autre chose, le temps réel, ça veut dire arrivé à temps… donc si malgré les erreurs tu arrives à finir ton algo avant la deadline, pourquoi chercher à réagir plus vite ? Néanmoins j’avoue que l’écart type d’erreur laisse songeur. ;-)

  • # driver

    Posté par  (site web personnel) . Évalué à 3.

    Tu peux avoir des drivers mauvais pour le rt (proprio) etc…

    Tu peux tenter de les désactiver.

    "La première sécurité est la liberté"

  • # setitimer

    Posté par  . Évalué à 6.

    Salut,
    Je me suis tenté une implémentation avec setitimer…

    #define _POSIX_C_SOURCE 199309L
    
    #include <stdio.h>
    //    #include <sys/timerfd.h>
        #include <errno.h>
        #include <string.h>
        #include <stdlib.h>
    //  #include <unistd.h>
        #include <sys/mman.h>
    
    //#include <time.h>
    #include <unistd.h>
    #include <threads.h>
    #include <signal.h>
    #include <sys/time.h>
    
    
        #define COUNT 10001
        #define PERIODE 1000000 //1ms (in ns)
    
    mtx_t myLock;
    void (*old_handler)(int); // sighandler_t don't work !!! :-(
    
    void alarm_callback(int sig)
    {
      // Unlock du mutex pour continuer la boucle.
      (void) mtx_unlock(&myLock);
         if ((old_handler = signal(SIGALRM, alarm_callback)) == SIG_ERR)
        {
          fprintf(stderr,"Error setting SIGALARM in alarm_callback\n");
          return ;
        }
    
    
    }
    
        int main(int argc, char const *argv[])
        {
          if (mtx_init(&myLock, mtx_plain ) ==  thrd_error)
        {
          fprintf(stderr,"Error creating mutex\n");
          return -1;
        }
    
          // Lock one time to init locked. First loop must unlock
          if (mtx_lock(&myLock) == thrd_error)
        {
          fprintf(stderr,"Error locking mutex first time\n");
          return -1;
        }
    
          if ((old_handler = signal(SIGALRM, alarm_callback)) == SIG_ERR)
        {
          fprintf(stderr,"Error setting SIGALARM\n");
          return -1;
        }
    
    
    
           struct itimerval setTimer, oldTimer;
    
           setTimer.it_interval.tv_sec = 0;
           // Warning, c’est des us.
           setTimer.it_interval.tv_usec = PERIODE / 1000;
           // First period start in 1s
           setTimer.it_value.tv_sec = 1;
           setTimer.it_value.tv_usec = 0;
           //       Timer values are defined by the following structures:
           //
           //           struct itimerval {
           //               struct timeval it_interval; /* Interval for periodic timer */
           //               struct timeval it_value;    /* Time until next expiration */
           //           };
           //
           //           struct timeval {
           //               time_t      tv_sec;         /* seconds */
           //               suseconds_t tv_usec;        /* microseconds */
           //           };
    
    
           if (setitimer(ITIMER_REAL,&setTimer, &oldTimer) == -1)
         {
           fprintf(stderr,"Setitimer initialization failed\n");
           return -1;
         }
            struct timespec now, previous;
            long somme = 0;
            long peak = PERIODE, base = PERIODE;
            long tabTimer[COUNT];
            __uint64_t ret;
    
    
            if (clock_gettime(CLOCK_MONOTONIC,&previous ) < 0 )
            {
                printf("error clock_gettime : %s\n", strerror(errno));
                return -1;
            }
    
            for (size_t i = 0; i < COUNT; i++)
          {
          // Wait for sync
            if (mtx_lock(&myLock) == thrd_error)
              {
            fprintf(stderr,"Error lock in loop number %d\n",i);
            return -1;
              }
          clock_gettime(CLOCK_MONOTONIC,&now );
                tabTimer[i] = (now.tv_sec - previous.tv_sec) * 1000000000 + now.tv_nsec - previous.tv_nsec;
                previous = now;
    
            }
    
        const long high_10 = PERIODE + PERIODE / 10;
        const long low_10 = PERIODE - PERIODE / 10;
        int higher = 0;
        int lower = 0;
        int normal = 0;
            //we write results
            for (size_t i = 1; i < COUNT; i++)
            {
          //      printf("[%2d] = %ld\n",i,tabTimer[i]);
                somme += tabTimer[i];
                if( tabTimer[i] > peak)
                {
                    peak = tabTimer[i];
                }
                if( tabTimer[i] < base)
                {
                    base = tabTimer[i];
                }
            if (tabTimer[i] > high_10)
              {
            higher++;
              }
            else if (tabTimer[i] < low_10)
              {
            lower++;
              }
            else
              {
            normal++;
              }
            }
    
        int total = lower + higher + normal;
            printf("average en ns = %ld, peak = %ld(+%2.2f%%), base = %ld(-%2.2f%%)\n", somme/(COUNT - 1), peak, (((float)peak/PERIODE) - 1) * 100, base, (1. - ((float)base)/PERIODE)*100);
        printf(" Total : %d,\n"
               "\t- lower : %6d (%2.2f%%)\n"
               "\t- normal: %6d (%2.2f%%)\n"
               "\t- higher: %6d (%2.2f%%)\n",
               total,
               lower, 100. * ((float) lower)/total,
               normal, 100.* ((float)normal)/total,
               higher, 100.* ((float)higher)/total);
    
            return 0;
        }

    Pour le sigalarm, il faut le recabler à chaque fois.
    Voici quelques stats, j’y ai ajouté le nombre de fois ou on a un écart de plus de 10%. De plus, si on déborde sur un cycle, la différence entre les deux sera forcément très inférieur sur le cycle suivant. D’où la relative symétrie (+ou- 1) entre les higher et les lower.

    $ ./a.out 
    average en ns = 999998, peak = 1057467(+5.75%), base = 944193(-5.58%)
     Total : 10000,
            - lower :      0 (0.00%)
            - normal:  10000 (100.00%)
            - higher:      0 (0.00%)
    $ ./a.out 
    average en ns = 1000098, peak = 2083044(+108.30%), base = 603560(-39.64%)
     Total : 10000,
            - lower :      1 (0.01%)
            - normal:   9997 (99.97%)
            - higher:      2 (0.02%)
    $ ./a.out 
    average en ns = 999998, peak = 1206658(+20.67%), base = 796835(-20.32%)
     Total : 10000,
            - lower :      2 (0.02%)
            - normal:   9997 (99.97%)
            - higher:      1 (0.01%)
    $ ./a.out 
    average en ns = 999997, peak = 1107952(+10.80%), base = 894580(-10.54%)
     Total : 10000,
            - lower :      1 (0.01%)
            - normal:   9998 (99.98%)
            - higher:      1 (0.01%)
    $ su 
    Mot de passe : 
    ####################
    # En Temps Réel
    ####################
    .../Linuxfr/TR# chrt 50 ./a.out 
    average en ns = 999997, peak = 1009857(+0.99%), base = 978338(-2.17%)
     Total : 10000,
            - lower :      0 (0.00%)
            - normal:  10000 (100.00%)
            - higher:      0 (0.00%)
    .../Linuxfr/TR# chrt 50 ./a.out 
    average en ns = 999997, peak = 1011019(+1.10%), base = 979650(-2.03%)
     Total : 10000,
            - lower :      0 (0.00%)
            - normal:  10000 (100.00%)
            - higher:      0 (0.00%)
    .../Linuxfr/TR# chrt 50 ./a.out 
    average en ns = 999997, peak = 1009547(+0.95%), base = 976955(-2.30%)
     Total : 10000,
            - lower :      0 (0.00%)
            - normal:  10000 (100.00%)
            - higher:      0 (0.00%)
    .../Linuxfr/TR# chrt 50 ./a.out 
    average en ns = 999997, peak = 1015868(+1.59%), base = 979951(-2.00%)
     Total : 10000,
            - lower :      0 (0.00%)
            - normal:  10000 (100.00%)
            - higher:      0 (0.00%)
    .../SSD/C++/Linuxfr/TR#

    Un essai supplémentaire sur 2 minutes (2 × 60 × 1000 = 120 000ms)

    $ ./a.out 
    average en ns = 999999, peak = 1425844(+42.58%), base = 585376(-41.46%)
     Total : 120000,
            - lower :     22 (0.02%)
            - normal: 119955 (99.96%)
            - higher:     23 (0.02%)
    $ su 
    Mot de passe : 
    .../Linuxfr/TR# chrt 50 ./a.out 
    average en ns = 999999, peak = 1030337(+3.03%), base = 968720(-3.13%)
     Total : 120000,
            - lower :      0 (0.00%)
            - normal: 120000 (100.00%)
            - higher:      0 (0.00%)
    # Ajout d'une affinité CPU
    .../Linuxfr/TR# taskset 1 chrt 50 ./a.out 
    average en ns = 999999, peak = 1272574(+27.26%), base = 727705(-27.23%)
     Total : 120000,
            - lower :     10 (0.01%)
            - normal: 119981 (99.98%)
            - higher:      9 (0.01%)
    .../C++/Linuxfr/TR#

    En forçant l’affinité sur un CPU, on augmente l’erreur… ça ma surpris. Il faudrait voir si on peut interdire un CPU à tout le monde sauf un processus ;-)
    Après, j'avais choisi le CPU 0, peut-être que en prenant le 3…

    Linuxfr/TR# taskset 4 chrt 50 ./a.out 
    average en ns = 999999, peak = 1018414(+1.84%), base = 971595(-2.84%)
     Total : 120000,
            - lower :      0 (0.00%)
            - normal: 120000 (100.00%)
            - higher:      0 (0.00%)
    .../Linuxfr/TR#

    C’est mieux… est-ce le hazard, ou les IT sont cablé sur le premier CPU sous Linux ?

    Voilà pour mes tests du soir.

    Bonne nuit

  • # désactiver l'hyperthreading ?

    Posté par  (site web personnel) . Évalué à 3.

    Je ne suis pas certain que ce soit pertinent dans ce cas, mais une des recommandations que j'avais lue pour faire du temps réel sous Linux était de désactiver l'hyperthreading. Si jamais, voilà un script que j'avais trouvé pour le faire depuis Linux.

  • # le probleme vient peut etre que mes cpu entre dans un etat de sommeil

    Posté par  . Évalué à 1. Dernière modification le 29 janvier 2022 à 13:41.

    merci pour vos messages,

    -> je lance mon programme avec sudo chrt -f 99 ./a.out
    -> je ne pense pas qu'il soit necessaire de passer sur du PREMPT_RT ou xenomai pour avoir un timer de 1ms avec une marge d'erreur seulement de 10%
    -> pour moi le fait d'utiliser timerfd_create ou timer_create ou setitimer revient au meme.

    j'ai remarqué que j'avais une erreur seulement de 2 à 3% (au lieu des 10 à 40%) quand mon systeme était stressé (avec le programme stress). On me renseignant un peu j'ai appris que le systeme pouvait entrer dans des niveaux de sommeil (suspend, hibernation), et lorsque l'interruption de mon timer se déclenche ca met un certain temps avant de pouvoir déclencher le handler associé. J'ai l'impression que le probleme vient de là.

    Je vais donc essayer de changer certain parametre noyau. Peut etre retiré le mode tickless, mettre une frequence de timer à 1000Hz (au lieu des 250Hz par défaut) et essayer de retirer les états de sommeil de mes cpu

    • [^] # Re: le probleme vient peut etre que mes cpu entre dans un etat de sommeil

      Posté par  . Évalué à 3.

      On me renseignant un peu j'ai appris que le systeme pouvait entrer dans des niveaux de sommeil (suspend, hibernation), et lorsque l'interruption de mon timer se déclenche ca met un certain temps avant de pouvoir déclencher le handler associé. J'ai l'impression que le probleme vient de là.

      Je vais donc essayer de changer certain parametre noyau. Peut etre retiré le mode tickless, mettre une frequence de timer à 1000Hz (au lieu des 250Hz par défaut) et essayer de retirer les états de sommeil de mes cpu

      commence peut-etre par désactiver les états de sommeil des CPUs avant de vouloir changer des paramètres de noyau, de frequence de timer…

  • # Revenir à pourquoi une telle contrainte.

    Posté par  . Évalué à 4.

    Bonjour,

    Il faudrait peut-être revenir à pourquoi tu as ce besoin de timing ultra précis.
    Si juste avant l'échéance une interruption réseau ou disque déclenche c'est une centaine de µs de perdues. Si on a les 2 et qu'en plus le processeur doit sortir de veille c'est 300µs d'écart. C'est inévitable quelle que soit la priorité de ton application. Je travaille sur des noyaux temps réel et je descend au mieux à la milliseconde d'incertitude et toi tu souhaites quelques dizaines de µs de fluctuation et en plus sous Linux.

    Sur une carte dédiée, on peut avoir une contrainte beaucoup plus forte. Pour reprendre mon cas, je dois récupérer des événements toutes les secondes et utiliser leur durée. J'ai une contrainte de 0.0000015ms sur la mesure de cette durée. Aucun problème la durée est garantie et est lue toutes les secondes environ.

    Donc si tu as besoin d'un contrainte aussi forte, il faut peut-être revenir au pourquoi et trouver un moyen de la garantir. Et il faut toujours se poser la question quelles sont les conséquences si la contrainte n'est pas atteinte en séparant les cas : écart maximum fréquent et écart maximum exceptionnel.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.