1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 问题背景
负责在 Linux 4.9 内核上开发 Qt 应用的同事反馈了一个问题:将Qt由一个低版本迁移到更高版本后,准备进入测试阶段,于是将Qt程序由平时开发时的手工启动,变更为开机自启动,但是开机后发现Qt程序无法启动!奇怪的是,用 kill命令停止Qt程序后,等待一段时间后(系统引导完成后,等待一段时间),再手动启动Qt程序,程序大多数时候可以运行起来,但仍然有一定的几率无法启动。
3. 问题分析
首先,使用 gdb attach 定位了程序卡在了 glibc 的 getrandom()调用上:
main(int argc, char *argv[])
QApplication app(argc, argv)
...
getrandom(..., ..., 0)
接下来, 通过 glibc 源码了解到 getrandom() 仅是对系统调用 sys_getrandom() 的简单调用,不存在任何可能进入无限循环的代码,这引导我们将目光投向了内核侧。在这一点上,我们也通过
cat /proc/<pid>/stack # 需开启内核配置项 CONFIG_STACKTRACE
cat /proc/<pid>/syscall # 需开启内核配置项 CONFIG_HAVE_ARCH_TRACEHOOK
cat /proc/<pid>/wchan # 需开启内核配置项 CONFIG_KALLSYMS
进行了进一步的确认,发现程序确实在内核的 wait_for_random_bytes() 内打转,而且 top 也观察到程序大多时候处于 S 态(即睡眠状态),这意味着程序在等待某个事件的发生。如果内核打开了 ftrace ,也可以通过 ftrace 来观察程序内核侧的调用,会比前面的方式观察得更加清楚。让我们来看一下系统调用 sys_getrandom()为什么会卡住,或者说为什么它一直无法返回用户空间:
sys_getrandom()
/* 场景中的调用 flags == 0, 所以不用考虑 flags != 0 的代码路径 */
...
/*
* 如果 crng 没有准备好,需要进行等待。
* 在我们的问题场景,程序就是卡在这里一直无法返回。
*/
if (!crng_ready()) { /* (likely(crng_init > 1)) */
wait_for_random_bytes()
wait_event_interruptible(crng_init_wait, crng_ready()) /* 进程陷入睡眠 */
}
从上面的代码看出,导致程序无法返回用户空间的原因,是因为 crng_ready() 不成立。进一步分析内核代码,我们能够看到如下这些场景,可能使 crng_ready() 成立继而唤醒睡眠进程:
/*
* crng 随机数熵池数据来源 1:用户随机输入事件。
*/
input_event()
input_handle_event()
add_input_randomness()
add_timer_randomness(&input_timer_state, ...)
r = &input_pool;
credit_entropy_bits(r, ...)
if (crng_init < 2 && entropy_bits >= 128)
crng_reseed(&primary_crng, r)
if (crng == &primary_crng && crng_init < 2) {
...
crng_init = 2; /* 使 crng_readdy() 成立 */
wake_up_interruptible(&crng_init_wait); /* 唤醒等待随机数的进程 */
pr_notice("random: crng init done\n"); /* 告知 crng 随机数子系统已经准备好了 */
...
}
/*
* crng 随机数熵池数据来源 2:磁盘操作。
*/
blk_update_bidi_request()
if (blk_queue_add_random(rq->q))
add_disk_randomness(rq->rq_disk)
...
crng_reseed(&primary_crng, r)
/* 后面流程同上 */
/*
* crng 随机数熵池数据来源 3:中断。
*/
handle_irq_event_percpu()
add_interrupt_randomness()
...
crng_reseed(&primary_crng, r)
/* 后面流程同上 */
/*
* crng 随机数熵池数据来源 4:随机数硬件驱动。
*/
add_hwgenerator_randomness()
...
crng_reseed(&primary_crng, r)
/* 后面流程同上 */
/*
* 更多的 crng 随机数熵池数据来源
*/
......
CRNG(Cryptographic Random Number Generator) 随机数子系统的基本工作原理,是利用随机的事件,如用户输入、磁盘操作、中断等,向随机数熵池填充数据,而用户则从熵池中拿取数据,从而使得随机数真正变得随机。而导致问题场景中,开机启动Qt程序时,卡在 sys_getrandom() ,是因为嵌入式系统中,外设少,产生的对随机数熵池做贡献的事件少,导致熵池数据不足,从而卡在内核侧;而手工启动程序时,一般系统都运行了一段时间,随机数熵池被各种事件填充了足够的数据,所以Qt程序可以启动起来,偶尔的不能启动,也是因为Qt程序,在随机数熵池尚未填充足够数据时运行。另外,低版本的Qt能够运行起来,是因为它在getrandom()是采用非阻塞的方式进行调用。
4. 问题修复
找到了问题的根因,接下来我们要做的事情,自然是对问题进行修复。一开始的修复方案,我们的思路是,既然问题是由于 CRNG 随机数熵池数据不足导致的,而且系统又提供接口,允许特权用户对该熵池进行填充,那么我们就主动的填充数据,来解决问题。后来经过一番Google搜索,发现已经有现成的方案 haveged ,我们就直接拿来用了。haveged 的基本工作原理就是对 /dev/random 发起 ioctl(RNDADDENTROPY)。
当然,haveged 并不是唯一的修正方案,事实上,社区针对该问题进行了一系列的讨论,Linus 本人在 Linux 5.4 合入了针对该问题内核侧的一个解决方案:
https://lwn.net/Articles/802360/
https://lore.kernel.org/lkml/CAHk-=wgjC01UaoV35PZvGPnrQ812SRGPoV7Xp63BBFxAsJjvrg@mail.gmail.com/