LWN:epoll也要用ring buffer来跟用户程序交互了

栏目: 后端 · 发布时间: 4年前

内容简介:ByMay 30, 2019epoll这一组system call系统调用设计目的就是希望能大大增强对大量I/O events的polling能力。它能减少针对每次system call的准备工作,直接返回多个events,这样系统调用的使用次数也会减少。不过有些用户仍然不满意,觉得不能支撑自己更大规模场景下的使用。相应的,Roman Penyaev提出了一组patch,给kernel增加了另一个ring-buffer接口。

A ring buffer for epoll

By Jonathan Corbet

May 30, 2019

epoll这一组system call系统调用设计目的就是希望能大大增强对大量I/O events的polling能力。它能减少针对每次system call的准备工作,直接返回多个events,这样系统调用的使用次数也会减少。不过有些用户仍然不满意,觉得不能支撑自己更大规模场景下的使用。相应的,Roman Penyaev提出了一组patch,给kernel增加了另一个ring-buffer接口。

poll()和select()这两个系统调用会等待,直到一组file descriptor中的某一个可以开始进行I/O操作。每次调用的时候,都需要kernel准备一个内部数据结构,这样当某个file descriptor状态改变的时候能得到通知。epoll就是对这里的一个改进,它把准备和等待这两个阶段分离开,内部的数据结构就可以一直保留着。

应用程序先用epoll_create1()来创建file descriptor(后续简称fd),后续步骤需要用到。这个API基本上取代了epoll_create(),把那个用不到的参数换成了一个flag参数。接下来调用epoll_ctl()来把各个需要监控的fd加到epoll的监控集合里面。最后调用epoll_wait(),会一直阻塞住,直到至少有一个fd有状态变化才返回。这个流程比poll()多了几个步骤,但当应用程序在监控大量的fd的时候,就能有很大的性能改善。

其实,还能够有性能改善的空间。尽管epoll比此前的方案都更加高效,不过application还是需要通过调用system call才能让下一组fd准备好进行I/O。在繁忙系统里,如果能够不用调用系统调用就能拿到最新的event,肯定会提高效率。Penyaev的patch set就是这样实现的。他创建了一个ring buffer,让application和kernel公用,可以用来传递events。

epoll_create() — the third time is the charm

想要利用这个机制的application,第一步需要告诉kernel后续会开始用polling机制了,以及需要多大的ring buffer。epoll_create1()没有这些参数,因此他创建了一个新的system call,epoll_create2():

int epoll_create2(int flags, size_t size);

也增加了一个新的flag,EPOLL_USERPOLL,目的是告诉kernel要用ring buffer来传递events。size参数就是指明ring buffer需要容纳多少项。size会被增长到按2的幂次,然后用于设置这个epoll命令能监控的 fd 最大数量。目前的patch set里面限制了不能超过65536个项目。

接下来仍旧使用epoll_ctl()来把所关心的多个fd加入polling集合。这里还是会有一些限制,因为某些操作跟user-space polling不兼容,例如每个fd都需要用EPOLLET flag来指定成边沿触发的条件。当某个fd报告说已经准备就绪的时候,只有一个event会被放入ring buffer,这里肯定不能用电平触发的方式来持续不断的放event进入ring buffer。EPOLLWAKEUP flag(用于防止系统在某些event被处理的过程中进入suspend)在这个模式下也无法工作。EPOLLEXCLUSIVE也不支持。

后面还需要两三次mmap()调用,来将ring buffer映射到user space。第一次调用需要指定0作为offset、length是一个page的大小,这样就能获得一个包含如下信息的结构:

struct epoll_uheader {
	u32 magic;          /* epoll user header magic */
	u32 header_length;  /* length of the header + items */
	u32 index_length;   /* length of the index ring, always pow2 */
	u32 max_items_nr;   /* max number of items */
	u32 head;           /* updated by userland */
	u32 tail;           /* updated by kernel */

	struct epoll_uitem items[];
    };

这里的header_length成员变量其实包含了epoll_uheader结构和它的数据数组的长度。可以参照这个例子程序(https://github.com/rouming/test-tools/blob/master/userpolled-epoll.c  ),后续application会把header结构map出来,得到实际的长度,把这个page unmap掉,然后重新用header_length的长度来再次map出来,得到全部数据的数组。

大家可能以为item就是这个ring buffer,其实这里有点绕。还需要再调用mmap()一次才能得到真正的ring buffer,传入的参数里offset是header_length,length是index_length。mmap获取的结果就是一组数据,里面每一项都存放着指向items数组的一个整形索引数字,这才是真正的ring buffer。

最终表示每个event的结构如下:

struct epoll_uitem {
	__poll_t ready_events;
	__poll_t events;
	__u64 data;
    };

其中,events是epoll_ctl()调用时提供的那组events集合。ready_events就是真正已经触发的event集合。这里data成员直接来自epoll_ctl()调用时加入的fd。

如果head和tail两个值不等,说明ring buffer里至少有一个event存在了。application需要拿到这个event,它就直接从head指向的位置读出内容,一直等到读出内容非0为止。这里实际上就是在等kernel把数据写入ring buffer。最后读出的数值就是item数组里面的一个索引值(其实是索引值+1)。把这一项的data复制出来之后,ready_events设置为0,最后head指针就自己加一指向后一项。

简单来说,代码类似下面这样:

while (header->tail == header->head)
        ;  /* Wait for an event to appear */
    while (index[header->head] == 0)
        ;  /* Wait for event to really appear */
    item = header->items + index[header->head] - 1;
    data = item->data;
    item->ready_events = 0;  /* Mark event consumed */
    header->head++;

这只是个示例,实际操作中,这里的代码应该用 C语言 的原子操作,而不是简单的read和write。这里head增加到位之后需要从0开始。不过总体来说概念应该展示得很清楚了。

这里会在看到ring buffer是空的时候,会忙等在这里,这肯定不是最优的方案。如果application觉得没有什么能做了的,就可以调用epoll_wait()来阻塞住,直到有event出现。这个调用只有在传入events数组为NULL并且maxevents是0的时候才能成功,其他情况下,epoll_wait()会阻塞住,但不会返回任何event给调用者,而很有可能会返回ESTALE,提醒说ring buffer里面有events在等待处理。这组patch set已经是第三版了,目前看来没有更多反对意见了。目前还没能进入linux-next,不过很有可能能够赶上 Linux 5.3的合入窗口。

一些感触

理解上述流程,是通过仔细研读代码才得到的。这是一个挺复杂的新API,不过几乎没有任何文档。这里导致了使用困难,不过在这之前,首先导致了API的review也很困难。很怀疑直到目前未知,除了作者,没有其他人真正试用过这组API。到底社区是否真正理解了它的实现,也是个问好。

很可惜的是,可能它会成为kernel的众多ring-buffer接口中的一个最新例子。其他的还包括perf events,ftrace,io_uring,AF_XDP,等等等等。这些接口,每一个都是从头创建出来,需要相对应的领域里的user-space开发者来仔细理解以及实现。这里是不是应该让kernel来定义一组ring buffer管理标准,专门针对user space交互场景的,这样不用每次都创建一个完全不同的一套新的机制?我们不能责备当前的patch set有这个问题,毕竟在作者开始实现的时候还没有其他参考。不过这里确实展示了Linux kernel API设计中的一个缺点,经常缺乏一个一致性很强的整体设计。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Coding the Matrix

Coding the Matrix

Philip N. Klein / Newtonian Press / 2013-7-26 / $35.00

An engaging introduction to vectors and matrices and the algorithms that operate on them, intended for the student who knows how to program. Mathematical concepts and computational problems are motiva......一起来看看 《Coding the Matrix》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

随机密码生成器
随机密码生成器

多种字符组合密码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码