io_submit:Linux内核新加入的epoll替代方案


在Linux内核4.18版本中新添加了一种新的内核轮询接口Linux AIO 方法IOCB_CMD_POLL。该补丁由Christoph Hellwig,还提议将Linux AIO接口配合网络套接字等一起使用。

Linux的AIO是最初是设计用于磁盘异步IO的接口。文件与网络套接字是大相径庭的东西,可以用Linux AIO 接口,将其统一起来呢?

在本文中,我们介绍如何使用Linux AIO API的优势来编写更好,更快的网络服务器。

Linux AIO简介

我们先来介绍一下Linux AIO。

io_submit:Linux内核新加入的epoll替代方案

Linux AIO用于将异步磁盘IO暴露给用户空间。在Linux上所有磁盘操作都是阻塞的。无论是open(),read(),write()还是fsync(),如果所需要的数据和元数据还没有在磁盘缓存准备好,则线程就会挂住。通常这不是问题。如果做少量的IO操作或者内存足够大,则磁盘syscall会逐渐填充高速缓存,这样整体来说速度还是会很快。

但是对于IO繁重的工作负载(例如数据库或缓存Web代理),IO操作性能会下降很多。在这类应用中,很可能由于一些read()系统调用等待磁盘导致卡顿那是致命的。

io_submit:Linux内核新加入的epoll替代方案

要解决此类问题,通常变通使用如下方法:

使用线程池并将卸载的系统调用到工作线程。这就是glibc POSIX AIO(不要与Linux AIO混淆)包装器的作用。

posix_fadvise使用预热磁盘缓存,并希望达到最佳效果。

将Linux AIO与XFS文件系统一起使用,使用O_DIRECT打开文件,并避免出现未说明的陷阱。

这些变通的方法都不是完美的。

即使不小心使用Linux AIO,也可能会阻塞io_submit()调用。

Linux异步I/O(AIO)自从产生以来争议就很多,大多数人希望的至少是异步的,实际上也没有实现。但是由于种种原因,AIO操作可能会在内核中阻塞,从而使AIO在调用线程确实无法承受的情况下难以使用。

最简单的AIO示例

要使用Linux AIO,首先需要定所需的系统调用。glibc不提供包装函数。要使用Linux AIO,需要:

首先调用io_setup()以设置aio_context数据结构。内核提供了一个不透明的指针。

然后调用io_submit()提交一个"I/O控制块"向量结构体 iocb进行处理。

最后,调用io_getevents() 块并等待一个向量结构体 io_event-iocb的完成通知。

一个iocb中可以提交8个命令。4.18内核中引入了两个读取,两个写入,两个fsync变体和一个POLL命令:

IOCB_CMD_PREAD = 0,

IOCB_CMD_PWRITE = 1,

IOCB_CMD_FSYNC = 2,

IOCB_CMD_FDSYNC = 3,

IOCB_CMD_POLL = 5, /* 4.18 */

IOCB_CMD_NOOP = 6,

IOCB_CMD_PREADV = 7,

IOCB_CMD_PWRITEV = 8,

在iocb结构体传递到io_submit,调整为磁盘IO。这是一个简化的版本:

struct iocb {

__u64 data; /* 用户数据 */

...

__u16 aio_lio_opcode; /* IOCB_CMD_ */

...

__u32 aio_fildes; /* 文件描述符 */

__u64 aio_buf; /* 缓冲指针 */

__u64 aio_nbytes; /* 缓冲大小*/

...

}

从io_getevents以下位置检索完成通知:

struct io_event {

__u64 data; /* 用户数据 */

__u64 obj; /* iocb请求指针 */

__s64 res; /* 事件结果码*/

__s64 res2; /* 第二结果*/

};


让我们举一个简单的例子,使用Linux AIO API读取/etc/passwd文件:

io_submit:Linux内核新加入的epoll替代方案

fd = open("/etc/passwd", O_RDONLY);

aio_context_t ctx = 0;

r = io_setup(128, &ctx);

char buf[4096];

struct iocb cb = {.aio_fildes = fd,

.aio_lio_opcode = IOCB_CMD_PREAD,

.aio_buf = (uint64_t)buf,

.aio_nbytes = sizeof(buf)};

struct iocb *list_of_iocb[1] = {&cb};

r = io_submit(ctx, 1, list_of_iocb);

struct io_event events[1] = {{0}};

r = io_getevents(ctx, 1, 1, events, NULL);

bytes_read = events[0].res;

printf("read %lld bytes from /etc/passwd ", bytes_read);

我们用strace追踪程序的执行

io_submit:Linux内核新加入的epoll替代方案

这一切都OK!但磁盘读取并非异步的,io_submit系统调用被阻止并完成了所有工作!io_getevents通话瞬间完成。

我们可以尝试使磁盘异步读取,但这需要O_DIRECT标志来跳过缓存。

让举另一个更好能说明io_submit普通文件的阻止性质。从文件中读取大的1GiB块时显示strace /dev/zero:

io_submit(0x7fe1e800a000, 1, [{aio_lio_opcode=IOCB_CMD_PREAD, aio_fildes=3, aio_buf=0x7fe1a79f4000, aio_nbytes=1073741824, aio_offset=0}]) = 1 <0.738380>

io_getevents(0x7fe1e800a000, 1, 1, [{data=0, obj=0x7fffb9588910, res=1073741824, res2=0}], NULL) = 1 <0.000015>

内核中io_submit花费738ms,只有15us io_getevents消耗的。内核的行为与网络套接字相同,所有工作都在io_submit中完成。

用Linux AIO处理Socket

io_submit的处理相当保守。除非传递的描述符是O_DIRECT文件,否则它将阻塞并执行请求的操作。对于网络套接字:

对于阻塞套接字,IOCB_CMD_PREAD将挂起,直到数据包到达为止。

对于非阻塞套接字,IOCB_CMD_PREAD返回-11(EAGAIN)。

这些语法与read()系统调用完全相同。对于网络套接字而言,io_submit并不比旧式读/写调用更好用。

重要的是要注意iocb传递给内核的请求是按顺序进行的。

尽管Linux AIO不能帮助异步操作,但它绝对可以用于系统调用批处理。如果有一个Web服务器需要从数百个网络套接字发送和接收数据,那么使用io_submit是个好主意。这可以避免不必的send和recv数百次调用,将提高性能。从用户空间和内核之间来回跳转是需要耗时的。

io_submit:Linux内核新加入的epoll替代方案

为了说明的io_submit批处理,我们创建一个小程序,将数据从一个TCP套接字转发到另一个。最简单的形式,如果没有Linux AIO,该程序将像下面这样简单:

while True:

d = sd1.read(4096)

sd2.write(d)


我们可以使用Linux AIO表达相同的逻辑。该代码将如下:

struct iocb cb[2] = {{.aio_fildes = sd2,

.aio_lio_opcode = IOCB_CMD_PWRITE,

.aio_buf = (uint64_t)&buf[0],

.aio_nbytes = 0},

{.aio_fildes = sd1,

.aio_lio_opcode = IOCB_CMD_PREAD,

.aio_buf = (uint64_t)&buf[0],

.aio_nbytes = BUF_SZ}};

struct iocb *list_of_iocb[2] = {&cb[0], &cb[1]};

while(1) {

r = io_submit(ctx, 2, list_of_iocb);


struct io_event events[2] = {};

r = io_getevents(ctx, 2, 2, events, NULL);

cb[0].aio_nbytes = events[1].res;

}

以上代码向io_submit提交两个作业。首先,请求将数据写入sd2,然后从sd1中读取数据。读取完成后,代码将确定写缓冲区的大小并再次循环。该代码用了一个很酷的技巧:第一次写入大小为0。之所以这样做,是因为我们可以在一个io_submit中融合写入+读取(但不能读取+写)。读取完成后,我们必须修复写入缓冲区的大小。

这代码比简单的读/写版本快吗?还不是。

两个版本都有两个系统调用:read + write和io_submit + io_getevents。

但是我们改善它。

摆脱io_getevents

当运行io_setup()时,内核为该进程分配几页内存。这是此内存块在/proc/ /maps中的样子:

cat /proc/`pidof -s aio_passwd`/maps

...

7f7db8f60000-7f7db8f63000 rw-s 00000000 00:12 2314562 /[aio] (deleted)

...

内存区域由io_setup分配。内存范围用于存储完成事件的环形缓冲区。在大多数情况下,没有任何理由对io_geteventssyscall真正的调用。可以从环形缓冲区轻松检索完成数据,而无需请求内核。下面是一个无需内核调用的修订版本:

int io_getevents(aio_context_t ctx, long min_nr, long max_nr,

struct io_event *events, struct timespec *timeout)

{

int i = 0;

struct aio_ring *ring = (struct aio_ring*)ctx;

if (ring == NULL || ring->magic != AIO_RING_MAGIC) {

goto do_syscall;

}

while (i < max_nr) {

unsigned head = ring->head;

if (head == ring->tail) {

break;

} else {

/* There is another completion to reap */

events[i] = ring->events[head];

read_barrier();

ring->head = (head + 1) % ring->nr;

i++;

}

}

if (i == 0 && timeout != NULL && timeout->tv_sec == 0 && timeout->tv_nsec == 0) {

return 0;

}


if (i && i >= min_nr) {

return i;

}


do_syscall:

return syscall(__NR_io_getevents, ctx, min_nr-i, max_nr-i, &events[i], timeout);

}


通过此代码修复了该io_getevents功能, Linux AIO版本的TCP代理每个循环仅只需要一个系统调用,并且读写版本代码快一点。

替代Epoll

通过在内核4.18中添加IOCB_CMD_POLL,io_submit还可以用于替代select/poll/epoll。例如,以下代码在Socket监听等待数据:

struct iocb cb = {.aio_fildes = sd,

.aio_lio_opcode = IOCB_CMD_POLL,

.aio_buf = POLLIN};

struct iocb *list_of_iocb[1] = {&cb};

r = io_submit(ctx, 1, list_of_iocb);

r = io_getevents(ctx, 1, 1, events, NULL);

strace追踪显示:

io_submit(0x7fe44bddd000, 1, [{aio_lio_opcode=IOCB_CMD_POLL, aio_fildes=3}]) = 1 <0.000015>

io_getevents(0x7fe44bddd000, 1, 1, [{data=0, obj=0x7ffef65c11a8, res=1, res2=0}], NULL) = 1 <1.000377>

如上,该程序"异步"部分运行良好,io_submit立即完成,io_getevents等待数据成功阻塞了1秒钟。这是非常强大的功能,可以代替epoll_wait()系统调用使用。

另外,通常情况下,epoll杂项处理需要epoll_ctl系统调用。应用程序开发人员竭尽全力避免过多地调用调用。使用io_submit轮询可以解决整个的复杂工作,并且过程中不需要任何虚假的系统调用。只需将套接字推给iocb请求向量,只调用io_submit一次并等待完成即可。

总结

本文中,我们回顾了Linux AIO接口。尽管最初被认为是仅用于磁盘的接口API,但它似乎与网络套接字上的常规读/写系统调用的工作方式相同。但是与读/写不同的是,它允许系统调用批处理io_submit,从而可以提高性能。从4.18版内核开始io_submit,io_getevents可用于等待网络套接字上的事件如POLLIN和POLLOUT,可以用于替代epoll()事件循环。

展开阅读全文

页面更新:2024-04-13

标签:内核   缓冲区   线程   缓存   磁盘   接口   两个   版本   代码   操作   文件   方案   数据   工作   系统   科技   网络

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top