Execute o comando unix com precisão em intervalos muito curtos, sem acumular atraso de tempo ao longo do tempo

38

Pergunta

Gostaria de poder executar um comando do UNIX precisamente a cada segundo por um longo período de tempo .

Eu preciso de uma solução, que não fique para trás depois de um certo tempo, por causa do tempo que o comando precisa para a execução. dormir , assistir , e um certo script python todos me falharam nesse aspecto.

No microcontrolador, como o link , eu faria isso através de interrupções de clock de hardware. Gostaria de saber se existe uma solução semelhante de script de shell preciso no tempo. Todas as soluções que encontrei no StackExchange.com resultaram em um lapso de tempo perceptível, se forem executadas ao longo de horas. Veja os detalhes abaixo.

Propósito prático / aplicação

Eu quero testar se a minha conexão de rede está continuamente ativa, enviando timestamps via nc (netcat) a cada 1 segundo.

Remetente:

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

Receptor:

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

Após a conclusão, compare os dois logs:

diff netcat-sender.txt netcat-receiver.txt

Os diffs seriam os timestamps não transmitidos. A partir disso, eu saberia a que horas minha LAN / WAN / ISP causaria problemas.

Solução SLEEP

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

Obtém um certo offset ao longo do tempo, pois o comando dentro do loop também demora um pouco.

Precisão

cat timelog-sleep.txt

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

Segundos decorridos: 34520

wc -l timelog-sleep.txt

Linhas no arquivo: 34243

Precisão resumida:

  • 34520-34243 = 277 problemas de tempo
  • 34520/34243 = 1,008 = 0,8% de desconto

Solução REPETIÇÃO PYTHON

Encontrado em: Repita um comando Unix a cada x segundo para sempre

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

Deverá evitar o deslocamento de tempo, mas não o faz.

Precisão

wc -l timelog-repeat-py.txt

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

Segundos decorridos: 10960

wc -l timelog-repeat-py.txt

Linhas no arquivo: 10859

Precisão resumida:

  • 10960-10859 = 101 problemas de tempo
  • 10960/10859 = 1,009 = 0,9% de desconto

Solução RELÓGIO

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

Precisão

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

Segundos decorridos: 8499

wc -l timelog-watch.txt

Linhas no arquivo: 8366

Precisão resumida:

  • 8499-8366 = 133 problemas de tempo.
  • 8499/8366 = 1,016 = 1,6% de desconto.
por porg 16.07.2012 / 17:33

15 respostas

12

Como esse script Perl que eu acabei de fazer funciona?

#!/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'

Ele está rodando 45 minutos sem um único salto, e eu suspeito que continuará a fazê-lo a menos que a) carga do sistema se torne tão alta que fork () demore mais de um segundo ou b) um segundo seja inserido. / p>

Não pode garantir, no entanto, que o comando seja executado em segundos intervalos exatos, pois há alguma sobrecarga, mas duvido que seja muito pior do que uma solução baseada em interrupções.

Eu corri por cerca de uma hora com date +%N (nanossegundos, extensão GNU) e fiz algumas estatísticas sobre ele. O maior atraso foi de 1 155 microssegundos. Média (média aritmética) 216 µs, mediana 219 µs, desvio padrão 42 µs. Ele correu mais rápido que 270 µs em 95% do tempo. Eu não acho que você pode vencê-lo, exceto por um programa em C.

    
por 16.07.2012 / 19:21
28

A função POSIX ualarm() permite agendar o kernel para sinalizar periodicamente seu processo, com precisão de microssegundo.

Crie um programa simples:

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

Compile

 gcc -O2 tick.c -o tick

Em seguida, anexe-o ao que você precisa fazer periodicamente assim:

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

Você já tentou watch com o parâmetro --precise ?

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

Da página do manual:

Normally, this interval is interpreted as the amout of time between the completion of one run of command and the beginning of the next run. However, with the -p or --precise option, you can make watch attempt to run command every interval seconds. Try it with ntptime and notice how the fractional seconds stays (nearly) the same, as opposed to normal mode where they continuously increase.

O parâmetro pode não estar disponível em seu sistema.

Você também deve considerar o que deve acontecer quando a execução do seu programa precisa de mais de um segundo. A próxima execução programada deve ser ignorada ou atrasada?

Atualizar : eu corri o script por algum tempo e não perdi uma única etapa:

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

Atualização: O --precise flag é uma adição do Debian, mas o patch é bem simples: link

    
por 17.07.2012 / 09:48
18

crontab tem uma resolução de 1 minuto. Se você está bem com o tempo de latência acumulado por esse minuto e, em seguida, redefinindo no próximo minuto, essa ideia básica poderia funcionar:

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

Observe que script.sh também é executado em segundo plano. Isso deve ajudar a minimizar o atraso que se acumula a cada iteração do loop.

Dependendo da quantidade de atraso que o sleep gera, existe a chance de o segundo 59 se sobrepor ao segundo 0 do minuto seguinte.

EDITAR para inserir alguns resultados, no mesmo formato da pergunta:

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

1 hora e 52 minutos = 6720 segundos

$ wc -l timelog-cron
6720 timelog-cron

0 problemas de tempo, 0% de desconto. Qualquer acúmulo de tempo é redefinido a cada minuto.

    
por 16.07.2012 / 20:15
15

Seu problema é que você está dormindo por um período fixo de tempo depois de executar o programa sem levar em consideração a quantidade de tempo decorrido desde a última vez que você dormiu.

Você pode fazer isso é bash ou qualquer outra linguagem de programação, mas a chave é usar o relógio para determinar por quanto tempo agendar o próximo sono. Antes de dormir, verifique o relógio, veja quanto tempo resta e durma a diferença.

Devido a compromissos de agendamento de processos, você não tem a garantia de acordar no relógio, mas deve estar razoavelmente perto (dentro de alguns ms descarregados ou dentro de algumas centenas de ms sob carga). E você não acumulará erros ao longo do tempo, porque cada vez que você estiver sincronizando novamente em cada ciclo de sono e removendo qualquer erro acumulado.

Se você precisa acertar o relógio, o que você está procurando é um sistema operacional em tempo real , que são projetados exatamente para essa finalidade.

    
por 16.07.2012 / 23:34
7

Eu sempre desisti de executar algo exatamente no intervalo. Eu acho que você terá que escrever um programa em C, e prestar muita atenção para não exceder a parte do intervalo de 1 segundo com o seu próprio código. Você provavelmente terá que usar processos threading ou múltiplos de intercomunicação para fazer isso funcionar. Tome cuidado para evitar a sobrecarga do processo ou o início da inicialização do processo.

Uma referência que parece relevante data de 1993: Um relógio de amostragem aleatório para estimativa de utilização da CPU e Perfil de código Você vai querer dar uma olhada no apêndice "Adversary Source Code" para ver como eles mediram com precisão os intervalos de tempo, e "acordou" o seu programa na hora certa. Como o código tem 19 anos, ele provavelmente não será portado de maneira direta ou fácil, mas se você o ler e tentar entendê-lo, os princípios envolvidos podem guiar seu código.

EDITAR: Encontrou outra referência que pode ajudar: Efeitos da Resolução do Relógio no Agendamento de Realidade Interativa e Suave Processos Temporais Isso deve ajudá-lo com qualquer formação teórica.

    
por 16.07.2012 / 18:35
4

Dê uma olhada no nanosleep () (no link ). Em vez de fazer o seu programa dormir 1 segundo, faça-o dormir (1 - quantidade demora a correr) segundos. Você terá uma resolução muito melhor.

    
por 16.07.2012 / 22:00
3

Tente executar seu comando em segundo plano para que ele não afete muito o tempo do loop, mas mesmo isso não será suficiente se você não quiser acumular por longos períodos de tempo, pois certamente há um custo associado a alguns milissegundos associado com isso.

Então, isso provavelmente é melhor, mas provavelmente ainda não é bom o suficiente:

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

No meu computador, isso causou 2 erros em 20 minutos ou 0,1 por minuto, o que é aproximadamente uma pequena melhoria de cinco vezes na sua corrida.

    
por 16.07.2012 / 19:05
1

Feio mas funciona. Você provavelmente deve repensar o design do seu programa se você precisar de um loop como este. Basicamente verifica se o segundo inteiro atual é igual ao verificado anteriormente e imprime o número de nanossegundos desde a mudança do segundo. A precisão é influenciada pelo sono .001.

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

A precisão está em milissegundos, desde que a 'carga útil' date "+%N nanoseconds late" não demore mais do que um segundo. Você pode diminuir a carga da CPU aumentando o período de suspensão ou, se realmente não se importa, basta substituir o comando sleep por 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

Isso é uma prática ruim porque basicamente você faz a pesquisa de CPU para um evento e está desperdiçando ciclos de CPU. Você provavelmente deseja anexar a uma interrupção de timer (não é possível a partir do bash) ou usar hardware dedicado como um microcontrolador. Um PC e seu sistema operacional não foram projetados para alta precisão de tempo.

    
por 16.07.2012 / 19:56
1

Outro método seria usar uma suspensão em um loop e enviar o SIGCONT a partir de um programa externo preciso. Enviar um sinal é muito leve e terá muito menos latência do que executar alguma coisa. Você também pode pré-enfileirar um monte de comandos com o comando "at", dificilmente alguém usa "at" mais, não tenho certeza de quão precisa é.

Se a precisão é crítica e você quer levar isso a sério, isso soa como o tipo de aplicativo em que você normalmente usaria RTOS, o que poderia ser feito no Linux com o kernel corrigido RT-Preempt, que lhe dará a precisão e alguma medida de controle de interrupção, mas pode ser mais incômodo do que vale a pena.

link

O Xenomai também pode ser útil, é uma implementação completa do RTOS e é portado para x86 e x86_64, mas há alguma programação envolvida.

link

    
por 16.07.2012 / 20:44
1

Com ksh93 (que tem um ponto flutuante $SECONDS e um sleep incorporado)

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

O mesmo script também funcionará com zsh , mas invocará o comando sleep do seu sistema. zsh tem zselect incorporado, mas apenas com resolução de 1/100.

    
por 13.11.2014 / 17:27
0

Eu iria com um pequeno programa em 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));
    }
}

Este programa espera que o programa chame com o caminho completo como seu primeiro argumento e transmite os argumentos restantes. Ele não irá aguardar o término do comando, por isso, ele iniciará várias instâncias com prazer.

Além disso, o estilo de codificação aqui é realmente desleixado, e uma série de suposições são feitas que podem ou não ser garantidas pelos padrões aplicáveis, ou seja, a qualidade deste código é "funciona para mim".

Este programa terá intervalos mais longos ou mais curtos quando o relógio for ajustado pelo NTP ou manualmente. Se o programa deve lidar com isso, o POSIX fornece timer_create(CLOCK_MONOTONIC, ...) que não é afetado por isso.

    
por 16.07.2012 / 23:15
0

Você deve acompanhar a hora atual e compará-la à hora de início. Então você dorme uma quantidade calculada de tempo a cada iteração, não uma quantia fixa. Desta forma, você não acumulará erros de temporização e mudará para longe de onde deveria estar, porque você redefine seus timings cada loop para o tempo absoluto desde o início.

Além disso, algumas funções do sono retornam cedo se houver uma interrupção, portanto, nesse caso, você terá que chamar seu método de suspensão novamente até que o tempo total tenha passado.

    
por 17.07.2012 / 15:59
0

Aqui está um script bash e altamente preciso. Usa o usleep para precisão de microssegundo.

link

Existem 3 scripts lá. Olhe para aquele no fundo. Vai fazer até cerca de 2400 execuções por minuto com razoável precisão. E é muito simples.

    
por 23.01.2014 / 18:16
0

Este pode ser executado pelo menos 100 vezes por segundo com resolução muito precisa.

A existência do diretório do número de loops por minuto cria o agendamento. Esta versão suporta resolução de microssegundos, supondo que o seu computador pode lidar com isso. O número de execuções por minuto não tem que ser divisível por 60, nem é limitado a 60 Eu o testei para 6000 e funciona.

Esta versão pode ser instalada no diretório /etc/init.d e executada como um serviço.

#! /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
    
por 05.03.2014 / 04:43