精确地以非常短的时间间隔运行unix命令,而不会随着时间的推移而累积时间滞后


38

我希望能够在很长一段时间内每秒精确地运行UNIX命令。

我需要一个解决方案,该解决方案在一定时间后不会落后,因为命令本身需要执行时间。在这方面,sleepwatch和某些python脚本都使我失败了。

在诸如http://Arduino.cc之类的微控制器上,我将通过硬件时钟中断来实现。我想知道是否存在类似的时间精确的shell脚本解决方案。我在StackExchange.com中找到的所有解决方案,如果运行数小时,将导致明显的时间延迟。请参阅下面的详细信息。

实际用途/应用

我想通过nc每1秒通过(netcat)发送时间戳来测试我的网络连接是否连续不断。

发件人:

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

接收方:

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

完成后,比较两个日志:

diff netcat-sender.txt netcat-receiver.txt

差异将是未传输的时间戳。由此,我会知道我的LAN / WAN / ISP在什么时候出现问题。


解决方案休眠

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

随着时间的推移获得一定的偏移量,因为循环内的命令也需要一些时间。

精确

cat timelog-sleep.txt

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

秒已逝:34520

wc -l timelog-sleep.txt

文件中的行:34243

精度总结:

  • 34520-34243 = 277时序问题
  • 34520/34243 = 1.008 = 0.8%折扣

解决方案重复台球

发现于:永远每x秒重复一次Unix命令

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

本来可以避免时间偏移,但不能这样做。

精确

wc -l timelog-repeat-py.txt

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

秒已过:10960

wc -l timelog-repeat-py.txt

文件中的行数:10859

精度总结:

  • 10960-10859 = 101计时问题
  • 10960/10859 = 1.009 = 0.9%折扣

解决方案观察

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

精确

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

秒已过:8499

wc -l timelog-watch.txt

文件中的行数:8366

精度总结:

  • 8499-8366 = 133计时问题。
  • 8499/8366 = 1.016 = 1.6%折扣

5
所需的分辨率,准确性是什么?为什么需要它?/将其用于什么用途?
jippie 2012年

如果您nice进入休眠状态,会发生什么?
蒂洛·维克伦德

1
但是,您是否使用REAL TIME调度希望将sleep()调用的延迟减至最小。
mdpc

令我惊讶的是,您的时间安排总是会受到包装盒当前操作状态的影响。如果某种原因导致您需要从缓存中卸载所需的程序,则时间会增加,除非可以保证程序的典型运行时间大大少于所需的时间间隔。我希望在一个实时系统上,在没有其他人登录的情况下进行精简配置或以单用户模式运行。可能更好的解决方案是修改有问题的程序以执行循环本身,而不是从另一个程序中调用它。
Hack Saw

2
在它自己的线程中启动每个命令,这样任何与IO相关的阻止都不会浪费您的时间。
乔尔·科内特

Answers:


12

我刚刚整理的这个Perl脚本如何工作?

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

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

它已经运行了45分钟,没有一次跳过,并且我怀疑它将继续这样做,除非a)系统负载变得如此之高,以至于fork()花费了超过一秒钟的时间,或者b)插入了leap秒。

但是,由于存在一些开销,因此不能保证命令以精确的第二个间隔运行,但是我怀疑它比基于中断的解决方案差很多。

我用date +%N(纳秒,GNU扩展)运行了大约一个小时,并对其进行了一些统计。它的最大滞后时间是1155微秒。平均值(算术平均值)为216 µs,中值为219 µs,标准偏差为42 µs。95%的时间运行速度超过270 µs。除了C程序,我认为您无法击败它。


1
我在没有其他活动用户应用程序的情况下以1秒的间隔运行了它一整夜,并且运行了29241秒,没有跳过任何一秒钟!这符合我的目的。然后今天早上我又跑它有0.1秒的间隔,GNU date+%N只有3分钟后,扔的错误:Time::HiRes::sleep(-0.00615549): negative time not invented yet at ~/bin/repeat.pl line 23.第23行:在我保存的脚本:sleep $start - time();
porg

如果以0.01秒或0.001秒的间隔运行它,则仅几秒钟或更短的时间,直到程序因“负时间”错误而中止。但就我的目的而言,它很合适!
porg 2012年

28

POSIX ualarm()函数使您可以调度内核以微秒的精度定期向您的进程发出信号。

编写一个简单的程序:

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

编译

 gcc -O2 tick.c -o tick

然后将其附加到您需要定期执行的操作,如下所示:

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

我是否需要特殊的外壳或C-std?我已经对其进行了编译(这会在缺少回报时给出一个小警告),但是没有产生任何输出。
数学

@math使用-std=c99,您将不会收到有关缺少收益的警告。否则,您不需要任何特别的东西。您是否输错了额外的零?strace ./tick将从系统调用的角度向您展示它的功能
Dave

我得到:gcc -O2 -std = c99 -o tick tick.c tick.c:在函数'main'中:tick.c:10:5:警告:函数'ualarm'的隐式声明[-Wimplicit-function-declaration ] tick.c:在函数“ tick”中:tick.c:5:10:警告:忽略使用属性warn_unused_result声明的“ write”的返回值[-Wunused-result] ::似乎我的系统(Ubuntu 12.04)可以不支持它。但是,至少有一个手册页应将unarmist.h。禁用。(gcc为4.6.3)
数学

28

您是否尝试过watch使用参数--precise

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

从手册页:

通常,此间隔被解释为一次命令执行完成与下一次命令开始执行之间的时间间隔。但是,使用-p或--precise选项,可以使监视尝试每间隔秒运行一次命令。与ntptime一起尝试,请注意小数秒如何(几乎)保持不变,而不是普通模式(秒不断增加)。

但是,该参数可能在您的系统上不可用。

您还应该考虑当程序执行时间超过一秒时会发生什么。应该跳过下一个计划执行,还是应该延迟运行?

更新:我运行了一段时间的脚本,它并没有松开一个步骤:

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

更新:--precise标志是Debian的附加功能,但是补丁相当简单:http : //patch-tracker.debian.org/patch/series/view/procps/1 : 3.2.8-9squeeze1/watch_precision_time.patch


确切的方法。希望我能+10。
krlmlr 2012年

哪个版本watch支持该选项?我没有在任何机器上检查过。
tylerl 2012年

其版本为0.3.0,这是Ubuntu 12.04的当前版本。它来自procps软件包的3.2.8-11ubuntu6版本。
丹尼尔·库尔曼2012年

嗯,procps源码包不支持--precise。这是Debian新增功能(3.2.8-9,watch_precision_time.patch)
daniel kullmann 2012年

1
好的,但是与mdpc在有关问题的评论中所述:当系统负载沉重时,这也可能会失败。我只是结合压力(在磁盘和内核上施加负载)进行了测试,并得到了这样的信息: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 实时信息(内核等)在那里是有原因的!
数学

18

crontab分辨率为1分钟。如果您对每分钟的累积滞后时间没问题,然后在下一分钟重置,则可以使用以下基本思路:

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

请注意,该script.sh操作也在后台运行。这应该有助于最大程度地减少循环的每次迭代所累积的滞后。

但是,根据sleep产生的滞后时间,秒59可能与下一分钟的秒0重叠。

编辑,以与问题相同的格式折腾一些结果:

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

1小时52分钟= 6720秒

$ wc -l timelog-cron
6720 timelog-cron

0个计时问题,0%off。任何时间的累积都会每分钟重置一次。


1
请问为什么这被否决了?
伊兹卡塔

2
这是一个丑陋的
骇客

2
@hhaamu这有什么丑陋的?PC上的通用OS并非为非常精确的时序关键型操作而设计,那么您还能期望什么呢?如果您想要“优雅”且绝对精确的时序,则必须使用其他CPU调度程序,或者切换到实时内核,或者使用专用硬件,等等。这是完全合法的解决方案,我认为没有任何理由下注。这肯定是对仅在后台运行而不通过cron进行定期重新同步的一种改进。
2012年

1
再加上停止很容易。无需冒险在周期的中间将其杀死-从crontab中删除条目,并在分钟结束时自行完成。
Izkata 2012年

您只是很幸运,您的系统cron上精确到秒,但通常情况并非如此
德米特里·格里戈里耶夫

15

您的问题是,在运行程序后,您要睡一段固定的时间,而没有考虑自上次睡眠以来经过的时间。

您可以使用bash或任何其他编程语言来执行此操作,但是关键是使用时钟来确定安排下一次睡眠的时间。睡觉之前,请检查时钟,看看还剩多少时间,然后睡一会儿。

由于进程调度的折衷,不能保证您一定会在时钟滴答声中立即唤醒,但是您应该相当接近(在卸载几毫秒之内,或在加载几百毫秒之内)。而且您不会随时间累积错误,因为每次您在每个睡眠周期重新同步并消除任何累积的错误时,您就不会累积错误。

如果您需要精确地计时,那么您需要的是实时操作系统,它是专门为此目的而设计的。


我认为porg程序在运行预期的进程时也测试了程序块很有可能-从逻辑上讲,它们应该这样做,以避免杀死正在运行的计算机。
symcbean 2012年

无论您是否阻止,该机制都可以正常工作。如果您阻止了,则您会睡在阻止之后剩下的时间。如果您不阻塞,则您的计时线程或进程正在睡眠,而其他线程正在运行。无论哪种方式,结果都是一样的。
tylerl 2012年

@tylerl:您的解决方案的具体命令行看起来如何?
porg 2012年

我猜你的意思和@lynxlynxlynx
porg

@porg您需要使用date +%S.%N以亚秒精度来获取秒数,并usleep以亚秒精度来进行睡眠,但是之后这只是数学问题。
tylerl 2012年

7

我一直只是放弃让某些内容精确地按间隔运行。我认为您必须编写一个C程序,并要非常小心地注意不要用自己的代码超过1秒间隔的一部分。您可能必须使用线程或多个相互通信的流程才能使其正常工作。注意避免线程启动或进程启动的时间开销。

似乎与1993年相关的参考文献:用于CPU使用率估计和代码性能分析的随机采样时钟 您需要查看附录“专家源代码”,以了解它们如何精确地测量时间间隔并“唤醒”。他们的程序在正确的时间。由于该代码已有19年的历史,因此可能无法直接或轻松移植,但是如果您阅读并尝试理解它,则所涉及的原则可能会指导您的代码。

编辑:找到了另一个可能有用的参考:时钟分辨率对交互式和软实时过程的安排的影响, 这对任何理论背景都应该有帮助。



3

尝试在后台运行命令,这样不会对循环时序产生太大影响,但是如果您长时间不希望任何累加,那么即使这样做也不够,因为肯定会花费几毫秒的时间。

因此,这可能会更好,但可能还不够好:

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

在我的计算机上,这在20分钟内产生2个错误,或者每分钟0.1个错误,与运行相比,降低了大约五倍。


问题sleep 1在于它保证至少睡一秒钟-永远不会少于一秒钟。因此,误差累积。
hhaamu'7

除非您已在系统上运行原始代码并获得与OP相同的结果,否则比较两台不同计算机的计时结果就毫无意义。
德米特里·格里戈里耶夫

1

丑陋但行得通。如果您需要这样的循环,则可能应该重新考虑程序的设计。它基本上检查当前的秒数是否等于先前检查的秒数,并打印自更改秒数以来的纳秒数。精度受睡眠时间.001的影响。

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

准确度以毫秒为单位,前提是“有效负载”所date "+%N nanoseconds late"花费的时间不超过一秒钟。您可以通过增加睡眠时间来降低CPU负载,或者如果您真的不介意,只需将睡眠命令替换为即可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

这是一种不好的做法,因为您基本上会对事件进行CPU轮询并且浪费CPU周期。您可能想要附加到计时器中断(无法通过bash进行附加)或使用专用硬件(例如微控制器)。PC及其操作系统的设计不是为了实现高定时精度。


1

另一种方法是在循环中使用暂挂,然后从精确的外部程序发送SIGCONT。发送信号非常轻巧,与执行某些操作相比,延迟要短得多。您还可以使用“ at”命令预先排队一堆命令,几乎没有人再使用“ at”,我不确定它的精确度。

如果精度至关重要,并且您想认真对待这一点,这听起来像是通常在其中使用RTOS的一种应用程序,可以在带有RT-Preempt补丁内核的Linux下完成,这将为您提供精度和某种程度的衡量。中断控制,但可能比它值得的麻烦。

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

Xenomai也可能会有所帮助,它是完整的RTOS实施,并已移植到x86和x86_64,但其中涉及一些编程。

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


1

使用ksh93(具有浮点$SECONDS和内置sleep

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

相同的脚本也可以使用zsh,但是将调用系统的sleep命令。zsh具有zselect内置功能,但仅具有1/100分辨率。


0

我将使用一个小的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));
    }
}

该程序希望程序以完整路径作为第一个参数来调用,并传递所有剩余的参数。它不会等待命令完成,因此会很高兴地启动多个实例。

另外,这里的编码风格确实很草率,并且做出了一些假设,这些假设可能会或可能不会受到适用标准的保证,即该代码的质量“对我有用”。

通过NTP或通过手动设置来调整时钟时,该程序将获得更长或更短的间隔。如果程序应处理此问题,则POSIX将提供timer_create(CLOCK_MONOTONIC, ...)不受此影响的内容。


0

您应该跟踪当前时间并将其与开始时间进行比较。因此,您每次迭代都会消耗计算的时间量,而不是固定的时间量。这样,您就不会累积时序错误,也不会偏离原本的位置,因为您可以将每个循环的时序从开始就重置为绝对时间。

另外,如果发生中断,某些睡眠函数会提前返回,因此在这种情况下,您将不得不再次调用睡眠方法,直到经过了全部时间。



0

每秒可以运行至少100次,分辨率非常精确。

每分钟循环数的目录的存在创建了时间表。该版本支持微秒分辨率,前提是您的计算机可以处理。每分钟的执行次数不必一定要被60整除,也不必限于60。我已经测试了6000次,并且可以正常工作。

该版本可以安装在/etc/init.d目录中并作为服务运行。

#! /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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.