你不想完全避免上下文切换,你想让内核在它不运行的 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 并对其进行上下文切换。