Execute o comando unix precisamente em intervalos muito curtos SEM acumular atraso de tempo ao longo do tempo

38

Questão

Eu gostaria de poder executar um comando UNIX precisamente a cada segundo durante um longo período de tempo .

Preciso de uma solução, que não fique para trás depois de um certo tempo, devido ao tempo que o próprio comando precisa para execução. sleep , watch e um certo script python falharam comigo nesse aspecto.

No microcontrolador como o http://Arduino.cc, eu faria isso através de interrupções no relógio do hardware. Gostaria de saber se existe uma solução semelhante de script de shell com precisão de tempo. Todas as soluções que encontrei no StackExchange.com resultaram em um atraso de tempo notável, se executado durante horas. Veja os detalhes abaixo.

Finalidade prática / aplicação

Desejo testar se minha conexão de rede está continuamente ativada enviando registros de data e hora 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

As diferenças seriam os carimbos de hora não transmitidos. A partir disso, eu saberia a que horas minha LAN / WAN / ISP causa 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 deslocamento ao longo do tempo, pois o comando dentro do loop também leva um pouco de tempo.

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 temporização
  • 34520/34243 = 1,008 = 0,8% de desconto

Solução REPETIR PYTHON

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

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

Supõe-se evitar o deslocamento de tempo, mas falha ao fazê-lo.

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 temporização
  • 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 temporização.
  • 8499/8366 = 1,016 = 1,6% de desconto.
porg
fonte
5
Qual é a resolução, precisão e por que você precisa / para o que está usando?
jippie
O que acontece se você nicedorme no processo?
Tilo Wiklund
1
Você já pensou em usar a programação em TEMPO REAL para minimizar o atraso em uma chamada de suspensão ().
Mdpc
Parece-me que seu tempo sempre será afetado pelo estado operacional atual da caixa. Se algo fizer com que o programa com o qual você se preocupe seja descarregado do cache, você aumentará o tempo, a menos que possa garantir que o tempo de execução típico do programa seja significativamente menor que o intervalo desejado. Eu gostaria de estar em um sistema em tempo real, em uma configuração significativamente reduzida, sem mais ninguém conectado ou no modo de usuário único. Provavelmente, uma solução melhor é modificar o programa em questão para executar o próprio loop, em vez de invocá-lo de outro programa.
Corte Saw
2
Inicie cada comando em seu próprio encadeamento, dessa forma, qualquer bloqueio relacionado a IO não desperdiça seu tempo.
Joel Cornett

Respostas:

12

Como esse script Perl que acabei de criar 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();
}

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

Ele está em execução 45 minutos sem um único salto, e suspeito que continuará a fazê-lo, a menos que a) a carga do sistema fique tão alta que o fork () demore mais de um segundo ou b) um segundo de salto seja inserido.

Não é possível garantir, no entanto, que o comando seja executado em intervalos precisos de segundos, pois há alguma sobrecarga, mas duvido que seja muito pior do que uma solução baseada em interrupção.

Eu o executei por cerca de uma hora com date +%N(nanossegundos, extensão GNU) e executei algumas estatísticas nele. 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 rodava mais rápido que 270 µs 95% do tempo. Eu não acho que você pode vencê-lo, exceto por um programa em C.

hhaamu
fonte
1
Eu o executei da noite para o dia sem outros aplicativos de usuário ativos com um intervalo de 1 segundo e ele foi executado por 29241 segundos, sem um único segundo ignorado! Isso vai se encaixar no meu propósito. Em seguida, executei novamente esta manhã com um intervalo de 0,1 s; GNU datecom +%Napenas 3 minutos, ocorreu o seguinte erro: Time::HiRes::sleep(-0.00615549): negative time not invented yet at ~/bin/repeat.pl line 23.Linha 23 no meu script salvo:sleep $start - time();
porg
Se você executá-lo com intervalos de 0,01 s ou 0,001 s, é apenas uma questão de alguns segundos ou menos até que o programa seja interrompido com o erro "tempo negativo". Mas, para o meu propósito, cabe!
porg
28

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

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

Compilar

 gcc -O2 tick.c -o tick

Em seguida, anexe-o ao que você precisar fazer periodicamente da seguinte maneira:

./tick | while read x; do
    date "+%Y-%m-%d %H:%M:%S"
done | tee timelog-sleep.txt
Dave
fonte
Preciso de um shell especial ou C-std para isso? Eu o compilei (que deu um pequeno aviso de retorno ausente), mas nenhuma saída foi produzida.
math
@math Com -std=c99, você não receberá o aviso sobre o retorno ausente. Caso contrário, você não deve precisar de nada de especial. Você digitou errado um zero extra? strace ./tickirá mostrar-lhe o que está fazendo de uma perspectiva syscall
Dave
Eu recebo: gcc -O2 -std = c99 -o tick tick.c tick.c: Na função 'main': tick.c: 10: 5: aviso: declaração implícita da função 'ualarm' [-Wimplicit-function-statement ] tick.c: Na função 'tick': tick.c: 5: 10: aviso: ignorando o valor de retorno de 'write', declarado com o atributo warn_unused_result [-Wunused-result] :: Parece que meu sistema (Ubuntu 12.04) faz não suportá-lo. No entanto, pelo menos há uma página de manual em que ualarm deve estar em unistd.h. (gcc é 4.6.3)
math
28

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

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

Na página do manual:

Normalmente, esse intervalo é interpretado como o tempo decorrido entre a conclusão de uma execução de comando e o início da próxima execução. No entanto, com a opção -p ou --precise, é possível fazer com que o watch tente executar o comando a cada intervalo de segundos. Experimente com ntptime e observe como os segundos fracionários permanecem (quase) os mesmos, em oposição ao modo normal, onde eles aumentam continuamente.

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

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

Atualização : executei o script por algum tempo e ele não perdeu uma única etapa:

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

Atualização: A --preciseflag é uma adição do Debian, mas o patch é bastante simples: http://patch-tracker.debian.org/patch/series/view/procps/1:3.2.8-9squeeze1/watch_precision_time.patch

daniel kullmann
fonte
Exatamente o caminho a percorrer. Gostaria de poder +10 isso.
krlmlr
Qual versão do watchsuporta essa opção? Não estava em nenhuma das máquinas que verifiquei.
21412 Tylerl
Sua versão 0.3.0, que é a versão atual no Ubuntu 12.04. Vem da versão 3.2.8-11ubuntu6 do pacote procps.
Daniel kullmann 17/07/12
Hmm, o pacote de fontes procps não suporta --precise. Esta é uma adição Debian (3.2.8-9, watch_precision_time.patch)
Daniel Kullmann
1
Ok, mas o mesmo que mdpc nos comentários da pergunta afirmou: Isso também pode falhar quando o sistema está sob carga pesada. Acabei de testá-lo em conjunto com o estresse (colocando carga no disco e nos núcleos) e obtive o seguinte: 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 O material em tempo real (kernel etc.) está disponível por um motivo!
math
18

crontabtem uma resolução de 1 minuto. Se você estiver bem com o tempo de atraso acumulado por esse minuto e redefinido no próximo minuto, essa ideia básica poderá funcionar:

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

Observe que script.shtambém é executado em segundo plano. Isso deve ajudar a minimizar o atraso acumulado a cada iteração do loop.

Dependendo de quanto atraso é sleepgerado, existe a chance de o segundo 59 se sobrepor ao segundo 0 do minuto seguinte.

EDIT para lançar em alguns resultados, no mesmo formato da pergunta:

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

1 hora 52 minutos = 6720 segundos

$ wc -l timelog-cron
6720 timelog-cron

0 problemas de tempo, 0% de desconto. Qualquer acumulação de tempo é redefinida a cada minuto.

Izkata
fonte
1
Posso perguntar por que isso foi rebaixado?
21712 Izkata
2
É um truque feio
hhaamu 17/07/12
2
@hhaamu O que é feio nisso? Os SOs de uso geral nos PCs não são projetados para operações críticas de tempo muito precisas, então o que mais você pode esperar? Se você deseja um tempo "elegante" e absolutamente preciso, você precisará usar um agendador de CPU diferente, mudar para um kernel em tempo real ou usar hardware dedicado, etc. Essa é uma solução perfeitamente legítima e não vejo razão para votos negativos. Certamente é uma melhoria na que só teve a "execução em segundo plano" sem a ressincronização periódica via cron.
Jw013 17/07
1
Além disso, é fácil parar. Não é necessário arriscar matá-lo no meio de um ciclo - remova a entrada do crontab e ela termina sozinha no final do minuto.
21912 Izkata
Você tem sorte de que no seu sistema cronseja preciso o segundo, mas não é o caso em geral.
Dmitry Grigoryev
15

Seu problema é que você dorme por um período fixo de tempo após a execução do programa sem levar em consideração a quantidade de tempo decorrido desde a última vez em que dormiu.

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

Devido a compromissos de agendamento de processos, não é garantido que você acorde imediatamente, mas você deve estar bastante próximo (dentro de alguns ms descarregados ou dentro de algumas centenas de ms carregados). E você não acumulará erros ao longo do tempo, pois cada vez que estiver sincronizando novamente a cada ciclo de suspensão e removendo qualquer erro acumulado.

Se você precisa acertar o relógio exatamente, então o que você procura é um sistema operacional em tempo real , projetado exatamente para esse fim.

tylerl
fonte
Eu acho que também é muito provável que o porg dos programas tenha testado o bloco durante a execução do processo pretendido - o que logicamente eles devem fazer para evitar matar a máquina na qual estão executando.
21412 sycbean
Independentemente de você bloquear ou não, o mecanismo funciona bem. Se você bloquear, você dorme o tempo restante após o bloqueio. Se você não bloquear, seu processo ou thread de temporização está inativo enquanto o outro está funcionando. De qualquer maneira, o mesmo resultado.
22412 Tylerl
@tylerl: Como seria a linha de comando concreta para sua solução?
porg
Eu acho que você quis dizer o mesmo como @lynxlynxlynx
porg
@porg você precisa usar date +%S.%Npara obter o número de segundos com precisão de segundos e usleeppara dormir com precisão de segundos, mas depois disso é apenas uma questão de matemática.
22612 Tylerl
7

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

Uma referência que parece datas relevantes para 1993: A Randomized Sampling clock para utilização da CPU Estimativa e Código Profiling Você vai querer dar uma olhada no apêndice "Adversário Source Code" para ver como eles mediram com precisão intervalos de tempo, e "acordou" seu programa no momento correto. Como o código tem 19 anos, provavelmente não será portado direta ou facilmente, mas se você o ler e tentar entendê-lo, os princípios envolvidos poderão orientar seu código.

Edição: Encontrou outra referência que pode ajudar: Efeitos da resolução do relógio no agendamento de processos interativos e flexíveis em tempo real que devem ajudá-lo com qualquer embasamento teórico.

Bruce Ediger
fonte
4

Dê uma olhada em nanosleep () (em http://linux.about.com/library/cmd/blcmdl2_nanosleep.htm ). Em vez de fazer seu programa dormir 1 segundo, faça-o dormir (1 - quanto tempo demora para ser executado) segundos. Você terá uma resolução muito melhor.

woliveirajr
fonte
Você pode fazer o mesmo com regular sleep, ou seja sleep 0.99. O problema é que a quantidade de tempo que leva para executar está longe de ser constante, até mesmo seu valor médio pode variar ao longo do tempo.
Dmitry Grigoryev
3

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

Portanto, isso provavelmente é melhor, mas também 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 representa aproximadamente uma melhoria de cinco vezes na sua execução.

lynxlynxlynx
fonte
O problema sleep 1é que é garantido que você durma pelo menos um segundo - nunca menos. Portanto, o erro se acumula.
hhaamu
Comparar resultados de tempo de dois computadores diferentes não faz sentido, a menos que você tenha executado o código original no sistema e obtido o mesmo resultado do OP.
Dmitry Grigoryev
1

Feio, mas funciona. Você provavelmente deve repensar o design do seu programa se precisar de um loop como este. Ele basicamente verifica se o segundo inteiro atual é igual ao anterior verificado e imprime o número de nanossegundos desde a mudança do segundo. A precisão é influenciada pelo sono 0,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 pouco menos de um segundo. Você pode diminuir a carga da CPU aumentando o período de suspensão ou, se realmente não se importa, substitua o comando de suspensão 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

Essa é uma prática ruim, porque você basicamente faz uma pesquisa de CPU para um evento e está desperdiçando ciclos de CPU. Você provavelmente deseja se conectar a uma interrupção do 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 oferecer alta precisão de tempo.

jippie
fonte
1

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

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 o RTOS, o que poderia ser feito no Linux com o kernel corrigido pelo RT-Preempt, que fornecerá a precisão e alguma medida de controle de interrupção, mas pode ser mais incômodo do que vale a pena.

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

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

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

cdslashetc
fonte
1

With ksh93(que tem um ponto flutuante $SECONDSe um builtin sleep)

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

O mesmo script também funcionará zsh, mas invocará o sleepcomando do sistema . zshpossui um zselectbuiltin, mas apenas com resolução de 1/100.

Stéphane Chazelas
fonte
0

Eu iria com um pequeno programa 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 caminho completo como seu primeiro argumento e transmita os argumentos restantes. Ele não aguardará a conclusão do comando, portanto, iniciará várias instâncias.

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

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

Simon Richter
fonte
0

Você deve acompanhar a hora atual e compará-la com a hora de início. Portanto, você dorme uma quantidade calculada de tempo a cada iteração, não uma quantidade fixa. Dessa maneira, você não acumulará erros de tempo e se afastará de onde deveria estar, pois redefine seus tempos em cada loop para o tempo absoluto desde o início.

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

fwgx
fonte
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 a programação. Esta versão suporta resolução de microssegundos, assumindo que o seu computador possa lidar com isso. O número de execuções por minuto não precisa ser igualmente divisível por 60 nem é limitado a 60. 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
user56318
fonte