【问题标题】:Linux, timerfd accuracyLinux,timerfd 精度
【发布时间】:2010-06-26 17:38:28
【问题描述】:

我的系统需要至少 10 毫秒的计时器精度。
我选择了 timerfd,因为它非常适合我,但发现即使在长达 15 毫秒的时间内它也完全不准确,或者我不明白它是如何工作的。

在 10 毫秒计时器上,我测量的时间最长为 21 毫秒。
我整理了一个快速测试来显示我的问题。
这里是一个测试:

#include <sys/timerfd.h>
#include <time.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <inttypes.h>

int main(int argc, char *argv[]){

    int timerfd = timerfd_create(CLOCK_MONOTONIC,0);
    int milliseconds = atoi(argv[1]);
    struct itimerspec timspec;
    bzero(&timspec, sizeof(timspec));
    timspec.it_interval.tv_sec = 0;
    timspec.it_interval.tv_nsec = milliseconds * 1000000;
    timspec.it_value.tv_sec = 0;
    timspec.it_value.tv_nsec = 1;

    int res = timerfd_settime(timerfd, 0, &timspec, 0);
    if(res < 0){
       perror("timerfd_settime:");
       return 1;
    }
    uint64_t expirations = 0;
    int iterations = 0;
    while( res = read(timerfd, &expirations, sizeof(expirations))){
        if(res < 0){ perror("read:"); continue; }
        if(expirations > 1){
            printf("%" PRIu64 " expirations, %d iterations\n", expirations, iterations);
            break;
        }
        iterations++;
    }
    return 0;
}

并像这样执行:

Zack ~$ for i in 2 4 8 10 15; do echo "intervals of $i milliseconds"; ./test $i;done
intervals of 2 milliseconds
2 expirations, 1 iterations
intervals of 4 milliseconds
2 expirations, 6381 iterations
intervals of 8 milliseconds
2 expirations, 21764 iterations
intervals of 10 milliseconds
2 expirations, 1089 iterations
intervals of 15 milliseconds
2 expirations, 3085 iterations

即使假设有一些可能的延迟,15 毫秒的延迟对我来说听起来也太过分了。

【问题讨论】:

  • 我关于该地区的数据相当生疏。听起来你想实时做一些事情。检查实时 Linux。否则,调度程序使用更长的粒度来避免更高的开销。您可能希望将您的进程作为 RT(实时)进程 (man sched_setparam) 并且可能在 root 下运行。对于正常流程(例如游戏或多媒体),您会(1)在紧密循环中等待或(2)在每个计时器事件上计算错误(预期与实际唤醒时间)并在推进内部时间时将其计入帐户 -依赖状态。
  • 感谢您的提示,但我不认为 20 毫秒间隔是实时域的一部分。我的意思是,一个被要求等待 10 毫秒的标准系统永远不应该等待 21 毫秒,RT 听起来太多了。
  • 您可以尝试将您的程序设置为以实时优先级运行,您可能不需要使用 RT Linux 内核,这些天主线在实时方面非常好,并且考虑到它从未错过超过仅以 RT 优先级运行的一次到期应该足以让您永远不会错过一个
  • 另外你应该考虑天气与否你绝对不能容忍错过唤醒,如果你有时会错过,不要担心它会发生,但这不是世界末日,你会得到其中的大部分。我正在我的系统上运行它,它还没有错过 4ms 循环上的计时器,而且我已经运行了一段时间......
  • @Arkaitz Jimenez:实时是指有严格的要求,而不是任何实际的准确度下限。影响计时器交付及时性的因素有很多,包括您使用的 Linux(内核)版本、编译时使用的配置、您使用的硬件以及系统上运行的其他内容。

标签: c linux timer


【解决方案1】:

尝试如下改变它,这应该可以保证它永远不会错过唤醒,但要小心它,因为运行实时优先级可能会在机器不睡眠时硬锁定你的机器,你也可能需要设置让您的用户能够以实时优先级运行内容(请参阅/etc/security/limits.conf

#include <sys/timerfd.h>
#include <time.h>
#include <string.h>
#include <stdint.h>
#include <stdio.h>
#include <sched.h>

int main(int argc, char *argv[]) 
{
    int timerfd = timerfd_create(CLOCK_MONOTONIC,0);
    int milliseconds = atoi(argv[1]);
    struct itimerspec timspec;
    struct sched_param schedparm;

    memset(&schedparm, 0, sizeof(schedparm));
    schedparm.sched_priority = 1; // lowest rt priority
    sched_setscheduler(0, SCHED_FIFO, &schedparm);

    bzero(&timspec, sizeof(timspec));
    timspec.it_interval.tv_sec = 0;
    timspec.it_interval.tv_nsec = milliseconds * 1000000;
    timspec.it_value.tv_sec = 0;
    timspec.it_value.tv_nsec = 1;

    int res = timerfd_settime(timerfd, 0, &timspec, 0);
    if(res < 0){
       perror("timerfd_settime:");
    }
    uint64_t expirations = 0;
    int iterations = 0;
    while( res = read(timerfd, &expirations, sizeof(expirations))){
        if(res < 0){ perror("read:"); continue; }
        if(expirations > 1){
            printf("%ld expirations, %d iterations\n", expirations, iterations);
            break;
        }
        iterations++;
    }
}

如果您使用线程,则应使用pthread_setschedparam 而不是sched_setscheduler

实时也不是关于低延迟,而是关于保证,RT 意味着如果你想每秒准确地唤醒一次,你会的,正常的调度不会给你这个,它可能会决定唤醒100 毫秒后你就起来了,因为那时它还有其他工作要做。如果您想每 10 毫秒唤醒一次并且您确实需要,那么您应该将自己设置为作为实时任务运行,然后内核将每 10 毫秒唤醒您一次,而不会失败。除非更高优先级的实时任务正忙着做事。

如果您需要确保唤醒间隔恰好是某个时间(无论是 1 毫秒还是 1 秒),除非您作为实时任务运行,否则您将无法获得它。内核会为您这样做是有充分理由的(其中之一是省电,另一个是更高的吞吐量,还有其他),但是这样做完全在它的权利范围内,因为您从未告诉过它您需要更好的保证。大多数东西实际上并不需要如此准确,或者永远不要错过,所以你应该认真考虑你是否真的需要它。

引用http://www.ganssle.com/articles/realtime.htm

硬实时任务或系统是 一项活动必须是 完成 - 总是 - 由指定的 最后期限。截止日期可能是 特定时间或时间间隔,或 可能是某个事件的到来。难的 根据定义,实时任务失败, 如果他们错过了这样的最后期限。

注意这个定义没有 关于频率的假设或 任务期间。一微秒或 一周 - 如果错过最后期限 导致失败,则任务有 严格的实时要求。

软实时几乎相同,除了错过最后期限虽然不可取,但并不是世界末日(例如,视频和音频播放是软实时任务,您不想错过显示帧,或者缓冲区用完,但如果你这样做只是暂时的打嗝,你只需继续)。如果您尝试做的是“软”实时,我不会打扰以实时优先级运行,因为您通常应该及时唤醒(或至少接近它)。

编辑:

如果您没有实时运行,默认情况下,内核会为您设置一些“松弛”的任何计时器提供时间,以便它可以将您的唤醒请求与在您请求的时间接近的时间发生的其他事件合并(即如果其他事件在您的“空闲”时间内,它不会在您要求的时间唤醒您,而是早一点或晚一点,同时它已经准备做其他事情,这样可以节省电力)。

有关更多信息,请参阅 High- (but not too high-) resolution timeoutsTimer slack(请注意,我不确定这些内容是否正是内核中的内容,因为这两篇文章都是关于 lkml 邮件列表讨论的,但类似于第一个确实在内核中。

【讨论】:

  • @maxschlepzig 是的,答案就是这样。
  • 好的,我跳过了那部分。但是,我将留下timerslack link 作为参考,以便人们可以检查内核中的内容。 FWIW,为了测试实时调度的含义,您实际上不必修改示例。用例如包装电话chrt -r 1 ./timerfd 2 具有相同的效果。顺便说一句,关于你关于保证的陈述——它们有点误导。我的意思是,普通 Linux 中的实时保证非常软 - 即使在实时优先级下运行任务时也是如此。
【解决方案2】:

我感觉您的测试非常依赖于硬件。当我在我的系统上运行您的示例程序时,它似乎在 1 毫秒时挂起。为了使您的测试在我的计算机上完全有意义,我不得不从毫秒更改为微秒。 (我将乘数从 1_000_000 更改为 1_000。)

$ grep 1000 测试.c timspec.it_interval.tv_nsec = 微秒 * 1000; $ for i in 1 2 4 5 7 8 9 15 16 17\ 31 32 33 47 48 49 63 64 65;做\ echo "$i 微秒的间隔";\ ./test $i;完成 间隔 1 微秒 11 次到期,0 次迭代 2 微秒的间隔 5 次到期,0 次迭代 间隔 4 微秒 3 次到期,0 次迭代 间隔 5 微秒 2 次到期,0 次迭代 间隔 7 微秒 2 次到期,0 次迭代 间隔 8 微秒 2 次到期,0 次迭代 间隔 9 微秒 2 次到期,0 次迭代 间隔 15 微秒 2 次到期,7788 次迭代 16 微秒的间隔 4 次过期,1646767 次迭代 间隔 17 微秒 2 次到期,597 次迭代 31 微秒的间隔 2 次到期,370969 次迭代 32 微秒的间隔 2 次过期,163167 次迭代 33 微秒的间隔 2 次到期,3267 次迭代 间隔 47 微秒 2 次过期,1913584 次迭代 间隔 48 微秒 2 次到期,31 次迭代 间隔 49 微秒 2 次到期,17852 次迭代 间隔 63 微秒 2 次到期,24 次迭代 64 微秒的间隔 2 次到期,2888 次迭代 间隔 65 微秒 2 次过期,37668 次迭代

(有趣的是,我从 16 和 47 微秒获得了最长的运行时间,但 17 和 48 很糟糕。)

time(7) 对我们的平台为何如此不同提出了一些建议:

高分辨率计时器 Linux 2.6.21之前,定时器和睡眠系统的准确性 调用(见下文)也受到 jiffy 大小的限制。 从 Linux 2.6.21 开始,Linux 支持高分辨率计时器 (HRT),可选择通过 CONFIG_HIGH_RES_TIMERS 进行配置。在 支持 HRT、睡眠准确性和计时器的系统 系统调用不再受 jiffy 约束,而是 可以与硬件允许的一样准确(微秒精度 是现代硬件的典型代表)。您可以确定是否 通过检查分辨率支持高分辨率计时器 通过调用 clock_getres(2) 或查看 /proc/timer_list 中的“分辨率”条目。 并非所有硬件架构都支持 HRT。 (支持 在 x86、arm 和 powerpc 等平台上提供。)

/proc/timer_list 中的所有“分辨率”行在我(不可否认的强大)x86_64 系统上都是 1ns。

我决定尝试找出“断点”在我的计算机上的位置,但放弃了 110 微秒的运行:

$ for i in 70 80 90 100 110 120 130\ ;回显“$i 微秒的间隔”;\ ./test $i;完成 间隔 70 微秒 2 次到期,639236 次迭代 80 微秒的间隔 2 次过期,150304 次迭代 间隔 90 微秒 4 次过期,3368248 次迭代 100 微秒的间隔 4 次过期,1964857 次迭代 110 微秒的间隔 ^C

90 微秒运行了 300 万次迭代,然后失败了几次;这比您第一次测试的分辨率高出 22 倍,所以我想说,如果硬件合适,10 毫秒应该不难。 (90 微秒的分辨率是 10 毫秒的 111 倍。)

但是,如果您的硬件不提供用于高分辨率计时器的计时器,那么 Linux 在不求助于 SCHED_RR 或 SCHED_FIFO 的情况下将无法帮助您。即便如此,也许另一个内核可以更好地为您提供所需的软件计时器支持。

祝你好运。 :)

【讨论】:

  • 是的,内核有 hrtimers,所以它可以在你要求它时在 1ns 内唤醒你,天气与否是另一回事,定时器有一些“松弛”(如果你不是一个实时任务),以便内核可以减少总唤醒次数。 (即,如果它在闲置期间已经有事可做,它会让你在看到lwn.net/Articles/296578 的其他任何东西的同时醒来)
  • 如果示例程序出现挂起,则意味着您永远不会错过任何计时器到期。 IOW,除非错过计时器到期,否则示例程序是一个无限循环。错过计时器到期的概率与 CPU 内核的数量成反比,并随着系统负载的增加而增加。例如,在我的一个 8 核系统上,我可以通过并行启动 stress-ng --vm 8 来可靠地终止 ./timerfd 2
【解决方案3】:

这是一个理论。如果您的系统将HZ 设置为250(这是典型的),那么您的计时器分辨率为4 毫秒。一旦您的进程被调度程序换出,在您的进程获得另一个时间片之前,可能会调度并运行许多其他进程。这可以解释您看到 15 到 21 毫秒范围内的计时器分辨率。解决这个问题的唯一方法是运行实时内核。

在非实时系统上实现高分辨率定时的典型解决方案基本上是忙于等待调用 select。

【讨论】:

  • 我不认为许多面向桌面的发行版将 HZ 设置为 250,而且现在大多数都打开了无滴答操作,因此 HZ 对时间的影响应该不大。现在的大多数设置通常应该能够在没有实时内核的情况下轻松获得毫秒粒度,并且不需要 RT Linux 补丁,除非您的实时负载真的很紧...
  • 如今,Mainline Linux 是一个实时系统,只是没有多少人知道它:P(如果您编写代码以利用实时功能,则可以)
【解决方案4】:

根据系统正在执行的其他操作,切换回您的任务可能会有点慢。除非你有一个“真正的”实时系统,否则不能保证它会比你看到的更好,尽管我同意结果有点令人失望。

您可以(大部分)消除任务切换/调度程序时间。如果您有多余的 CPU 资源(和电力!),一个残酷但有效的解决方案是繁忙的等待自旋循环。

这个想法是在一个紧密的循环中运行你的程序,不断地轮询时钟的时间,然后在时间合适时调用你的其他代码。以使您的系统在其他所有方面都表现得非常缓慢和 CPU 升温为代价,您最终会得到几乎没有抖动的任务调度。

我曾经在 Windows XP 下编写过这样的系统来旋转步进电机,提供高达每秒 40K 次的均匀间隔脉冲,并且运行良好。当然,您的里程可能会有所不同。

【讨论】:

  • 你为什么不买一个像 PIC 或 Amtel 这样的便宜的微电脑来做跑腿工作?现在我会使用 Ardiuno 进行步进控制,并使用 USB 连接向它发送更快/更慢/启动/停止信号。
猜你喜欢
  • 2013-02-03
  • 2014-02-20
  • 1970-01-01
  • 2012-09-11
  • 2015-12-21
  • 2013-05-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多