为什么getrandom()调用无法返回

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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值