Exécuter la commande unix avec précision, à de très courts intervalles SANS accumuler de décalage dans le temps


38

Question

J'aimerais pouvoir exécuter une commande UNIX précisément chaque seconde sur une longue période .

J'ai besoin d'une solution qui ne tarde pas après un certain temps, à cause du temps nécessaire à l'exécution de la commande elle-même. dormir , regarder , et un certain script python m'a tous échoué à cet égard.

Sur le microcontrôleur tel que le http://Arduino.cc, je le ferais par le biais d'interruptions d'horloge matérielle. Je voudrais savoir s'il existe une solution similaire de script de shell avec précision dans le temps. Toutes les solutions que j'ai trouvées dans StackExchange.com ont entraîné un décalage perceptible, si elles duraient plusieurs heures. Voir les détails ci-dessous.

But pratique / application

Je veux tester si ma connexion réseau est en permanence en hausse en envoyant des horodatages via nc(netcat) toutes les secondes.

Expéditeur:

precise-timestamp-generator | tee netcat-sender.txt | nc $receiver $port

Receveur:

nc -l -p $port > netcat-receiver.txt

Une fois l’opération terminée, comparez les deux journaux:

diff netcat-sender.txt netcat-receiver.txt

Les diffs seraient les horodatages non transmis. De cela, je saurais à quelle heure mon LAN / WAN / ISP crée des problèmes.


Solution SLEEP

while [ true ]; do date "+%Y-%m-%d %H:%M:%S" ; sleep 1; done | tee timelog-sleep.txt

Obtient un certain décalage dans le temps, car la commande dans la boucle prend également un peu de temps.

Précision

cat timelog-sleep.txt

2012-07-16 00:45:16
[...]
2012-07-16 10:20:36

Secondes écoulées: 34520

wc -l timelog-sleep.txt

Lignes dans le fichier: 34243

Précision résumée:

  • 34520-34243 = 277 problèmes de synchronisation
  • 34520/34243 = 1,008 = 0,8% de réduction

Solution RÉPÉTER PYTHON

Trouvé à: Répéter une commande Unix toutes les x secondes pour toujours

repeat.py 1 "date '+%Y-%m-%d %H:%M:%S'" >> timelog-repeat-py.txt

Censé éviter le décalage horaire, mais ne parvient pas à le faire.

Précision

wc -l timelog-repeat-py.txt

2012-07-16 13:42:44
[...]
2012-07-16 16:45:24

Secondes écoulées: 10960

wc -l timelog-repeat-py.txt

Lignes dans le fichier: 10859

Précision résumée:

  • 10960-10859 = 101 problèmes de chronométrage
  • 10960/10859 = 1,009 = 0,9% de réduction

Solution MONTRE

watch -n 1 "date '+%Y-%m-%d %H:%M:%S' >> ~/Desktop/timelog-watch.txt"

Précision

wc -l timelog-watch.txt
2012-07-16 11:04:08
[...]
2012-07-16 13:25:47

Secondes écoulées: 8499

wc -l timelog-watch.txt

Lignes dans le fichier: 8366

Précision résumée:

  • 8499-8366 = 133 problèmes de synchronisation.
  • 8499/8366 = 1.016 = 1.6% de réduction.

5
Quelle est la résolution requise, la précision et pourquoi en avez-vous besoin / pourquoi l'utilisez-vous?
Jippie

Que se passe-t-il si vous êtes nicele processus qui dort?
Tilo Wiklund

1
Avez-vous pensé utiliser la planification en temps réel pour minimiser, espérons-le, le délai d'un appel sleep ().
Mdpc

Il me semble que votre timing sera toujours affecté par l'état de fonctionnement actuel de la boîte. Si quelque chose provoque le déchargement du programme qui vous tient à coeur du cache, vous obtiendrez une augmentation du temps, à moins que vous ne puissiez garantir que le temps d'exécution typique du programme est nettement inférieur à l'intervalle souhaité. Je voudrais être sur un système en temps réel, dans une configuration considérablement réduite sans personne d'autre, ou en mode mono-utilisateur. Une meilleure solution consiste probablement à modifier le programme en question pour qu'il effectue la boucle elle-même, au lieu de l'invoquer à partir d'un autre programme.
Hack Saw

2
Lancez chaque commande dans son propre thread, ainsi aucun blocage lié aux E / S ne perdra votre temps.
Joel Cornett

Réponses:


12

Comment fonctionne ce script Perl que je viens de fouetter?

#!/usr/bin/perl

use strict;
use warnings;
use Time::HiRes qw/time sleep/;

sub launch {
    return if fork;
    exec @_;
    die "Couldn't exec";
}

$SIG{CHLD} = 'IGNORE';

my $interval = shift;
my $start = time();
while (1) {
    launch(@ARGV);
    $start += $interval;
    sleep $start - time();
}

Utilisation: perl timer.pl 1 date '+%Y-%m-%d %H:%M:%S'

Il a fonctionné pendant 45 minutes sans un seul saut, et je pense qu'il continuera de le faire sauf si a) la charge du système devient tellement élevée que fork () prend plus d'une seconde ou b) une seconde intercalaire est insérée.

Cependant, il ne peut pas garantir que la commande s'exécute à la seconde exacte, car il y a une surcharge, mais je doute que ce soit bien pire qu'une solution basée sur des interruptions.

Je l'ai couru pendant environ une heure avec date +%N(nanosecondes, extension GNU) et quelques statistiques à ce sujet. Le délai le plus long était de 1 155 microsecondes. Moyenne (moyenne arithmétique) 216 µs, médiane 219 µs, écart-type 42 µs. Il a fonctionné plus rapidement que 270 µs 95% du temps. Je ne pense pas que vous puissiez le battre, sauf avec un programme en C.


1
Je l'ai couru pendant la nuit sans aucune autre application utilisateur active avec un intervalle d'une seconde, et ça a duré 29241 secondes, sans une seule seconde ignorée! Cela conviendra à mon but. Puis je l' ai couru à nouveau ce matin avec un intervalle de 0,1 sec, GNU dateavec +%Net après seulement 3 minutes , il a lancé cette erreur: la Time::HiRes::sleep(-0.00615549): negative time not invented yet at ~/bin/repeat.pl line 23.ligne 23 dans mon script enregistré:sleep $start - time();
PORG

Si vous l'exécutez avec des intervalles de 0,01 s ou 0,001 s, il suffit de quelques secondes ou moins jusqu'à ce que le programme s'annule avec l'erreur "heure négative". Mais pour mon but, ça va!
porg

28

La ualarm()fonction POSIX vous permet de programmer le noyau pour qu'il signale périodiquement votre processus, avec une précision de l'ordre de la microseconde.

Fabriquez un programme simple:

 #include<unistd.h>
 #include<signal.h>
 void tick(int sig){
     write(1, "\n", 1);
 }
 int main(){
     signal(SIGALRM, tick);
     ualarm(1000000, 1000000); //alarm in a second, and every second after that.
     for(;;)
         pause();
 }

Compiler

 gcc -O2 tick.c -o tick

Ensuite, attachez-le à ce que vous devez faire régulièrement, comme ceci:

./tick | while read x; do
    date "+%Y-%m-%d %H:%M:%S"
done | tee timelog-sleep.txt

Ai-je besoin d'un shell spécial ou de C-std pour cela? Je l'ai compilé (ce qui a donné un petit avertissement sur le retour manquant) mais aucune sortie n'a été produite.
math

@math Avec -std=c99, vous ne recevrez pas d'avertissement concernant le retour manquant. Sinon, vous ne devriez pas avoir besoin de quelque chose de spécial. Avez-vous mal saisi un zéro supplémentaire? strace ./tickva vous montrer ce qu'il fait d'un point de vue systémique
Dave

Je reçois: gcc -O2 -std = c99 -o tick tick.c tick.c: dans la fonction 'main': tick.c: 10: 5: avertissement: déclaration implicite de la fonction 'ualarm' [-Wimplicit-function-declaration ] tick.c: Dans la fonction 'tick': tick.c: 5: 10: avertissement: ignorer la valeur de retour de 'write', déclarée avec l'attribut warn_unused_result [-Wunused-result] :: Il semble que mon système (Ubuntu 12.04) le fasse pas le supporter. Cependant, au moins, il y a une page de manuel que ualarm devrait être dans unistd.h. (gcc est 4.6.3)
math

28

Avez-vous essayé watchavec le paramètre --precise?

watch -n 1 --precise "date '+%Y-%m-%d %H:%M:%S.%N' >> ~/Desktop/timelog-watch.txt"

De la page de manuel:

Normalement, cet intervalle est interprété comme le délai entre la fin d'une exécution de commande et le début de l'exécution suivante. Cependant, avec l'option -p ou --precise, vous pouvez essayer de regarder pour exécuter la commande toutes les secondes d'intervalle. Essayez-le avec ntptime et remarquez que la fraction de seconde reste (presque) identique, par opposition au mode normal où elle augmente continuellement.

Le paramètre peut ne pas être disponible sur votre système, cependant.

Vous devez également envisager ce qui devrait se passer lorsque l'exécution de votre programme nécessite plus d'une seconde. La prochaine exécution planifiée doit-elle être ignorée ou doit-elle être retardée?

Mise à jour : j'ai exécuté le script pendant un certain temps, et il n'a pas perdu une seule étape:

2561 lines
start: 2012-07-17 09:46:34.938805108
end:   2012-07-17 10:29:14.938547796

Mise à jour: le --precisedrapeau est un ajout de Debian, le correctif est cependant assez simple: http://patch-tracker.debian.org/patch/series/view/procps/1:3.2.8-9squeeze1/watch_precision_time.patch


Exactement le chemin à parcourir. J'aimerais pouvoir +10 cela.
krlmlr

Quelle version de watchprend en charge cette option? Ce n'était sur aucune des machines que j'ai vérifiées.
tylerl

Sa version 0.3.0, qui est la version actuelle sur Ubuntu 12.04. Il provient de la version 3.2.8-11ubuntu6 du paquet procps.
daniel kullmann

Hmm, le paquet source procps ne supporte pas --precise. Ceci est un ajout de Debian (3.2.8-9, watch_precision_time.patch)
daniel kullmann

1
Ok, mais identique à mdpc dans les commentaires sur la question: Cela peut également échouer lorsque votre système est soumis à une charge importante. Je viens de le tester en conjonction avec le stress (mettre la charge sur le disque et les cœurs) et voici ce qui se passe: 2012-07-24 07:20:21.864818595 2012-07-24 07:20:22.467458430 2012-07-24 07:20:23.068575669 2012-07-24 07:20:23.968415439 le contenu en temps réel (noyau, etc.) est là pour une raison!
math

18

crontaba une résolution de 1 minute. Si le temps de latence s’accumule par minute et s’initialise à la minute suivante, cette idée de base peut fonctionner:

* * * * * for second in $(seq 0 59); do /path/to/script.sh & sleep 1s;done

Notez qu'il script.shest également exécuté en arrière-plan. Cela devrait aider à minimiser le décalage qui s’accumule à chaque itération de la boucle.

En fonction du retard sleepgénéré, il est toutefois possible que le second 59 se chevauche avec le second 0 de la minute suivante.

EDIT pour ajouter des résultats, dans le même format que dans la question:

$ cat timelog-cron
2012-07-16 20:51:01
...
2012-07-16 22:43:00

1 heure 52 minutes = 6720 secondes

$ wc -l timelog-cron
6720 timelog-cron

0 problème de chronométrage, 0% de réduction. Toute accumulation de temps est réinitialisée toutes les minutes.


1
Puis-je demander pourquoi cela a été voté?
Izkata

2
C'est un hack laid
hhaamu

2
@hhaamu Qu'est-ce qui est laid? Les systèmes d'exploitation à usage général sur PC ne sont pas conçus pour des opérations très précises dans le temps, alors que pouvez-vous attendre de plus? Si vous voulez un chronométrage "élégant" et absolument précis, vous devez utiliser un autre programmateur de processeur, passer à un noyau en temps réel, utiliser un matériel dédié, etc. Ceci est une solution parfaitement légitime et je ne vois aucune raison de le faire. votes négatifs. C'est certainement une amélioration par rapport à celui qui n'avait que le "run in background" sans la ré-synchronisation périodique via cron.
jw013

1
De plus, arrêter c'est facile. Inutile de risquer de la tuer au milieu d'un cycle - supprimez l'entrée de la crontab et elle se termine toute seule à la fin de la minute.
Izkata

Vous avez juste de la chance que votre système cronsoit précis à la seconde près, mais ce n'est pas le cas en général.
Dmitry Grigoryev

15

Votre problème est que vous dormez pendant un certain temps après avoir exécuté votre programme sans tenir compte du temps écoulé depuis votre dernier sommeil.

Vous pouvez le faire, bash ou n’importe quel autre langage de programmation, mais la clé consiste à utiliser l’horloge pour déterminer la durée de la prochaine mise en veille. Avant de dormir, observez l'horloge, voyez combien de temps il vous reste et dormez la différence.

En raison de compromis en matière de planification des processus, il n'est pas garanti que vous vous réveilliez au même moment, mais vous devriez être assez proche (pas de charge de quelques ms ou moins de quelques centaines de ms sous charge). Et vous n'accumulerez pas d'erreur au fil du temps, car chaque fois que vous vous resynchronisez à chaque cycle de veille et supprimez toute erreur accumulée.

Si vous avez exactement besoin de l’horloge, vous recherchez un système d’exploitation en temps réel , conçu précisément à cet effet.


Je pense qu'il est également très probable que les programmes porg aient testé le blocage tout en exécutant le processus prévu - ce qu'ils devraient faire en toute logique pour éviter de tuer la machine sur laquelle ils fonctionnent.
symcbean

Que vous bloquiez ou non, le mécanisme fonctionne parfaitement. Si vous bloquez, vous dormez le temps qui reste après le blocage. Si vous ne bloquez pas, votre thread ou processus de synchronisation est en veille pendant que l’autre fonctionne. De toute façon, même résultat.
tylerl

@tylerl: À quoi ressemblerait la ligne de commande concrète de votre solution?
porg

Je suppose que vous vouliez dire la même chose comme @lynxlynxlynx
porg

@porg vous devez utiliser date +%S.%Npour obtenir le nombre de secondes avec une précision inférieure à la seconde et usleepdormir avec une précision inférieure à une seconde, mais après cela, ce n’est qu’une question de calcul.
tylerl

7

J'ai toujours juste renoncé à faire fonctionner quelque chose exactement à intervalle. Je pense que vous devrez écrire un programme C et faire très attention à ne pas dépasser la partie de l'intervalle d'une seconde avec votre propre code. Vous devrez probablement utiliser des threads ou plusieurs processus inter-communicants pour que cela fonctionne. Veillez à éviter les temps supplémentaires de démarrage du processus ou du processus.

Une référence qui semble pertinente date de 1993: une horloge d'échantillonnage aléatoire pour l'estimation de l'utilisation de la CPU et le profilage du code. Vous voudrez jeter un coup d'œil à l'annexe "Code source de l'adversaire" pour voir comment ils ont mesuré avec précision les intervalles de temps et se "réveiller". leur programme au bon moment. Comme le code a 19 ans, il ne portera probablement pas directement ou facilement, mais si vous le lisez et essayez de le comprendre, les principes en jeu pourraient guider votre code.

EDIT: trouvé une autre référence qui pourrait aider: Effets de la résolution d'horloge sur la planification de processus interactifs et de processus en temps réel souples Cela devrait vous aider pour n'importe quel fond théorique.


4

Jetez un coup d’oeil à nanosleep () (à partir de http://linux.about.com/library/cmd/blcmdl2_nanosleep.htm ). Au lieu de laisser votre programme dormir 1 seconde, laissez-le dormir (1 - il faut courir) secondes. Vous obtiendrez une bien meilleure résolution.


Vous pouvez faire la même chose avec régulier sleep, c.-à-d sleep 0.99. Le problème est que la durée d'exécution est loin d'être constante, même si sa valeur moyenne peut fluctuer dans le temps.
Dmitry Grigoryev

3

Essayez d’exécuter votre commande en arrière-plan pour ne pas trop affecter le minutage de la boucle, mais même cela ne suffira pas si vous ne voulez pas d’accumulation pendant de longues périodes, car il en coûte sûrement quelques millisecondes.

Donc, c’est probablement mieux, mais aussi probablement pas assez bon:

while [ true ]; do date "+%Y-%m-%d %H:%M:%S" & sleep 1; done | 
tee timelog-sleep.txt

Sur mon ordinateur, cela donnait 2 erreurs en 20 minutes, soit 0,1 par minute, ce qui représente une amélioration cinq fois moins importante que votre course.


Le problème, sleep 1c'est qu'il est garanti de dormir au moins une seconde, jamais moins. Par conséquent l'erreur s'accumule.
Hhaamu

Comparer les résultats de synchronisation de deux ordinateurs différents n'a aucune signification, à moins que vous n'ayez exécuté le code d'origine sur votre système et obtenu le même résultat que l'OP.
Dmitry Grigoryev

1

Moche mais ça marche. Vous devriez probablement repenser la conception de votre programme si vous avez besoin d'une boucle comme celle-ci. Il vérifie fondamentalement si la seconde entière est égale à la précédente et affiche le nombre de nanosecondes depuis le changement de seconde. La précision est influencée par le sommeil .001.

while true; do T=$( date +%s ); while [[ $T -eq $( date +%s ) ]]; do sleep .001; done; date "+%N nanoseconds late"; done

La précision est de l'ordre de la milliseconde, à condition que la "charge utile" date "+%N nanoseconds late"ne prenne pas plus longtemps qu'un peu moins d'une seconde. Vous pouvez réduire la charge du processeur en augmentant la période de veille ou, si cela ne vous dérange pas, remplacez simplement la commande de veille par true.

002112890 nanoseconds late
001847692 nanoseconds late
002273652 nanoseconds late
001317015 nanoseconds late
001650504 nanoseconds late
002180949 nanoseconds late
002338716 nanoseconds late
002064578 nanoseconds late
002160883 nanoseconds late

C'est une mauvaise pratique car vous faites en sorte que l'unité centrale interroge un événement et que vous gaspillez des cycles de l'unité centrale. Vous voudrez probablement vous associer à une interruption de la minuterie (ce qui n'est pas possible à partir de bash) ou utiliser un matériel dédié tel qu'un microcontrôleur. Un PC et son système d'exploitation ne sont pas conçus pour une précision de synchronisation élevée.


1

Une autre méthode consisterait à utiliser une suspension dans une boucle et à envoyer SIGCONT à partir d’un programme externe précis. L'envoi d'un signal est très léger et aura beaucoup moins de temps de latence que l'exécution de quelque chose. Vous pouvez également pré-mettre en file d'attente un tas de commandes avec la commande "at", presque personne n'utilise "at". Je ne suis pas sûr de sa précision.

Si la précision est essentielle et que vous souhaitez prendre au sérieux ce problème, cela ressemble au type d'application où vous utiliseriez généralement le RTOS, ce qui pourrait être fait sous Linux avec le noyau patché RT-Preempt, qui vous donnera la précision et une certaine mesure de interrompez le contrôle, mais cela risque d’être plus ennuyeux que ça vaut la peine.

https://rt.wiki.kernel.org/index.php/RT_PREEMPT_HOWTO

Xenomai pourrait également être utile, il s’agit d’une implémentation RTOS complète et est porté pour x86 et x86_64, mais une certaine programmation est nécessaire.

http://www.xenomai.org/index.php/Main_Page


1

Avec ksh93(qui a une virgule flottante $SECONDSet une construit sleep)

typeset -F SECONDS=0
typeset -i i=0
while true; do
   cmd
   sleep "$((++i - SECONDS))"
done

Le même script fonctionnera zshégalement mais invoquera la sleepcommande de votre système . zsha zselectintégré, mais avec une résolution de 1/100 seulement.


0

J'irais avec un petit programme C:

#include <sys/time.h>
#include <unistd.h>

int main(int argc, char **argv, char **envp)
{
    struct timeval start;
    int rc = gettimeofday(&start, NULL);
    if(rc != 0)
            return 1;

    for(;;)
    {
        struct timeval now;
        rc = gettimeofday(&now, NULL);
        useconds_t delay;
        if(now.tv_usec < start.tv_usec)
            delay = start.tv_usec - now.tv_usec;
        else
            delay = 1000000 - now.tv_usec + start.tv_usec;
        usleep(delay);
        pid_t pid = fork();
        if(pid == -1)
            return 1;
        if(pid == 0)
            _exit(execve(argv[1], &argv[1], envp));
    }
}

Ce programme s'attend à ce que le programme appelle avec le chemin complet comme premier argument et transmet tous les arguments restants. Il n'attendra pas que la commande se termine, il lancera donc plusieurs instances avec plaisir.

De plus, le style de codage est vraiment bâclé, et un certain nombre d’hypothèses sont émises qui peuvent ou non être garanties par les normes applicables, c’est-à-dire que la qualité de ce code est "fonctionne pour moi".

Ce programme aura des intervalles un peu plus longs ou plus courts lorsque l’horloge sera ajustée par NTP ou en la réglant manuellement. Si le programme doit gérer cela, POSIX fournit timer_create(CLOCK_MONOTONIC, ...)ce qui n’est pas affecté par cela.


0

Vous devriez garder une trace de l'heure actuelle et la comparer à l'heure de début. Donc, vous dormez une quantité de temps calculée à chaque itération, pas un montant fixe. De cette façon, vous n'accumulerez pas d'erreurs de chronométrage et ne vous éloignerez pas de l'endroit où vous devriez être, car vous réinitialisez chaque chronométrage de chaque boucle en temps absolu à partir du début.

De plus, certaines fonctions de veille reviennent tôt en cas d'interruption. Dans ce cas, vous devrez rappeler votre méthode de veille jusqu'à la fin du temps imparti.



0

Celui-ci peut fonctionner au moins 100 fois par seconde avec une résolution très précise.

L’existence du répertoire du nombre de boucles par minute crée le programme. Cette version prend en charge la résolution microseconde en supposant que votre ordinateur puisse le gérer. Le nombre d'exécutions par minute ne doit pas être divisible par 60. Il n'est pas non plus limité à 60. Je l'ai testé à 6000 et cela fonctionne.

Cette version peut être installée dans le répertoire /etc/init.d et exécutée en tant que service.

#! /bin/sh

# chkconfig: 2345 91 61
# description: This program is used to run all programs in a directory in parallel every X times per minute. \
#              Think of this program as cron with microseconds resolution.

# Microsecond Cron
# Usage: cron-ms start
# Copyright 2014 by Marc Perkel
# docs at http://wiki.junkemailfilter.com/index.php/How_to_run_a_Linux_script_every_few_seconds_under_cron"
# Free to use with attribution

# The scheduling is done by creating directories with the number of"
# executions per minute as part of the directory name."

# Examples:
#   /etc/cron-ms/7      # Executes everything in that directory  7 times a minute
#   /etc/cron-ms/30     # Executes everything in that directory 30 times a minute
#   /etc/cron-ms/600    # Executes everything in that directory 10 times a second
#   /etc/cron-ms/2400   # Executes everything in that directory 40 times a second

basedir=/etc/cron-ms

case "$1" in

   start|restart|reload)
   $0 stop
   mkdir -p /var/run/cron-ms
   for dir in $basedir/* ; do
      $0 ${dir##*/} &
   done
   exit
   ;;

   stop)
   rm -Rf /var/run/cron-ms
   exit
   ;;

esac

# Loops per minute is passed on the command line

loops=$1
interval=$((60000000/$loops))

# Just a heartbeat signal that can be used with monit to verify it's alive

touch /var/run/cron-ms

# After a restart the PIDs will be different allowing old processes to terminate

touch /var/run/cron-ms/$$

# Sleeps until a specific part of a minute with microsecond resolution. 60000000 is full minute

usleep $(( $interval - 10#$(date +%S%N) / 1000 % $interval ))

# Deleting the PID files exit the program

if [ ! -f /var/run/cron-ms/$$ ]
then
   exit
fi

# Run all the programs in the directory in parallel

for program in $basedir/$loops/* ; do
   if [ -x $program ] 
   then
      $program &> /dev/null &
   fi
done

exec $0 $loops
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.