登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

Linux getrandom 设置 GRND_NONBLOCK 后为什么返回 EAGAIN

来源:17golang原创

时间:2026-11-15 00:50:55 173浏览 收藏

我在 Linux 服务启动阶段遇到 getrandom() 返回 EAGAIN 时,第一反应也曾是“随机数接口是不是坏了”。但如果调用参数里有 GRND_NONBLOCK,这个结果通常正是参数承诺的行为:随机源还没准备好时,不等待,立即返回。

官方文档:https://man7.org/linux/man-pages/man2/getrandom.2.html

默认从 urandom 源读取时,如果随机池尚未初始化,getrandom 会等待;加入 GRND_NONBLOCK 后则返回 -1,并把 errno 设为 EAGAIN。处理重点不是把 EAGAIN 当成参数错误,而是决定启动流程要延后、有限重试,还是允许系统调用阻塞。

GRND_NONBLOCK 为什么会把等待变成 EAGAIN

getrandom() 默认从 urandom 源获取随机字节。这里有一个启动时序:内核随机池尚未初始化时,普通调用会阻塞;如果 flags 包含 GRND_NONBLOCK,调用者明确告诉内核“不要在这里等”,于是内核用 EAGAIN 表示暂时没有可立即返回的数据。

getrandom 默认 urandom 源、随机池状态与 GRND_NONBLOCK 返回 EAGAIN 的静态关系图
图1:getrandom 随机源状态与 GRND_NONBLOCK 返回分支图;这是静态说明图,不是终端截图或运行证据。

因此下面两种写法的取舍不同:

/* 允许内核等待随机池就绪;适合不要求启动路径非阻塞的初始化。 */
ssize_t n = getrandom(buf, sizeof buf, 0);

/* 明确不等待;随机池未就绪时要检查 errno == EAGAIN。 */
ssize_t n = getrandom(buf, sizeof buf, GRND_NONBLOCK);

第二种写法并不保证“永远更快”。它只是把等待责任交还给应用:应用可以延后初始化、安排下一次事件,或者做有上限的重试。若把它写成无间隔死循环,反而会在启动早期持续占用 CPU。

先判断返回值,再判断 errno

调用约定很简单,但顺序不能反:返回值为 -1 时才读取 errno;返回非负值时,返回值本身表示已经复制到缓冲区的字节数。对于小于或等于 256 字节、且使用默认 urandom 源的读取,文档给出了“初始化完成后通常一次返回请求字节数”的保证,但代码仍应检查返回值。

#include 
#include 
#include 

int read_seed(unsigned char *buf, size_t size) {
    /* 非阻塞只改变“未就绪时怎么返回”,不会改变错误检查规则。 */
    ssize_t n = getrandom(buf, size, GRND_NONBLOCK);
    if (n == (ssize_t)size) {
        /* 读取完整,调用者可以继续初始化。 */
        return 0;
    }
    if (n == -1 && errno == EAGAIN) {
        /* 随机池暂未就绪:这是可延后的状态,不是参数错误。 */
        return 1;
    }
    if (n == -1 && errno == EINTR) {
        /* 被信号打断,是否重试要由上层策略决定。 */
        return 2;
    }
    /* 其他错误或短读都不能假装成完整种子。 */
    return 3;
}

这里返回 1、2、3 是给示例调用者区分状态的本地约定,不是 Linux 的 errno 编号。尤其不要把 n >= 0 直接等价于“成功”:如果请求较大,或者使用了 GRND_RANDOM,返回字节数可能小于请求值。

启动阶段怎样设计有限重试

如果服务必须尽快完成启动,又需要随机种子,可以只对 EAGAIN 做短暂、有限的等待。下面示例使用 256 字节缓冲区,并把重试次数写成显式上限;它是策略示例,不代表某个发行版的最佳时间参数。

启动阶段 getrandom 小缓冲区、有限重试与 EAGAIN 终止边界的静态关系图
图2:启动阶段的 getrandom 返回处理边界图;这是静态说明图,不代表真实执行结果。
#include 
#include 
#include 

int fill_seed(unsigned char seed[32]) {
    const int max_attempts = 5;  /* 上限防止启动路径无限忙等。 */
    for (int attempt = 0; attempt 

这个策略有三个关键点:第一,只有明确识别出的暂态情况才重试;第二,重试有总次数上限;第三,耗尽后必须把失败交给上层,而不是用时间戳、固定字符串等不等价的数据替代。对于对启动延迟不敏感的程序,直接不加 GRND_NONBLOCK 可能更简单。

256 字节、GRND_RANDOM 和短读边界

不要把“getrandom 返回 EAGAIN”与所有随机源情况混为一谈:

场景需要关注的结果处理重点
默认 urandom,随机池未初始化,带 GRND_NONBLOCK-1,errno=EAGAIN延后或有限重试
默认 urandom,读取不超过 256 字节且已初始化通常返回完整请求仍检查返回值
指定 GRND_RANDOM,随机源暂时没有足够字节可能短读或 EAGAIN不能假设一次填满
大于 256 字节的请求被信号打断可能短读或 EINTR按实际 n 和 errno 处理

在普通服务初始化中,我更倾向于请求较小的缓冲区,并让初始化函数明确返回“完成、暂时不可用、永久失败”三类状态。这样调用方能决定是稍后重试,还是让服务管理器重新安排启动,而不是把内核的时序信号吞掉。

排查 EAGAIN 时的一套判断顺序

  1. 确认 flags 是否真的包含 GRND_NONBLOCK,不要先去怀疑随机设备文件。
  2. 只在返回值为 -1 时读取 errno,确认是否为 EAGAIN。
  3. 确认请求使用的是默认 urandom 源还是 GRND_RANDOM,两者的短读和等待语义不同。
  4. 检查请求大小,优先把启动阶段的种子读取控制在 256 字节以内。
  5. 为 EAGAIN 设计有限重试或异步延后,为其他错误保留明确失败路径。

几个容易混淆的问题

EAGAIN 是不是说明 getrandom 参数非法?

不是。非法 flags 通常对应 EINVAL;EAGAIN 表示如果不使用非阻塞标志,调用本来会等待,但当前选择了立即返回。

把 GRND_NONBLOCK 去掉就一定更好么?

不一定。它会让调用可能阻塞,启动路径是否允许等待取决于服务的时延目标、依赖关系和故障处理方式。关键是明确选择,而不是盲目替换参数。

收到 EAGAIN 能不能马上 while 重试?

可以实现,但不建议无间隔无限重试。应使用短暂等待、次数或截止时间上限,并在耗尽后交给上层决定延后启动还是失败退出。

GRND_NONBLOCK 的真正含义,是把“随机源何时可用”从隐式阻塞变成了应用可观察的状态。理解这一点后,EAGAIN 就不再是神秘错误,而是启动流程需要认真处理的一条分支。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>