Existe-t-il des temporisateurs d’intervalle POSIX bien conçus?

Inspiré par la dernière seconde écasting, j’ai exploré la synchronisation (en particulier les timers d’intervalle) à l’aide d’appels POSIX.

POSIX propose plusieurs façons de configurer des timers, mais elles sont toutes problématiques:

  • sleep et nanosleep ils sont agaçants à redémarrer après avoir été interrompus par un signal et introduisent un décalage d’horloge. Vous pouvez éviter certains biais, mais pas tous, avec un peu de travail supplémentaire, mais ces fonctions utilisent l’horloge temps réel, ce qui ne va pas sans écueil.
  • setitimer ou setitimer plus moderne, sont conçus pour être des timers à intervalles, mais ils sont calculés par processus, ce qui pose problème si vous avez besoin de plusieurs timers actives. Ils ne peuvent pas non plus être utilisés de manière synchrone, mais c’est moins grave.
  • clock_gettime et clock_nanosleep semblent être la bonne réponse lorsqu’ils sont utilisés avec CLOCK_MONOTONIC . clock_nanosleep supporte les délais absolus, vous pouvez donc simplement dormir, incrémenter le délai et répéter. Il est également facile de redémarrer après une interruption. Malheureusement, ces fonctions pourraient également être spécifiques à Linux: elles ne sont pas sockets en charge sous Mac OS X ou FreeBSD.
  • pthread_cond_timedwait est disponible sur le Mac et peut fonctionner avec gettimeofday comme solution de contournement, mais sur le Mac, il ne peut fonctionner qu’avec une horloge en temps réel. Il est donc sujet à une erreur de comportement lorsque l’horloge système est réglée ou qu’une seconde est écasting.

Y a-t-il une API qui me manque? Existe-t-il un moyen raisonnablement portable de créer des timers d’intervalle sage sur des systèmes de type UNIX, ou cela résume-t-il l’état actuel des choses?

Par sage et raisonnablement portable, je veux dire:

  • Pas enclin au décalage d’horloge (moins bien sûr, le décalage de l’horloge du système)
  • Résilient à l’horloge système en cours d’établissement ou à une seconde intercalaire
  • Capable de supporter plusieurs timers dans le même processus
  • Disponible au moins sur Linux, Mac OS X et FreeBSD

Une note sur les secondes intercalaires (en réponse à la réponse de R .. ):

Les jours POSIX durent exactement 86 400 secondes, mais les jours réels peuvent rarement être plus longs ou plus courts. La manière dont le système résout cet écart est définie par l’implémentation, mais il est courant que la seconde intercalaire partage le même horodatage UNIX que la seconde précédente. Voir aussi: secondes bissextiles et quoi faire avec .

Le bogue bêta seconde du kernel Linux était dû à un échec de la maintenance après avoir réglé l’horloge d’une seconde: https://lkml.org/lkml/2012/7/1/203 . Même sans ce bug, l’horloge aurait reculé d’une seconde.

Les temporisateurs POSIX ( timer_create ) ne nécessitent pas de signaux; vous pouvez également prendre des dispositions pour que l’expiration du temporisateur soit livrée dans un thread via le type de notification SIGEV_THREAD . Malheureusement, la mise en œuvre de glibc crée en réalité un nouveau thread pour chaque expiration (ce qui entraîne beaucoup de surcharge et détruit tout espoir de robustesse en temps réel), même si la norme permet de réutiliser le même thread à chaque expiration.

Bref, je recommanderais simplement de créer votre propre thread qui utilise clock_nanosleep avec TIMER_ABSTIME et CLOCK_MONOTONIC pour un minuteur d’intervalle. Puisque vous avez mentionné que certains systèmes défectueux pourraient ne pas avoir ces interfaces, vous pourriez simplement avoir une implémentation pthread_cond_timedwait (basée sur pthread_cond_timedwait ) sur de tels systèmes, et imaginer que sa qualité pourrait être moindre en raison du manque d’horloge monotone, mais une limitation fondamentale de l’utilisation d’une implémentation de basse qualité telle que MacOSX.

En ce qui concerne votre préoccupation concernant les secondes intercalaires, si ntpd ou similaire fait en sorte que votre horloge en temps réel saute en arrière lorsqu’une seconde intercalaire se produit, c’est un sérieux problème dans ntpd. Les temps POSIX (en secondes depuis l’époque) sont exprimés en unités de secondes calendaires (exactement 1/86400 de jour) par seconde standard, et non pas en secondes SI. La seule place où la logique de seconde intercalaire appartient à un système POSIX (si gmtime / gmtime / localtime lorsqu’ils convertissent gmtime / heure. Je n’ai pas suivi les bugs qui ont frappé cette fois-ci, mais ils semblent résulter d’un logiciel système qui fait beaucoup de choses stupides et fausses, et non d’un problème fondamental.

kqueue et kevent peuvent être utilisés à cette fin. OSX 10.6 et FreeBSD 8.1 ajoutent un support pour EVFILT_USER , que nous pouvons utiliser pour EVFILT_USER la boucle d’événements à partir d’un autre thread.

Notez que si vous utilisez ceci pour implémenter vos propres conditions et timedwait, vous n’avez pas besoin de verrous pour éviter les conditions de concurrence, contrairement à cette excellente réponse , car vous ne pouvez pas “rater” un événement dans la queue.

Sources:

  • Page de manuel de FreeBSD
  • Page de manuel OS X
  • tutoriel kqueue
  • code source de libevent

Exemple de code

Comstackr avec clang -o test -std=c99 test.c

 #include  #include  #include  #include  #include  #include  #include  #include  // arbitrary number used for the identifier property const int NOTIFY_IDENT = 1337; static int kq; static void diep(const char *s) { perror(s); exit(EXIT_FAILURE); } static void *run_thread(void *arg) { struct kevent kev; struct kevent out_kev; memset(&kev, 0, sizeof(kev)); kev.ident = NOTIFY_IDENT; kev.filter = EVFILT_USER; kev.flags = EV_ADD | EV_CLEAR; struct timespec timeout; timeout.tv_sec = 3; timeout.tv_nsec = 0; fprintf(stderr, "thread sleep\n"); if (kevent(kq, &kev, 1, &out_kev, 1, &timeout) == -1) diep("kevent: waiting"); fprintf(stderr, "thread wakeup\n"); return NULL; } int main(int argc, char **argv) { // create a new kernel event queue kq = kqueue(); if (kq == -1) diep("kqueue()"); fprintf(stderr, "spawn thread\n"); pthread_t thread; if (pthread_create(&thread, NULL, run_thread, NULL)) diep("pthread_create"); if (argc > 1) { fprintf(stderr, "sleep for 1 second\n"); sleep(1); fprintf(stderr, "wake up thread\n"); struct kevent kev; struct timespec timeout = { 0, 0 }; memset(&kev, 0, sizeof(kev)); kev.ident = NOTIFY_IDENT; kev.filter = EVFILT_USER; kev.fflags = NOTE_TRIGGER; if (kevent(kq, &kev, 1, NULL, 0, &timeout) == -1) diep("kevent: sortingggering"); } else { fprintf(stderr, "not waking up thread, pass --wakeup to wake up thread\n"); } pthread_join(thread, NULL); close(kq); return EXIT_SUCCESS; } 

Sortie

 $ time ./test spawn thread not waking up thread, pass --wakeup to wake up thread thread sleep thread wakeup real 0m3.010s user 0m0.001s sys 0m0.002s $ time ./test --wakeup spawn thread sleep for 1 second thread sleep wake up thread thread wakeup real 0m1.010s user 0m0.002s sys 0m0.002s 

Vous pouvez consulter la question ici pour l’émulation clock_gettime , à laquelle j’ai également fourni une réponse, mais qui m’a aussi aidé. J’ai récemment ajouté une timer simple à un petit référentiel que je garde pour Mac OS X et qui émule partiellement les appels POSIX. Un simple test lance le minuteur à 2000Hz. Le référentiel s’appelle PosixMachTiming . Essaye le.

PosixMachTiming est basé sur Mach . Il semble qu’une partie de l’API de Mach liée au minutage a disparu des pages d’Apple et est devenue obsolète, mais des fragments de code source flottent toujours. Il semble que les unités AbsoluteTime et les abstractions de kernel trouvées ici constituent la nouvelle façon de faire les choses. Quoi qu’il en soit, le repository PosixMachTiming fonctionne toujours pour moi.

Vue d’ensemble de PosixMachTiming

clock_gettime est émulé pour CLOCK_REALTIME par des appels de fonction mach qui exploitent l’horloge temps réel du système, appelée CALENDAR_CLOCK .

clock_gettime est émulé pour CLOCK_MONOTONIC en utilisant une variable globale ( extern mach_port_t clock_port ). Cette horloge est initialisée lorsque l’ordinateur s’allume ou peut-être se réveille. Je ne suis pas sûr. Dans tous les cas, c’est la variable globale que la fonction mach_absolute_time() appelle.

clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, ...) est émulé en utilisant nanosleep sur la différence entre l’heure actuelle et l’heure monotone absolue.

itimer_start() et itimer_step() sont basés sur l’appel de clock_nanosleep pour un temps cible absolu monotone. Il incrémente l’heure cible du pas de temps à chaque itération (et non l’heure actuelle), de sorte que le décalage d’horloge ne pose pas de problème.

Notez que cela ne répond pas à votre exigence de pouvoir prendre en charge plusieurs minuteurs dans le même processus.