我注意到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 justCLOCK_REALTIME
, but still about 400 microseconds late. github.com/hnakamur/iorn/commit/… Does it meanclock_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/