关注了就能看到更多这么棒的文章哦~
A vDSO implementation of getrandom()
By Jonathan Corbet
January 6, 2023
DeepL assisted translation
https://lwn.net/Articles/919008/
大多数开发者可能不认为他们程序的性能瓶颈会在生成随机数上,但似乎真的会有例外。在过去的几年里,Jason Donenfeld 让内核随机数生成器的开发工作增添了许多活力,他现在把精力放在提高用户空间的性能上,用一组 patch 提供了内核的 "虚拟动态共享对象"(vDSO,virtual dynamic shared object)区域中实现 getrandom()系统调用的方案。这样一来确实有更好的性能了,但并不是所有的开发者都认为这种好处值得其引入的复杂性。
传统上来说,Linux 系统的用户空间进程是通过打开/dev/urandom(或/dev/random)并从中读取数据来获得随机数的。最近加入的 getrandom() 简化了随机数据的获取动作;对 getrandom() 的调用将用来自内核的随机数据填充到用户空间的缓冲区里,而不需要打开任何文件。内核对这些随机数提供了能够提供的尽量多的保障,包括尽最大努力确保数据确实是随机的,并在例如虚拟机 fork 等场景时防止出现数据序列重复的情况。
值得注意的是,在 BSD 世界中,调用 arc4random() 库函数是比较常见的做法。GNU C 库的 2.36 版本包括 arc4random()的具体实现代码,处于 pre-release 状态,其中包含了相当多的代码逻辑来完成随机数据生成和管理。在 2022 年 7 月,Donenfeld 质疑是否需要这个函数,他指出 "getrandom()和/dev/urandom 都非常快"。不过,支持 arc4random()可以就可以使代码更加便于移植,所以这个函数就留在了 C 中。最终发布的版本被 Donenfeld 大大简化了,以至于当系统中存在 getrandom()的系统调用时,C 库中这个函数基本上是一个 wrapper 而已。因此,getrandom()的性能也决定了 arc4random()的速度。
The vDSO API
尽管 getrandom()可能 "非常快",但一些用户显然抱怨说它仍然不够快。作为回应,Donenfeld 现在正在努力创建一个 vDSO 实现。vDSO 是一个内核中一个特殊的内存区域,会被映射到每个用户空间进程中;它包含了可以执行的代码,用来完成类似于系统调用的功能,可以在不实际进入内核态的情况下就完成这些动作,从而避免了上下文切换的成本。以这种方式实现的系统调用包括 getcpu()和 clock_gettime(),它们都是简单地从内核中读取一些数据的操作。
将 getrandom()移入 vDSO 有可能提高那些需要大量随机数的应用程序的性能,但如果要确保达到跟相应的系统调用同等的保障程度,显然就需要做更多事情,不仅仅是读一些内核数据而已。因此,实现这一功能的 patch 涉及到了 59 个文件,增加了大约 1200 行代码。它还使得接口变得有些复杂,因为现在有两个(virtual)系统调用。必须在请求任何随机数据之前至少调用一次第一个 API:
void *vgetrandom_alloc(unsigned int *num, unsigned int *size_per_each,
unsigned long addr, unsigned int flags);
调用者(可能是 C 库)应该在 *num 中指定所需的 "opaque states" 的数量,这个数字可能对应于预期会运行的线程数。这个调用里会先分配一个内存区域,并返回其地址。实际分配的 state 数量将存储在*num 中,每个 state 的 size 存储在*size_per_each 中。其他两个参数目前未使用,必须为零。
库函数需要给进程中的每个线程都分配一个返回的 state;指向任何一个 state 的指针都可以通过将基地址加上*size_per_each 中返回值的倍数来作为偏移量从而计算出来。内核使用这个区域来跟踪该线程的随机数发生器的状态;用户空间不应该访问其内容。如果需要更多的 state-storage 空间,可以多次调用 vgetrandom_alloc()。
有了 state 空间之后,就可以通过以下方式获得随机数据:
ssize_t vgetrandom(void *buffer, size_t len, unsigned int flags,
void *opaque_state);
前三个参数与 getrandom()的参数相同,而 opaque_state 指向 vgetrandom_alloc() 返回的 state 之一。我们的目的是让这个函数的行为与 getrandom()一样,只是速度更快。应用程序通常不会直接调用它,而是从 C 库的 getrandom() wrapper 中来间接调用。Donenfeld (在 patch 的封面邮件中)说 "性能相当出色(对于 uint32_t 形式生成的数据来说,大约是 15 倍的速度),而且看起来工作得很好"。
VM_DROPPABLE
虽然有些开发者显然不太需要优化 getrandom() 速度,但是目前来看,大部分的 patch 都是没有多少反对意见的。不过,有一个重要的例外,就是关于 "opaque state" 数据的管理。这个数据被内核用来确保每个调用 vgetrandom()的调用者都能得到唯一的一系列随机数据,每个线程的随机数生成器都能在需要时进行 reseed 等等;它可以被认为是一种无需进入内核就能查询到的 per-thread entropy storage (针对每个线程的随机熵数据的存储)。对于希望了解细节的人来说,每个 state entry(在内核中)被定义如下:
struct vgetrandom_state {
union {
struct {
u8 batch[CHACHA_BLOCK_SIZE * 3 / 2];
u32 key[CHACHA_KEY_SIZE / sizeof(u32)];
};
u8 batch_key[CHACHA_BLOCK_SIZE * 2];
};
vdso_kernel_ulong generation;
u8 pos;
bool in_use;
};
这个结构占用了 256 个字节,并不算多,但是一个运行了大量线程的系统中可能有相当多的这种结构。这是内核分配的内存,并且必须被锁定在 RAM 里,因为如果把它写入二级存储设备中就可能会暴露随机数生成器的 state,会产生安全问题。因此,这些 state memory 必须被特别处理。
早期版本的 patch set 中使用 mlock()来确保这块内存会一致保持在原位。但这种方法有一些问题,首先是 lock 下来的内存受到资源限制。如果内核和 C 库开始使用进程允许的一些 locked memory 来生成随机数,那么一些之前可以正常工作的程序可能开始出错。locked memory 在 fork 后也会在子进程中 unlock。所以使用 mlock()并不是一个理想的解决方案。
不过,用于存放 state 的内存还有一个有趣的特点:它可以随意在任何时候被销毁,不会带来什么特别的后果。因为它本质上是一个随机数据的缓存区域,它可以被更多的随机数来取代。例如在线程 fork 的时候就会发生这种情况。在最坏的情况下,如果 state 内存完全不可用了,vDSO 函数只需相应地调用 getrandom()就可以完成任务(尽管速度会慢)。因此,虽然 state 内存应该被锁定,即不被写入 swap 区,但它不需要绝对保证被固定在 RAM 里;在需要的时候完全可以被扔掉。
Donenfeld 决定利用这种内存的可以随意抛弃的特性。目前的 patch 中没有继续使用 mlock() 了,而是增加了一个新的(供内核内部使用的)memory flag,名为 VM_DROPPABLE。由这个标志标记的内存将永远不会被写入 swap 或 core dump 中,如果内存紧张的话,内存管理子系统可以直接将其回收,用于其他用途。VM_DROPPABLE 内存是按需求分配 page 的,在试图访问这个区域之前不会被实际分配出来;因为通常在这种情况下如果在 page allocate 的时候出错,对相关进程来说回事 fatal 错误。也就是说对于 VM_DROPPABLE 内存来说,这些可以直接忽略错误,任何尝试进行的写入都会被直接放弃,这是通过一些底层的、架构相关的魔法方式来实现的,会直接跳过那些 write 指令。
Dropping VM_DROPPABLE
Ingo Molnar 强烈反对这个补丁,说它给一个需要高度信任的子系统增加了复杂性。他说,最好是让 mlock() 能来满足需求,或者干脆让 vDSO 在现有资源限制之外多分配几个 page。Donenfeld 的回复很不好,他质疑 Molnar 的动机。Molnar 随后否决了这个 patch,引来了 Donenfeld 更多的抱怨,不过他确实指出,一个进程可以对 vgetrandom_alloc()进行无数次的调用,所以需要注意防止它被用来规避 locked memory 的限制。简单地调整 mlock()并不是一个足够的解决方案。
Andy Lutomirski 也不喜欢 VM_DROPPABLE,他说 Donenfeld "试图把各种奇怪的行为塞进 mm 核心代码中"。他建议 Donenfeld 可以创建一种特殊的 mapping 类型,用它自己的本地实现来获得所需的功能;内核提供了这样做所需的基础设施。这样就不需要修改 core memory-management 子系统了。Donenfeld 对这个建议的回复相对积极一些,并且似乎准备朝着这个方向前进。不过,Linus Torvalds 认为,这些努力都是不值得的。他说,加速随机数的生成的工作,在超过某个界限之后,就不再是内核的工作了,而是应该留给 C 库来完成。经过一些讨论,Torvalds 明确了他的立场:
我不赞同为了这种特殊的需求来中 VM (译者注:应该是指 virtual memory)进行侵入式的修改。我真的相信,有这个问题的人是 非常 少的,他们可以自己很好地处理虚拟机的 fork 和 reseed 问题。
Donenfeld 也明确表示不同意 Torvalds 对这一功能的需求方的判断。他声称,在用户空间创建一个安全的随机数生成器是不可能的(patch cover letter 中也深入介绍了这一点)。他后来又说:
我认为,昨天对这个很酷的新想法的嘘声,就说明了 Linux 内部氛围并不真正把关心 "安全问题" 作为工作的一个动机,而是只是采取最快和最容易的方案,像赶苍蝇一样把安全需求赶走,并假设这些关注是不真实的、小众的、偏执的或其他什么。
不出意料,Torvalds 不同意这种观点。
不过最后,从性能和安全的角度来看,vDSO getrandom()的实现似乎确实有合理的需求。因此,关于 vDSO 的工作可能会继续进行,但 VM_DROPPABLE 这个 flag 显然不能让大家同意了。Donenfeld 几乎肯定会带着基于 Lutomirski 建议的新的实现再回来;这应该能解决人们对这项工作的主要担忧。目前来看它能走到 upstream 的机会可能是相当大的,即使一些开发者仍然不相信它真的有多大必要。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~