关注了就能看到更多这么棒的文章哦~
A generic ring buffer for the kernel
By Jonathan Corbet
June 6, 2024
Gemini-1.5-flash translation
https://lwn.net/Articles/976836/
内核的用户空间 ABI 中不乏环形缓冲区(ring buffer);在例如 BPF、io_uring、perf 和 tracing 这些子系统中都有定义了。当然,这些环形缓冲区中的每一个都是独立的实现,它们之间没有公共接口。面对这种 ABI 泛滥,自然而然的反应就是添加另一个环形缓冲区作为通用选项;这就是 Kent Overstreet 的这个补丁系列 的目标,它添加了一套新的系统调用用于环形缓冲区。
环形缓冲区只是一个保存在内存中的循环缓冲区,它在用户空间和内核之间共享。数据流的一侧将数据写入缓冲区,而另一侧则消费数据;只要缓冲区既不会溢出(overflow)也不会下溢(underflow),数据就可以在没有任何系统调用的情况下传输。因此,添加环形缓冲区可以在数据速率相对较高的场景中实现高效的数据传输。Overstreet 认为其他内核子系统可以从环形缓冲区接口中受益,并希望能够添加这些接口而不必重新发明轮子。
用户空间接口
在用户空间方面,新提议的接口中要求把环形缓冲区始终与文件描述符相关联,因此第一步将是打开与环形缓冲区将要附加的对象(如文件、设备或管道)。然后,使用新的系统调用建立环形缓冲区:
int ringbuffer(unsigned int fd, int rw, u32 size, void *buffer);
此调用将请求创建与打开的文件描述符 fd
关联的环形缓冲区;对于读取访问, rw
应设置为零,对于写入访问,应设置为其他任何值。请求的缓冲区大小在 size
中传递。在成功返回时,缓冲区的地址将存储在 buffer
中。
请注意,由实现环形缓冲区的子系统决定是否通过创建新的缓冲区来响应此调用,或者是否将现有的缓冲区映射到调用者的地址空间。后者的目的是允许单个环形缓冲区在多个进程之间共享。
在此版本的系统调用中没有 flags
参数;这很可能在最终合并到主线之前会改。
返回的指针将指向一个内存区域,该区域从该结构的副本开始:
struct ringbuffer_ptrs {
__u32 head;
__u32 tail;
__u32 size; /* always a power of two */
__u32 mask; /* size - 1 */
__u32 data_offset;
};
缓冲区本身将从返回的指针开始的 data_offset
处开始;实际上,该偏移量可能总是 1 个 page。缓冲区的实际大小将存储在 size
中;如上所述,它将始终是 2 的幂,原因很快就会讲到。 head
偏移量是写入位置,而 tail
是读取位置。如果 head
和 tail
相等,则缓冲区为空。
head
和 tail
在添加或消费数据时都会无条件地递增;没有围绕循环的测试。相反,这些值必须与 mask
值进行 AND 运算以获得缓冲区的实际偏移量。这种技术消除了递增路径中分支的需要,但也意味着在使用 head 和 tail 值时需要小心。测试为空很容易,因为在这种情况下两个值始终相等。对是否已满的正确测试如下:
if ((head - tail) >= size)
/* buffer is full, can't write to it */
(这个检测已被简化;正确编写的测试必须使用原子操作以避免访存顺序 memory-ordering 问题;有关示例,请参见 此测试程序)。
写入环形缓冲区的进程可以继续向其中馈送数据,直到缓冲区填满,此时它必须停止。写入器可以轮询 tail
值以检测读取器何时追赶上来以及是否有更多空间可用,但这效率不高。类似的情况也适用于已清空缓冲区的读取方。无论哪种方式,进程都可以使用以下方法等待情况发生变化:
int ringbuffer_wait(unsigned int fd, int rw);
其中 fd
是与环形缓冲区关联的文件描述符,=rw= 指示需要读取访问还是写入访问。无论哪种方式,调用进程都会阻塞,直到环形缓冲区的情况发生变化。指示这种变化的责任也落在用户空间;将数据放入之前为空的缓冲区的写入器,或从之前已满的缓冲区消费数据的读取器应该调用:
int ringbuffer_wakeup(unsigned int fd, int rw);
在说明信中,Overstreet 提到 ringbuffer_wait()
和 ringbuffer_wakeup()
最终可能被 futex 操作取代。此补丁 还提到了需要改进 ringbuffer_wait()
以允许超时或指定在唤醒发生之前必须写入(或读取)缓冲区的最小数据量。
内核端
要向子系统添加环形缓冲区支持,应提供 file_operations
结构中的新增的 ringbuffer()
函数:
struct ringbuffer *(/ringbuffer)(struct file /file, int rw);
此函数将在响应来自用户空间的 ringbuffer()
调用时被调用。有一整套精心记录的用于处理环形缓冲区的支持函数可供驱动程序使用,包括 ringbuffer_alloc()
用于创建环形缓冲区, ringbuffer_read_iter()
和 ringbuffer_write_iter()
用于将数据移入或移出环形缓冲区等等。此补丁 提供了一个测试驱动程序,展示了在内核空间实现环形缓冲区的基础知识。
除了测试设备之外,此补丁集中没有提交任何内核内部用户。在说明信中,Overstreet 表示第一个这样的用户将是用户空间文件系统 (FUSE) 子系统,但管道和套接字可能会随后出现。据说环形缓冲区方法的性能优势显着:“以每次 16 字节的速度从环形缓冲区读取/写入比使用 read/write 系统调用快约 7 倍”。
截至撰写本文时,还没有对此提交内容的回复。这项工作仍处于早期阶段,在被认为可以合并之前,似乎需要进行一些演变;开发人员也可能希望在内核中看到一个真正的用户。更有趣的是,展示如何使用这种抽象来实现一些现有的内核环形缓冲区接口(虽然它们现在不会改变)。如果这项工作实现了其目标,它可能(从高度乐观的角度来看)是添加到内核中的最后一个环形缓冲区接口。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~