我一直在使用我的 Raspberry Pi 进行一些数据收集,并注意到其中一些人做了一些看起来不太理想的事情。有一个叫做“rngd”的过程,它真的很喜欢经常醒来并宣布它正在做的事情。通常大约是每小时一次,它倾向于将十几行推送到系统日志,我都不关心这些。
因此,在其中一个上,为了不污染日志,也不用大量写入给我的生活增加零价值来烧毁 SD 卡,我决定用“-S0”将其关闭。根据手册:
控制将统计信息输出到 syslog 的时间间隔。通常每小时一次,最长为一周(604800 秒)。设置为 0 以禁用自动统计记录,例如避免嵌入式系统中的硬盘旋转(发送 SIGUSR1 信号仍将导致记录统计)。
我在二月份就这样做了,事实上,它停止了记录。我很高兴。我暂时离开了其他人。
好吧,几个小时前,我正在研究这些东西,并注意到那个上的 rngd 正在运行大约 15% 的 CPU,而它在其他上没有做任何事情。这种事情让我很恼火,所以我很执着于它,这就是我的奖励:
[pid 422] 23:43:16.512699 clock_nanosleep_time64(CLOCK_REALTIME, 0, {tv_sec=0, tv_nsec=0}, 0x7ee28c68) = 0 <0.000103>
[pid 422] 23:43:16.512969 clock_nanosleep_time64(CLOCK_REALTIME, 0, {tv_sec=0, tv_nsec=0}, 0x7ee28c68) = 0 <0.000101>
[...一遍又一遍地这样...]
总之,WTF,结束。为什么你一遍又一遍地呼唤睡眠……没有延迟?检查一下:tv_sec 是睡眠的秒数。 tv_nsec 是休眠的纳秒数。毫不奇怪,使用两者都设置为 0 的结构调用它会立即返回。
为什么要这么做?那你为什么还要继续做?为什么你会一直这样做,这样你就拥有系统 15% 的 CPU?
我用 gdb 附加到它,以找出这个睡眠调用的确切来源,但是有一个问题。在 Raspbian 领域,似乎没有人关心他们的包有调试符号。通常在 Debian 的世界中,您可以在 apt 源文件中添加一些额外的内容,进行更新,然后您将拥有一个全新的 -dbgsym 软件包世界供您使用。如果那些存在于 Raspbian 中,我肯定找不到它们。
因此,除了 glibc 本身的符号之外,我没有其他符号。无论如何,我放弃了一个核心,因为我知道我可能会在某个时候终止该进程并希望稍后再回来。我很高兴我做到了,因为它最终帮助解决了这个谜团。
接下来的一个小时左右,我试图在我从源代码构建的版本中重现这一点。它不会进入那个循环。相反,它会启动并进入为期一周的睡眠。它在源中称为睡眠的少数几个地方似乎并没有导致这种情况。我对此感到非常恼火,差一点就结束了,但后来我决定从首要原则重新开始。
原来的进程早就死了,所以我不能再用 strace 或 gdb 来抓取它,但老实说,它们都不会告诉我任何新的东西。我确实还有核心文件,所以还有很多,我设法加载它来查看:
[当前线程为 1 (线程 0x76f49200 (LWP 422))]
(gdb) BT
#0 0x76e22f8c in __GI___clock_nanosleep_time64 (clock_id=clock_id@entry=0, flags=flags@entry=0, req=0x7ee28c58, req@entry=0x7ee28c50, rem=0x7ee28c68, rem@entry=0x7ee28c60) at ../sysdeps/unix/ sysv/linux/clock_nanosleep.c:52
#1 0x76e23080 in __GI___clock_nanosleep (clock_id=clock_id@entry=0, flags=flags@entry=0, req=req@entry=0x7ee28c9c, rem=rem@entry=0x7ee28c9c) at ../sysdeps/unix/sysv/linux/ clock_nanosleep.c:92
#2 0x76e29830 in __GI___nanosleep (requested_time=requested_time@entry=0x7ee28c9c, remaining=remaining@entry=0x7ee28c9c) 在 nanosleep.c:27
#3 0x76e2971c in __sleep (seconds=0) at ../sysdeps/posix/sleep.c:55
#4 0x00011470 在?? ()
请注意,有问题的线程是 gdb 调用的 #1,即 LWP 422,也就是 PID 422。如果您查看我之前的 strace,所有这些对 sleep 的调用都来自 pid 422。这告诉我它不是t 导致错误的其他线程之一,而是主线程。
从源代码运行它的所有这些闲逛都告诉我,程序的主线程只是启动一些工作线程,然后进入这个循环,它调用睡眠并可能执行一些统计信息。它看起来像这样:
sleepinterval = arguments->stats_interval ? arguments->stats_interval : STATS_INTERVAL_MAX; 睡眠时间 = 睡眠间隔; 而(!gotsigterm){ 睡眠时间 = 睡眠(睡眠时间); if ((arguments->stats_interval && sleeptime == 0) || gotsigusr1 || gotsigterm) { dump_rng_stats(); 睡眠时间 = 睡眠间隔; 得到sigusr1 = 0; } }
查看代码,sleepinterval 是从一个三元表达式中设置的,该表达式要么采用 stats_interval 的值(如果它不为零),要么将其设置为 STATS_INTERVAL_MAX,结果为 604800 秒。这相当于一周的几秒钟,每当我运行我从源代码构建的那个时,就会出现 604800。那里并不奇怪。
鉴于 strace 和 gdb 已经证明循环代码来自 main,这是 main 结束的地方,这怎么可能休眠 0 秒?毕竟,它在循环内部那个看起来很乱的“if”中重置它,并将它设置为 sleepinterval,sleepinterval 是 604800,所以一切都很好,对吧?
嗯,不,实际上。
大约在这个时候,我注意到这不是一个简单的睡眠呼叫。它实际上是使用睡眠的返回值。我看的不多,一开始就错过了。
睡眠时间 = 睡眠(睡眠时间);
这整件事。所以……为了让我们永远睡眠(0),那么睡眠时间必须为0。但我们总是将它重置得更高,不是吗?不,我们没有。
if ((arguments->stats_interval && sleeptime == 0) || gotsigusr1 || gotsigterm) {
展开该分支是这样的。
arguments->stats_interval 是真的吗?它设置为 0,所以不,它不是。结果,我们甚至不看第一个的其余部分。 gotsigusr1 和 gotsigterm 也没有设置,因为我们没有收到信号。这些 OR 可能性都不是真的,所以我们不采取分支。我们不会 dump_rng_stats,也不会将 sleeptime 重置为 sleepinterval 。
这意味着睡眠时间将保持设置为从睡眠中出来时的状态。可以设置为零吗?绝对地。 sleep 的返回值是“无论出于何种原因退出时,您还需要等待多长时间”。这是因为睡眠可以被中断,这样你就可以重新启动它。更糟糕的是更好,PC失败,所有这些东西。
但是如果你达到了你要求它等待的时间会发生什么?那么,它返回零。
所以……我们所要做的就是在没有收到任何一个信号的情况下一直通过睡眠,它会返回 0,然后永远不会被重置,所以它会一次又一次地被调用 0,一次又一次,……..在一个非常紧密的循环中,一路烧毁CPU。
诀窍,以及它如此难以重现的原因是,你必须等待整整一周才能第一次超时!是的。想象一下这种耐心。
我没有那种耐心,所以为了验证我的假设,我将 STATS_INTERVAL_MAX 降低到 30 秒,并在 strace 中启动了新的构建。它在那里坐了 30 秒,然后迅速开始在一个非常紧张的睡眠循环中咀嚼 CPU。
所以,是的,如果你用 -S0 运行 Debian 的 rngd,它会在一周左右的时间内完全正常,然后它会进入一个紧密的循环,从那时起你的一个 CPU 会被占用,直到进程结束由于某种原因停止了。迷人的。
如果您出于类似的保卡原因尝试在您的 Raspberry Pi 上以这种方式运行它,您可能想去看看您是否有一个正在吃核心的 rngd。如果您的正常运行时间超过一周,我敢打赌。
嘘。
…
旁注:这是我在深入研究代码时看到的第一件事。我在寻找可能调用睡眠的地方,并找到了一个名为 random_sleep() 的函数。看一下这个。
gettimeofday(&start, NULL); while (!gotsigterm && timeout_usec > 0 && 轮询(&pfd, 1, timeout_usec) < 0 && 错误号!= EINTR){ gettimeofday(&now, NULL); timeout_usec -= elapsed_time(&start, &now); 开始=现在; }
马上,我看到“gettimeofday”并认为“我选择了错误的星期来阻止某事” 。他们正在计算经过的时间……用挂钟。这就是 gettimeofday() 给你的。如果你想要一个单调的时钟,你需要使用 clock_gettime() 并指定正确的时钟。如果系统时钟在运行时发生变化,这只是自找麻烦。
这么说吧:他们明确检查 timeout_usec 是一个正数是一件好事,因为如果你通过 poll 一个负超时,这意味着“无穷大”。整洁,对吧?
我想在我找到其他东西之前我会停下来。