c - 如何以零纳秒实现每秒的计时器释放?

标签 c linux

我注意到io_uring内核侧在CLOCK_MONOTONIC上使用CLOCK_MONOTONIC,因此对于第一个计时器,我同时获得了CLOCK_REALTIME和CLOCK_MONOTONIC的时间,并像下面那样调整了纳秒,并对io_uring_prep_timeout使用IORING_TIMEOUT_ABS标志。 iorn/clock.c at master · hnakamur/iorn

const long sec_in_nsec = 1000000000;

static int queue_timeout(iorn_queue_t *queue) {
    iorn_timeout_op_t *op = calloc(1, sizeof(*op));
    if (op == NULL) {
        return -ENOMEM;
    }

    struct timespec rts;
    int ret = clock_gettime(CLOCK_REALTIME, &rts);
    if (ret < 0) {
        fprintf(stderr, "clock_gettime CLOCK_REALTIME error: %s\n", strerror(errno));
        return -errno;
    }
    long nsec_diff = sec_in_nsec - rts.tv_nsec;

    ret = clock_gettime(CLOCK_MONOTONIC, &op->ts);
    if (ret < 0) {
        fprintf(stderr, "clock_gettime CLOCK_MONOTONIC error: %s\n", strerror(errno));
        return -errno;
    }

    op->handler = on_timeout;
    op->ts.tv_sec++;
    op->ts.tv_nsec += nsec_diff;
    if (op->ts.tv_nsec > sec_in_nsec) {
        op->ts.tv_sec++;
        op->ts.tv_nsec -= sec_in_nsec;
    }
    op->count = 1;
    op->flags = IORING_TIMEOUT_ABS;

    ret = iorn_prep_timeout(queue, op);
    if (ret < 0) {
        return ret;
    }

    return iorn_submit(queue); 
}

从第二次开始,我仅增加第二部分tv_sec,并将IORING_TIMEOUT_ABS标志用于io_uring_prep_timeout。

这是我的示例程序的输出。毫秒部分为零,但比第二秒晚约400微秒。
on_timeout time=2020-05-10T14:49:42.000442
on_timeout time=2020-05-10T14:49:43.000371
on_timeout time=2020-05-10T14:49:44.000368
on_timeout time=2020-05-10T14:49:45.000372
on_timeout time=2020-05-10T14:49:46.000372
on_timeout time=2020-05-10T14:49:47.000373
on_timeout time=2020-05-10T14:49:48.000373

你能告诉我一个比这更好的方法吗?

最佳答案

Thanks for your comments! I'd like to update the current time for logging like ngx_time_update(). I modified my example to use just CLOCK_REALTIME, but still about 400 microseconds late. github.com/hnakamur/iorn/commit/… Does it mean clock_gettime takes about 400 nanoseconds on my machine?



是的,听起来不错。但是,如果您使用的是Linux下的x86 PC,则clock_gettime开销的400 ns可能会有点高(高一个数量级,请参见下文)。如果您使用的是arm CPU(例如Raspberry Pi,nvidia Jetson),那可能没问题。

我不知道你怎么得到400微秒。但是,我必须在linux下做很多实时工作,而400 us与我所测量的开销类似,即在系统调用挂起后进行上下文切换和/或唤醒进程/线程的开销。

我不再使用gettimeofday了。我现在只使用clock_gettime(CLOCK_REALTIME,...),因为除了得到纳秒而不是微秒之外,其他都是相同的。

众所周知,尽管clock_gettime是系统调用,但如今在大多数系统上,它都使用VDSO层。内核将特殊代码注入(inject)到用户空间应用程序中,以便它能够直接访问时间,而无需syscall的开销。

如果您有兴趣,可以在gdb下运行并反汇编代码以查看其仅访问某些特殊的内存位置,而不是进行syscall。

我认为您不必为此担心太多。只需使用clock_gettime(CLOCK_MONOTONIC,...)并将flags设置为0。就ioring调用而言,因为iorn层正在使用它,因此开销并不计入此开销。

当我做这种事情,并且想要/需要计算clock_gettime本身的开销时,我会循环调用clock_gettime(例如1000次),并尝试将总时间保持在[possible]时间片以下。我在每次迭代中使用时间之间的最小差异。这补偿了任何[可能]的时间片。

最小是 call 本身的开销(平均)。

您可以执行其他技巧来最大程度地减少用户空间中的延迟(例如,提高进程优先级,限制CPU亲和力和I/O中断亲和力),但是它们可能涉及其他一些事情,如果您不太谨慎的话,它们可能会产生更糟的结果。

在开始采取特殊措施之前,您应该有一个可靠的方法来衡量时间/基准,以证明您的结果不能满足您的时间/吞吐量/等待时间要求。否则,您将做复杂的事情而没有实际/可衡量/必要的利益。

以下是我刚刚创建,简化的一些代码,但是基于我已经/将要用来校准开销的代码:
#include <stdio.h>
#include <time.h>

#define ITERMAX     10000

typedef long long tsc_t;

// tscget -- get time in nanoseconds
static inline tsc_t
tscget(void)
{
    struct timespec ts;
    tsc_t tsc;

    clock_gettime(CLOCK_MONOTONIC,&ts);

    tsc = ts.tv_sec;
    tsc *= 1000000000;
    tsc += ts.tv_nsec;

    return tsc;
}

// tscsec -- convert nanoseconds to fractional seconds
double
tscsec(tsc_t tsc)
{
    double sec;

    sec = tsc;
    sec /= 1e9;

    return sec;
}

tsc_t
calibrate(void)
{
    tsc_t tscbeg;
    tsc_t tscold;
    tsc_t tscnow;
    tsc_t tscdif;
    tsc_t tscmin;
    int iter;

    tscmin = 1LL << 62;
    tscbeg = tscget();
    tscold = tscbeg;

    for (iter = ITERMAX;  iter > 0;  --iter) {
        tscnow = tscget();

        tscdif = tscnow - tscold;
        if (tscdif < tscmin)
            tscmin = tscdif;

        tscold = tscnow;
    }

    tscdif = tscnow - tscbeg;

    printf("MIN:%.9f TOT:%.9f AVG:%.9f\n",
        tscsec(tscmin),tscsec(tscdif),tscsec(tscnow - tscbeg) / ITERMAX);

    return tscmin;
}

int
main(void)
{

    calibrate();

    return 0;
}

在我的系统上,一个2.67GHz Core i7,输出为:
MIN:0.000000019 TOT:0.000254999 AVG:0.000000025

因此,我得到25 ns的开销[而不是400 ns]。但是,同样,每个系统都可能有所不同。

更新:

请注意,x86处理器具有“速度步进”。操作系统可以半自动调整CPU频率。较低的速度可以节省电量。更高的速度是最大的性能。

这是通过启发式方法完成的(例如,如果操作系统检测到该进程占用大量CPU用户,那么它将加快速度)。

为了达到最大速度,Linux具有以下目录:
/sys/devices/system/cpu/cpuN/cpufreq

其中N是CPU编号(例如0-7)

在此目录下,有许多感兴趣的文件。它们应该是不言自明的。

特别要注意scaling_governor。它具有ondemand [内核将根据需要调整]或performance [内核将强制最大CPU速度]。

要强制最大速度,请以root身份将此[once]设置为performance(例如):
echo "performance" > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor

对所有CPU执行此操作。

但是,我只是在系统上这样做,并且效果不大。因此,内核的启发式方法可能有所改进。

至于400us,当一个进程一直在等待某物时,当它“被唤醒”时,这是一个两步过程。

该过程标记为“可运行”。

在某些时候,系统/CPU会进行重新计划。该过程将根据调度策略和有效的过程优先级运行。

对于许多系统调用,[仅]重新调度在下一个系统计时器/时钟滴答/中断发生。因此,对于某些人来说,对于HZ值为1000的延迟可能会长达一个完整的时钟滴答声(即),之后可能会长达1ms(1000 us)。

平均而言,这是HZ的一半或500 us。

对于某些系统调用,当进程标记为可运行时,将立即进行重新计划。如果该进程具有更高的优先级,它将立即运行。

当我第一次查看此内容时(大约在2004年),我查看了内核中的所有代码路径,并且唯一立即进行重新计划的syscall是SysV IPC,用于msgsnd/msgrcv。也就是说,当进程A执行msgsnd时,将运行等待给定消息的任何进程B。

但是,其他人则没有(例如futex)。他们将等待计时器滴答声。从那时起,发生了很多变化,现在,更多的系统调用将立即进行重新计划。例如,我最近测量了futex [通过pthread_mutex_*调用],它似乎可以快速重新计划。

另外,内核调度程序已更改。较新的调度程序可以在一个时钟滴答声中唤醒/运行某些内容。

因此,对您来说,400 us是[可能]对准下一个时钟滴答。

但是,这可能只是进行系统调用的开销。为了进行测试,我修改了测试程序以打开/dev/null [和/或/dev/zero],并将read(fd,buf,1)添加到测试循环中。

我的MIN:值为529。因此,您获得的延迟可能只是执行任务切换所花费的时间。

这就是我所说的“目前足够好”。

要获得“ Razor 的边缘”响应,您可能必须编写一个自定义内核驱动程序,并由该驱动程序执行此操作。如果嵌入式系统必须在每个间隔上切换GPIO引脚,这就是嵌入式系统将要做的事情。

但是,如果您只是在做printf,那么printf和底层write(1,...)的开销往往会淹没实际的延迟。

另外,请注意,当您执行printf时,它将构建输出缓冲区,而当FILE *stdout中的缓冲区已满时,它将通过write刷新。

为了获得最佳性能,最好执行int len = sprintf(buf,"current time is ..."); write(1,buf,len);
同样,当您执行此操作时,如果TTY I/O的内核缓冲区已满(考虑到您正在处理的消息的频率很高,这很有可能),该过程将被挂起,直到将I/O发送到TTY设备。

要做到这一点,您必须注意有多少可用空间,如果没有足够的空间来容纳它们,则跳过一些消息。

您需要执行以下操作:ioctl(1,TIOCOUTQ,...)以获取可用空间,如果小于您要输出的消息的大小(例如,上面的len值),则跳过一些消息。

对于您的用法,您可能对最新的时间消息更感兴趣,而不是输出所有消息(最终会产生延迟)

关于c - 如何以零纳秒实现每秒的计时器释放?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61714203/

相关文章:

c - 使用 BaseCode 加密的段错误

c - 如果我在 C 中省略了 main 函数的返回类型怎么办?

linux - linux shell 中的无效别名

C++查找USB设备挂载点

linux - 在脚本中运行 gdb 时自动退出

c - 为什么我的动态字符串数组会泄漏?

c - 如何通过MPI加速这个问题

c - 在 C 中使用通配符枚举目录中的文件

c - 来自一个父进程的多个子进程

c - 环回接口(interface)是否在网卡/硬件上产生中断