【问题标题】:Best way to update a clock every second in Linux在 Linux 中每秒更新时钟的最佳方法
【发布时间】:2021-12-01 23:59:50
【问题描述】:

当我们谈论在 Linux 中每秒更新一个时钟时,我认为类似于以下代码的内容会浮现在脑海中。

while :; do date +%T; sleep 1; done

这段代码一直困扰着我,因为有一个无限循环每秒运行两个命令,这意味着上下文切换会产生处理器使用率的轻微峰值。

考虑到这一点,我想知道:这真的是最好的方法吗?有没有更聪明的方法呢?例如,如果我想用像 C 这样的低级语言来重现这个,唯一的方法是用printf 显示时钟和一秒sleep 的无限循环吗?也就是说,有没有办法避免这种上下文切换并以更智能的方式使用 CPU?

【问题讨论】:

  • 上下文切换是程序在无事可做时不浪费CPU周期的典型方式。您能否尝试描述一下您想象的“以更智能的方式使用 CPU”会是什么样子?
  • 您可以在 C 程序中实现这一点,或者使用 Perl 或 Python 等其他脚本语言来避免每秒启动一个 date 进程。 “处理器使用率略有飙升”有什么问题?
  • 首先,您绝对不应该尝试在 shell 脚本中推理上下文切换。那些发生在低得多的水平上。其次,您的系统每秒已经执行了数千次上下文切换。再多几个就不会明显移动针了。
  • 老实说,我不知道“以更智能的方式使用 CPU”会是什么样子。我只是好奇在这种情况下是否有办法做到这一点。
  • 问题是 也不知道“以更智能的方式使用 CPU”是什么。但这并不意味着没有这样的方法,它只是意味着我不明白你在想象什么。您认为 CPU 在每秒数百万个周期中应该做什么,这不是显示更新所必需的,并且不涉及切换到其他进程?

标签: c linux shell operating-system low-level


【解决方案1】:

你不想完全避免上下文切换,你想让内核在它不运行的 99% 秒内运行其他东西 /usr/bin/date 将时间格式化为字符串,write(2) 将它标准输出。 (或者让这个 CPU 内核进入睡眠状态,以节省电力。但这实际上不算作上下文切换,因为软件从未更改页表或保存/恢复 FP 寄存器。即使是系统调用也完全进入内核保存/恢复整数寄存器,并且在没有硬件修复的 Intel CPU 上启用软件 Meltdown 缓解实际上会更改页表。而 Spectre 缓解清除分支预测历史记录的成本更高。)

(如果您没有在 Linux 文本控制台上运行它,例如 ctrl+alt+F2,则需要对终端仿真器或 sshd 或控制伪终端主端的任何东西进行上下文切换。只有在后一种情况下才会在date 进行的write(0, buf, len) 系统调用中实际发生写入视频RAM,即在该进程的上下文中。)

如果您想最小化 上下文切换(以及一般的系统调用),您需要在单个进程中进行休眠和写入。但这在 bash 中是不可能的;它没有内置的睡眠功能。 (Bash 确实printf '%(%T)T\n' $EPOCHSECONDS 来打印当前时间,但是忙着等待那会很糟糕)。您可能想用 C 语言编写一个程序,只进行睡眠和时间打印。

使用固定 1 秒延迟的循环将累积错误,因为它直到 date 启动并退出之后才开始下一秒,并且 shell 已分叉/执行 /usr/bin/sleep 下一次迭代(加上启动sleep 可执行文件中的开销)。


无需编写自己的 C 程序,您可以使用 watch -p -t --exec 将其降低到每秒只有一个 fork/exec(以及一堆其他系统调用) >,它以一定的时间间隔运行给定的命令,直接使用 fork/exec 而不是/bin/sh -c

  • -t 告诉它不要打印标题(包括时间)
  • -p(精确)让它用clock_gettime查询当前时间,并用nanosleep避免错误累积,每次瞄准相同的目标时间在一秒内。 (默认设置是在命令运行之间的固定时间间隔内休眠,无论花费多长时间。)

我们可以跟踪它的系统调用,看看它做了什么。 (我使用了较短的睡眠间隔,所以我不必让它坐那么久。)请注意clock_gettime doesn't show up in strace,因为它进入内核; glibc 包装器调用 vDSO 实现。内核导出的代码(映射到每个用户空间进程)读取内核导出的数据:内核定时器中断更新的粗略时间,以及rdtsc 的比例因子/偏移量,用于插入当前粗略的偏移量时间,因为现代 x86-64 系统有a precise constant-frequency counter accessible from user-space

(watch 实际上打印在“备用”屏幕上,因此输出在退出时会从您的终端中消失;该部分输出是为了举例目的而伪造的。其余部分是从终端仿真器复制/粘贴的,添加了## cmets。)

  # use strace -f ...  to trace into child processes, and see all the syscalls from date
$ strace -o foo.tr    watch -p -t -n 0.5 --exec   date +%T
22:31:54
control-C

$ less foo.tr
... startup stuff from watch, including some terminal-size ioctl

pipe([3, 4])                            = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f0f744fba10) = 3832377
   # Linux implements fork() in terms of clone(2)
close(4)                                = 0
fcntl(3, F_GETFL)                       = 0 (flags O_RDONLY)
newfstatat(3, "", {st_mode=S_IFIFO|0600, st_size=0, ...}, AT_EMPTY_PATH) = 0
   # (IDK why it's doing an fstat on the pipe FD)
read(3, "22:16:45\n", 4096)             = 9
read(3, "", 4096)                       = 0
   # reads from the pipe until EOF
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=3832377, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
close(3)                                = 0
   # then closes it
wait4(3832377, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 3832377
rt_sigaction(SIGTSTP, {sa_handler=SIG_IGN, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7f0f7453ada0}, {sa_handler=0x7f0
f746f4790, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7f0f7453ada0}, 8) = 0
   # and waits for the child PID
write(1, "\33[?1049h\33[22;0;0t\33[1;42r\33(B\33[m\33["..., 46) = 46
   # clears the screen and moves cursor to the top left
write(1, "22:16:45\33[42;134H", 17)     = 17
   # and copies what it read from the pipe earlier.
rt_sigaction(SIGTSTP, {sa_handler=0x7f0f746f4790, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7f0f7453ada0}, NULL, 8) =
 0

## There's a clock_gettime() somewhere, probably here,
##  but the vDSO implementation avoids entering the kernel so strace doesn't see it.
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=0, tv_nsec=498451000}, NULL) = 0
  # After calculating the exact time until the next event
  # tell the kernel we're done until then

  # Then the cycle starts over again when it wakes
pipe([3, 4])                            = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f0f744fba10) = 3832378
close(4)                                = 0
fcntl(3, F_GETFL)                       = 0 (flags O_RDONLY)
...

watch 没有-t 将打印当前时间作为其标题的一部分。所以如果这就是你想要的,你就不再需要date了。

但它没有运行任何程序的选项。它每次都会统计/etc/localtime,以防当前时区发生变化。

您可以使用/bin/true,但这仍然需要分叉/执行并运行其动态链接器启动开销。或者你可以使用watch --exec /non-existant 并让它每次都打印一个execve 错误。但即便如此,它仍然会在尝试执行之前分叉,创建一个新的 PID 并对其进行上下文切换。

【讨论】:

  • 感谢您如此详尽的回答,我通过阅读确实学到了很多东西!我特别喜欢你甚至提到了 Spectre 和 Meltdown 缓解措施,我也非常喜欢评论中的 strace
  • @pvpscript:很高兴你发现这些部分很有用!是的,Spectre+Meltdown 缓解使一个微不足道的系统调用 maybe something like 10x 与在 Skylake CPU 上往返内核模式和返回(对于返回 -ENOSYS 的错误调用号)一样昂贵,因此它与关心有关每个系统调用的成本,而不仅仅是进程启动。加上耗尽无序 exec 后端的成本,以及内核代码污染这些缓存后额外的缓存/TLB/分支未命中。
【解决方案2】:

我怀疑有办法避免上下文切换——或者如果有办法,它会比涉及sleep 的技术更浪费。

你所体现的技术的真正问题

while :; do date +%T; sleep 1; done

是他们失去了时间。例如,如果我运行此修改,合并我自己的 dateexpr 程序,该程序除其他外还具有亚秒级处理能力:

while :; do dateexpr +%H:%M:%.2S now; sleep 1; done

,这是我看到的:

10:13:48.40
10:13:49.41
10:13:50.43
10:13:51.44
10:13:52.46
10:13:53.47
10:13:54.49
10:13:55.50

所以看起来“上下文切换”——启动每个 sleepdatedateexpr 进程的开销——需要 10-20 毫秒。

我已经编写了一个程序(用 C 语言)来解决这个问题。它持续监视时间,并计算一个略小于一秒的值来休眠,以便它可以每秒精确地调用子命令一次,在第二个。它看起来像这样:

$ synchro dateexpr +%H:%M:%.2S now
10:17:11.01
10:17:12.01
10:17:13.01
10:17:14.01
10:17:15.01
10:17:16.01
10:17:17.01

启动被调用的进程还有那个10ms的错误,但至少没有累积。

但是为了完成它的工作,我的synchro 程序不得不进行更多的系统调用,所以实际上有更多的上下文切换,而不是更少。

但是,当然,一般来说,当您想暂停一段时间时,调用类似sleep 的方法是正确的做法,因为您明确放弃了控制权,并且操作系统知道它不必安排您的进程完全运行,因此您在睡眠时对系统的其余部分施加的负载最小。是的,其中涉及到几个上下文切换,但它们似乎很少,需要付出很小的代价,而且正如我所说,我认为您无法绕过它们。

我想知道是否有一种方法可以完全在用户空间中运行时钟或计时器,也许这也是您要问的。但我怀疑是否有办法,因为在用户空间中没有任何东西 [脚注 1] 可以为您提供任何有关时间或时钟的信息——这些信息都在内核中,这意味着它将占用一个系统打电话去。

(当然,在这里我只考虑在传统的多任务操作系统下运行的进程。如果您正在为带有 RTC 的微处理器编写嵌入式代码,那么毫无疑问,您可以完全按照您的意愿行事,而无需上下文切换。)

有一种渺茫的可能性,即在至少某些(也许是现在大多数?)版本的 Linux 下,有一种称为 vDSO 的机制,它可以在用户空间中执行某些系统调用,而无需上下文切换。接受这种特殊处理的系统调用的首要候选人是gettimeofday 和相关的。因此,在使用 vDSO 的系统上,您可以编写一个带有忙等待循环的程序,重复调用 gettimeofday(或 timeclock_gettime,如果它们也使用 vDSO)直到所需时间到达,因为vDSO,您将在没有上下文切换的情况下执行此操作。但当然,忙碌等待是一个几乎无法挽回的可怕想法,所以我并不认真推荐这个。 (这就是我在这个答案开头的意思,当时我说“如果有办法,那将比涉及sleep 的技术更浪费。”)


脚注 1. 我说过“在用户空间中没有任何东西可以为您提供任何有关时间或时钟的信息”,但这并不完全正确。正如 Peter Cordes 的 cmets 提醒我们的那样,英特尔处理器至少会给我们“Time Stamp Counter”和rdtsc 指令来读取它。这是一个潜在的关键——但也有很大的问题! — 用于编写某些高精度计时应用程序的工具,但我从未使用过它,因此我不会尝试解释它或它的注意事项。

【讨论】:

  • 假设bash 的最新版本,printf "%(%T)T\n' 至少可以让您摆脱对date 的调用。
  • 非常感谢您,这是一个非常有见地的回答!我还查看了您的项目,但找不到synchro(顺便说一句,它们都是非常巧妙的想法)。您介意分享它吗,或者至少谈谈您是如何实现它的?
  • @pvpscript 我很高兴发布synchro 的代码。如果我忘记了,请随时 ping 或给我发电子邮件。
  • 是的,clock_gettime() 是现代 i386/x86-64 Linux 上的纯用户空间。它的 glibc 包装器知道它在 vDSO 中可用,并且在正常系统上,vDSO 代码读取由内核的计时器中断更新的全局变量,并且(在现代 x86-64 上)使用 rdtsc 和一些比例因子也来自vDSO 对粗略系统时间进行小幅修正。在没有从用户空间访问的高精度时序的系统上(例如非常量 TSC,就像在 Core 2 之前一样),我想,clock_gettime 的 vDSO 条目将包含使用 syscall 的代码。
  • 如果您知道 CPU 上的 TSC 频率,您甚至可以在没有 vDSO 内核交互的情况下自旋等待截止日期。 (仅适用于等待小于一微秒,可能更短,并且仅当您需要非常准确地达到唤醒时间时,因为是的,这是 spinning,而不是睡眠)。 How to calculate time for an asm delay loop on x86 linux? 有一个例子。未来的 CPU 将有一个tpause 指令以在执行此操作时节省一些功率,但当然仍然只对等待太短而无法让内核上下文切换到另一个任务并返回是明智的
猜你喜欢
  • 1970-01-01
  • 2011-07-24
  • 1970-01-01
  • 1970-01-01
  • 2010-10-02
  • 2020-05-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多