Esegui il comando unix con precisione a intervalli molto brevi SENZA accumulare ritardo nel tempo


38

Domanda

Mi piacerebbe poter eseguire un comando UNIX esattamente ogni secondo per un lungo periodo di tempo .

Ho bisogno di una soluzione, che non è in ritardo dopo un certo tempo, a causa del tempo necessario al comando stesso per l'esecuzione. sleep , watch e un certo script Python mi hanno deluso in questo senso.

Su un microcontrollore come http://Arduino.cc lo farei tramite interruzioni di clock hardware. Mi piacerebbe sapere se esiste una simile soluzione di script di shell precisa nel tempo. Tutte le soluzioni che ho trovato all'interno di StackExchange.com hanno comportato un notevole ritardo, se eseguito per ore. Vedi i dettagli di seguito.

Scopo / applicazione pratica

Voglio verificare se la mia connessione di rete è continuamente attiva inviando timestamp via nc(netcat) ogni 1 secondo.

Mittente:

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

Ricevitore:

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

Dopo il completamento, confrontare i due registri:

diff netcat-sender.txt netcat-receiver.txt

Le differenze sarebbero i timestamp non trasmessi. Da questo vorrei sapere a che ora la mia LAN / WAN / ISP crea problemi.


Soluzione DORMIRE

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

Ottiene un certo offset nel tempo, poiché anche il comando all'interno del ciclo richiede un po 'di tempo.

Precisione

cat timelog-sleep.txt

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

Secondi trascorsi: 34520

wc -l timelog-sleep.txt

Righe nel file: 34243

Riepilogo di precisione:

  • 34520-34243 = 277 problemi di temporizzazione
  • 34520/34243 = 1.008 = 0.8% di sconto

Soluzione REPEAT PYTHON

Trovato su: ripetere un comando Unix ogni x secondi per sempre

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

Presumibilmente per evitare il time offset, ma non riesce a farlo.

Precisione

wc -l timelog-repeat-py.txt

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

Secondi trascorsi: 10960

wc -l timelog-repeat-py.txt

Righe nel file: 10859

Riepilogo di precisione:

  • 10960-10859 = 101 problemi di temporizzazione
  • 10960/10859 = 1.009 = 0.9% di sconto

Soluzione WATCH

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

Precisione

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

Secondi trascorsi: 8499

wc -l timelog-watch.txt

Righe nel file: 8366

Riepilogo di precisione:

  • 8499-8366 = 133 problemi di temporizzazione.
  • 8499/8366 = 1.016 = 1.6% di sconto.

5
Qual è la risoluzione richiesta, la precisione e perché ne hai bisogno / per cosa la stai usando?
jippie,

Cosa succede se niceil processo che dorme?
Tilo Wiklund,

1
Hai mai pensato di utilizzare la programmazione REAL TIME per ridurre al minimo il ritardo su una chiamata sleep ().
mdpc,

Mi sembra che il tuo tempismo sia sempre influenzato dallo stato operativo attuale della scatola. Se qualcosa fa sì che il programma a cui tieni venga scaricato dalla cache, otterrai un aumento nel tempo, a meno che tu non possa garantire che il runtime tipico del programma sia significativamente inferiore all'intervallo desiderato. Vorrei essere su un sistema in tempo reale, in una configurazione notevolmente ridotta senza nessun altro connesso o in modalità utente singolo. Probabilmente una soluzione migliore è quella di modificare il programma in questione per fare il ciclo stesso, invece di invocarlo da un altro programma.
Hack Saw è stato il

2
Avvia ogni comando nel suo thread, in questo modo qualsiasi blocco correlato all'IO non ti farà perdere tempo.
Joel Cornett,

Risposte:


12

Come funziona questo script Perl che ho appena montato?

#!/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();
}

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

Ha funzionato per 45 minuti senza un singolo salto, e sospetto che continuerà a farlo a meno che a) il carico del sistema non sia così elevato da inserire fork () più di un secondo o b) un secondo di salto.

Non può garantire, tuttavia, che il comando venga eseguito esattamente al secondo intervallo, poiché esiste un certo sovraccarico, ma dubito che sia molto peggio di una soluzione basata su interrupt.

L'ho eseguito per circa un'ora con date +%N(nanosecondi, estensione GNU) e ho eseguito alcune statistiche su di esso. Il maggior ritardo che aveva era di 1 155 microsecondi. Media (media aritmetica) 216 µs, mediana 219 µs, deviazione standard 42 µs. Ha funzionato più velocemente di 270 µs il 95% delle volte. Non penso che tu possa batterlo se non con un programma C.


1
L'ho eseguito durante la notte senza altre applicazioni utente attive con un intervallo di 1 secondo e ha funzionato per 29241 secondi, senza un solo secondo saltato! Questo si adatterà al mio scopo. Poi l'ho eseguito di nuovo stamattina con un intervallo di 0,1 secondi, GNU datecon +%Ne dopo solo 3 minuti ha gettato Time::HiRes::sleep(-0.00615549): negative time not invented yet at ~/bin/repeat.pl line 23.sleep $start - time();
quell'errore

Se lo esegui con intervalli di 0,01 secondi o 0,001 secondi, è solo questione di pochi secondi o meno fino a quando il programma non si interrompe con l'errore "tempo negativo". Ma per il mio scopo si adatta!
porg,

28

La ualarm()funzione POSIX consente di programmare il kernel per segnalare periodicamente il processo, con precisione al microsecondo.

Prepara un semplice programma:

 #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();
 }

Compilare

 gcc -O2 tick.c -o tick

Quindi collegalo a tutto ciò che ti serve periodicamente in questo modo:

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

Ho bisogno di una shell speciale o C-std per questo? L'ho compilato (che ha dato un piccolo avvertimento in caso di mancato ritorno) ma non è stato prodotto alcun output.
matematica

@math With -std=c99, non riceverai l'avviso sul reso mancante. Altrimenti, non dovresti aver bisogno di qualcosa di speciale. Hai sbagliato a digitare uno zero in più? strace ./tickti mostrerà cosa sta facendo da una prospettiva generale
Dave,

Ottengo: gcc -O2 -std = c99 -o tick tick.c tick.c: nella funzione 'principale': tick.c: 10: 5: avviso: dichiarazione implicita della funzione 'ualarm' [-Wimplicit-function-dichiarazione ] tick.c: nella funzione 'tick': tick.c: 5: 10: avviso: ignora il valore di ritorno di 'write', dichiarato con l'attributo warn_unused_result [-Wunused-result] :: Sembra che il mio sistema (Ubuntu 12.04) faccia non supportarlo. Comunque almeno c'è una pagina man che ualarm dovrebbe essere in unistd.h. (gcc è 4.6.3)
matematica

28

Hai provato watchcon il parametro --precise?

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

Dalla pagina man:

Normalmente, questo intervallo viene interpretato come il tempo a disposizione tra il completamento di una corsa di comando e l'inizio della corsa successiva. Tuttavia, con l'opzione -p o --precise, puoi fare in modo che watch tenti di eseguire il comando ogni intervallo di secondi. Provalo con ntptime e nota come i secondi frazionari rimangono (quasi) gli stessi, al contrario della modalità normale in cui aumentano continuamente.

Tuttavia, il parametro potrebbe non essere disponibile sul tuo sistema.

Dovresti anche considerare cosa dovrebbe accadere quando l'esecuzione del tuo programma richiede più di un secondo. La prossima esecuzione pianificata deve essere ignorata o deve essere eseguita in ritardo?

Aggiornamento : ho eseguito lo script per qualche tempo e non ha perso un solo passaggio:

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

Aggiornamento: la --precisebandiera è un'aggiunta Debian, la patch è comunque piuttosto semplice: http://patch-tracker.debian.org/patch/series/view/procps/1:3.2.8-9squeeze1/watch_precision_time.patch


Esattamente la strada da percorrere. Vorrei poterlo fare +10.
krlmlr,

Quale versione di watchsupporta questa opzione? Non era su nessuna delle macchine che ho controllato.
Tylerl,

La sua versione 0.3.0, che è la versione corrente su Ubuntu 12.04. Viene dalla versione 3.2.8-11ubuntu6 del pacchetto procps.
Daniel Kullmann,

Hmm, il pacchetto sorgente procps non supporta --precise. Questa è un'aggiunta Debian (3.2.8-9, watch_precision_time.patch)
daniel kullmann

1
Ok, ma lo stesso di mdpc nei commenti sulla domanda dichiarata: questo potrebbe anche fallire quando il tuo sistema è sotto carico. L'ho appena testato insieme allo stress (caricamento su disco e core) e ho ottenuto questo: 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 il materiale in tempo reale (kernel ecc.) È disponibile per una ragione!
matematica

18

crontabha una risoluzione di 1 minuto. Se stai bene con il ritardo accumulato per quel minuto e poi resettato al minuto successivo, questa idea di base potrebbe funzionare:

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

Si noti che script.shviene eseguito anche in background. Ciò dovrebbe aiutare a ridurre al minimo il ritardo che si accumula con ogni iterazione del ciclo.

A seconda della quantità di ritardo sleepgenerata, esiste tuttavia la possibilità che il secondo 59 si sovrapponga al secondo 0 del minuto successivo.

MODIFICA per inserire alcuni risultati, nello stesso formato della domanda:

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

1 ora 52 minuti = 6720 secondi

$ wc -l timelog-cron
6720 timelog-cron

0 problemi di temporizzazione, 0% di sconto. L'accumulo di tempo viene ripristinato ogni minuto.


1
Posso chiederti perché questo è stato sottoposto a downgrade?
Izkata,

2
È un brutto hack
hhaamu il

2
@hhaamu Cosa c'è di brutto al riguardo? I sistemi operativi generici su PC non sono progettati per operazioni critiche di temporizzazione molto precise, quindi cosa si può aspettare di più? Se desideri un tempismo "elegante" e assolutamente preciso, dovresti utilizzare un diverso programmatore della CPU, o passare a un kernel in tempo reale, o utilizzare hardware dedicato, ecc. Questa è una soluzione perfettamente legittima e non vedo alcun motivo per downvotes. È certamente un miglioramento rispetto a quello che ha avuto solo "run in background" senza la periodica risincronizzazione tramite cron.
jw013,

1
Inoltre fermarlo è facile. Non c'è bisogno di rischiare di ucciderlo nel bel mezzo di un ciclo: rimuovi l'ingresso dal crontab e termina da solo alla fine del minuto.
Izkata,

Sei solo fortunato che sul tuo sistema cronsia preciso al secondo, ma non è il caso in generale.
Dmitry Grigoryev il

15

Il tuo problema è che stai dormendo per un determinato periodo di tempo dopo aver eseguito il programma senza prendere in considerazione il tempo trascorso dall'ultima volta che hai dormito.

Puoi farlo bash o qualsiasi altro linguaggio di programmazione, ma la chiave è usare l'orologio per determinare per quanto tempo programmare il prossimo sonno. Prima di dormire, controlla l'orologio, vedi quanto tempo ti rimane e dormi la differenza.

A causa dei compromessi di pianificazione del processo, non è garantito il risveglio al momento giusto, ma si dovrebbe essere abbastanza vicini (entro pochi ms scaricati o entro poche centinaia di sotto sotto carico). E non accumulerai errori nel tempo perché ogni volta ti risincronizzi su ogni ciclo di sospensione e rimuovi qualsiasi errore accumulato.

Se devi premere esattamente il segno di spunta dell'orologio, allora quello che stai cercando è un sistema operativo in tempo reale , progettato esattamente per questo scopo.


Penso che sia anche molto probabile che i programmi porg abbiano testato il blocco durante l'esecuzione del processo previsto, cosa che logicamente dovrebbero fare per evitare di uccidere la macchina su cui sono in esecuzione.
symcbean,

Che tu blocchi o meno, il meccanismo funziona perfettamente. Se blocchi, dormi il tempo rimasto dopo il blocco. Se non si blocca, il thread o il processo di temporizzazione è inattivo mentre l'altro sta funzionando. Ad ogni modo, stesso risultato.
Tylerl,

@tylerl: Come sarebbe la riga di comando concreta per la tua soluzione?
porg,

Immagino volessi
porg

@porg devi usare date +%S.%Nper ottenere il numero di secondi con precisione al secondo, e usleepdormire con precisione al secondo, ma dopo è solo una questione di matematica.
Tylerl,

7

Ho sempre rinunciato a far funzionare qualcosa esattamente a intervalli. Penso che dovrai scrivere un programma C e prestare molta attenzione a non superare la porzione dell'intervallo di 1 secondo con il tuo codice. Probabilmente dovrai usare il threading o più processi intercomunicanti per farlo funzionare. Fare attenzione a evitare il sovraccarico del tempo di avvio del thread o di avvio del processo.

Un riferimento che sembra pertinente risale al 1993: un orologio di campionamento randomizzato per l'utilizzo della CPU Stima e profilazione del codice Dovresti dare un'occhiata all'appendice "Codice sorgente avversario" per vedere come hanno misurato accuratamente gli intervalli di tempo e "svegliato" il loro programma al momento giusto. Poiché il codice ha 19 anni, probabilmente non verrà trasferito direttamente o facilmente, ma se lo leggi e cerchi di capirlo, i principi coinvolti potrebbero guidare il tuo codice.

EDIT: ho trovato un altro riferimento che potrebbe essere d'aiuto: Effetti della risoluzione del clock sulla programmazione di processi interattivi e soft in tempo reale che dovrebbero aiutarti con qualsiasi background teorico.


4

Dai un'occhiata a nanosleep () (da http://linux.about.com/library/cmd/blcmdl2_nanosleep.htm ). Invece di mettere in pausa il programma 1 secondo, impostalo in modalità sospensione (1 - quantità necessaria per l'esecuzione) secondi. Otterrai una risoluzione molto migliore.


Puoi fare lo stesso con regolari sleep, ad es sleep 0.99. Il problema è che il tempo necessario per l'esecuzione è lungi dall'essere costante, anche il suo valore medio può variare nel tempo.
Dmitry Grigoryev il

3

Prova a eseguire il comando in background in modo che non influisca molto sui tempi del loop, ma anche questo non sarà sufficiente se non vuoi accumulare per lunghi periodi di tempo in quanto vi è sicuramente un costo di pochi millisecondi associato ad esso.

Quindi, questo è probabilmente migliore, ma probabilmente non è ancora abbastanza buono:

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

Sul mio computer questo ha dato 2 errori in 20 minuti o 0,1 al minuto, il che è circa un miglioramento di cinque volte inferiore rispetto alla corsa.


Il problema sleep 1è che è garantito per dormire almeno un secondo - mai meno. Quindi l'errore si accumula.
hhaamu,

Il confronto dei risultati di temporizzazione da due computer diversi è piuttosto insignificante, a meno che non sia stato eseguito il codice originale sul sistema e ottenuto lo stesso risultato dell'OP.
Dmitry Grigoryev il

1

Brutto ma funziona. Probabilmente dovresti ripensare il design del tuo programma se hai bisogno di un loop come questo. In pratica controlla se l'intero secondo corrente è uguale al precedente controllato e stampa il numero di nanosecondi dal cambio del secondo. La precisione è influenzata dal sonno .001.

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

La precisione è espressa in millisecondi, a condizione che il "payload" date "+%N nanoseconds late"non richieda più tempo di poco meno di un secondo. Puoi ridurre il carico della CPU aumentando il periodo di sospensione o se davvero non ti dispiace semplicemente sostituire il comando di sospensione con 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

Questa è una cattiva pratica perché fondamentalmente fai il polling della CPU per un evento e stai sprecando i cicli della CPU. Probabilmente vuoi collegarti a un interrupt timer (non possibile da bash) o usare hardware dedicato come un microcontrollore. Un PC e il suo sistema operativo non sono progettati per un'elevata precisione di temporizzazione.


1

Un altro metodo sarebbe quello di utilizzare una sospensione in un ciclo e inviare SIGCONT da un preciso programma esterno. L'invio di un segnale è molto leggero e avrà una latenza molto inferiore rispetto all'esecuzione di qualcosa. Potresti anche pre-mettere in coda un gruppo di comandi con il comando "at", quasi nessuno usa "at" più non sono sicuro di quanto sia preciso.

Se la precisione è fondamentale e vuoi prenderti sul serio, questo suona come il tipo di applicazione in cui normalmente dovresti usare RTOS, che potrebbe essere fatto sotto Linux con il kernel patch RT-Preempt, che ti darà la precisione e una certa misura di interrompere il controllo, ma potrebbe essere più fastidioso di quanto valga la pena.

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

Anche Xenomai potrebbe essere utile, è un'implementazione RTOS completa ed è portato su x86 e x86_64, ma c'è qualche programmazione in gioco.

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


1

Con ksh93(che ha un punto mobile $SECONDSe un builtin sleep)

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

Lo stesso script funzionerà anche con zshma invocherà il sleepcomando del tuo sistema . zshha un zselectbuilt-in, ma solo con risoluzione 1/100.


0

Vorrei andare con un piccolo programma 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));
    }
}

Questo programma si aspetta che il programma chiami con il percorso completo come primo argomento e trasmetta tutti gli argomenti rimanenti. Non attenderà il completamento del comando, quindi avvierà felicemente più istanze.

Inoltre, lo stile di codifica qui è davvero sciatto, e vengono fatte alcune ipotesi che possono o meno essere garantite dagli standard applicabili, vale a dire la qualità di questo codice è "funziona per me".

Questo programma avrà intervalli leggermente più lunghi o più brevi quando l'orologio viene regolato da NTP o impostandolo manualmente. Se il programma deve gestirlo, POSIX fornisce ciò timer_create(CLOCK_MONOTONIC, ...)che non è interessato da questo.


0

Dovresti tenere traccia dell'ora corrente e confrontarla con l'ora di inizio. Quindi dormi una quantità calcolata di tempo ogni iterazione, non un importo fisso. In questo modo non accumulerai errori di temporizzazione e ti allontanerai da dove dovresti essere perché reimposti i tuoi tempi su ogni ciclo a tempo assoluto dall'inizio.

Inoltre, alcune funzioni di sospensione tornano presto in caso di interruzione, quindi in questo caso dovrai chiamare nuovamente il tuo metodo di sospensione fino a quando non sarà trascorso l'intero periodo di tempo.



0

Questo può essere eseguito almeno 100 volte al secondo con una risoluzione molto accurata.

L'esistenza della directory del numero di loop al minuto crea il programma. Questa versione supporta la risoluzione dei microsecondi presupponendo che il computer sia in grado di gestirlo. Il numero di esecuzioni al minuto non deve essere divisibile uniformemente per 60 né è limitato a 60, l'ho provato a 6000 e funziona.

Questa versione può essere installata nella directory /etc/init.d ed eseguita come servizio.

#! /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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.