云服务器价格_云数据库_云主机【优惠】最新活动-搜集站云资讯

微软云_网站建设简介_安全稳定

小七 141 0

提交:你从未听说过的epoll替代方案

LWN上一篇关于IOCB_CMD_POLL(一种新的内核轮询接口)的文章激起了我的好奇心。本文讨论了在linuxaioapi中添加的一种新的轮询机制,该机制被合并到4.18内核中。整个想法相当有趣。该补丁的作者建议将linuxaioapi用于诸如网络套接字之类的东西。等等。Linux AIO是为异步磁盘IO而设计的!磁盘文件和网络套接字不是一回事!有没有可能在第一时间使用带有网络套接字的linuxaio API?答案是一个强烈的肯定!在本文中,我将解释如何使用linuxaioapi的优势来编写更好更快的网络服务器。但是在我们开始之前,Linux AIO到底是什么?图片来源:Scott Schiller CC/by/2.0 Linux AIO简介Linux AIO向用户空间软件公开异步磁盘IO。以前在Linux上,所有的磁盘操作都是阻塞的。无论是open()、read()、write()还是fsync(),如果磁盘缓存中没有准备好所需的数据和元数据,则可以确保线程会暂停。这通常不是问题。如果sysio调用的内存量很小,那么应该逐渐地将其填满磁盘。IO操作性能会因IO繁重的工作负载(如数据库或缓存web代理)而降低。在这样的应用程序中,如果整个服务器都停了下来,仅仅因为一些奇怪的read()系统调用必须等待磁盘,这将是一个悲剧。为了解决这个问题,应用程序使用以下三种方法之一:(1) 使用线程池并将阻塞系统调用卸载到工作线程。这就是glibc posixaio(不要与Linux AIO混淆)包装器所做的。(请参阅:IBM的文档)。这也是我们在Cloudflare应用程序中所做的——我们将read()和open()调用卸载到线程池中。(2) 用posix_fadvise(2)预热磁盘缓存,并希望达到最佳效果。(3) 在XFS文件系统中使用Linux AIO,使用O\u DIRECT打开文件,并避免未记录的陷阱。这些方法都不是完美的。即使是Linux AIO,如果使用不当,也可能会阻塞io_submit()调用。最近LWN的另一篇文章提到了这一点:Linux异步I/O(AIO)层往往有许多批评者,但很少有维护者,但大多数人至少期望它实际上是异步的。实际上,AIO操作在内核中阻塞的原因有很多,这使得AIO很难在调用线程确实无法阻塞的情况下使用。现在我们知道了哪些linuxaio API做得不好,让我们看看它在哪里闪耀。最简单的Linux AIO程序要使用LinuxAIO,首先需要定义所需的5个系统调用—glibc不提供包装器函数。要使用Linux AIO,我们需要:(1) 首先调用io_setup()来设置aio嫒上下文数据结构。内核会给我们一个不透明的指针。(2) 然后我们可以调用io_submit()来提交一个"I/O控制块"结构iocb的向量进行处理。(3) 最后,我们可以调用iogetevents()来阻塞并等待iocb的struct io_event-completion通知的向量。在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,传递给io_submit的结构iocb很大,并且针对磁盘io进行了调整。下面是一个简化版本:结构iocb{__u64数据;/*用户数据*/...__u16 aio_lio_操作码;/*见上面的IOCB_CMD*/...__u32 aio_fildes;/*文件描述符*/__u64 aio_buf;/*指向缓冲区的指针*/__u64 aio_nbytes;/*缓冲区大小*/...}从io\u getevents检索到的完成通知:struct io_事件{__u64数据;/*用户数据*/__u64 obj;/*指向请求iocb的指针*/__s64 res;/*此事件的结果代码*/__s64 res2;/*次要结果*/};我们来举个例子。以下是使用Linux AIO API读取/etc/passwd文件的最简单程序:fd=打开("/etc/passwd",仅限O);aio\u context_t ctx=0;r=io峎峈(128,&ctx);字符buf[4096];结构iocb cb={.aio_fildes=fd,.aio_lio_操作码=IOCB_CMD_PREAD,.aio_buf=(uint64_t)buf,.aio_nbytes=sizeof(buf)};结构iocb*list_iocb[1]={&cb};r=io提交(ctx,1,iocb清单);struct io\u event events[1]={0}};r=io事件(ctx,1,1,事件,空);bytes_read=事件[0].res;printf("从/etc/passwd\n读取%lld字节",bytes\u read);像往常一样,完整的源代码在github上。以下是这个项目的一个策略:打开(AT_FDCWD,"/etc/passwd",仅限O)io\U设置(128,[0x7f4fd60ea000])io_提交(0x7f4fd60ea000,1,[{aio_lio_opcode=IOCB_CMD PREAD,aio_fildes=3,aio_buf=0x7ffc5ff703d0,aio_nbytes=4096,aio_offset=0}])io\u getevents(0x7f4fd60ea000,1,1,[{data=0,obj=0x7ffc5ff70390,res=2494,res2=0}],NULL)这一切都很好!但是磁盘读取并不是异步的:io嫒submit syscall被阻塞并完成了所有的工作!io\u getevents调用立即结束。我们可以尝试使磁盘读异步,但这需要O洇DIRECT标志跳过缓存。让我们试着更好地说明iou在普通文件上提交的阻塞特性。下面是一个类似的示例,显示从/dev/zero读取大的1GiB块时的strace:io_提交(0x7fe1e800a000,1,[{aio_lio_opcode=IOCB_CMD PREAD,aio_fildes=3,aio_buf=0x7fe1a79f4000,aio_nbytes=1073741824,aio_offset=0}])\=1io\u getevents(0x7fe1e800a000,1,1,[{data=0,obj=0x7fffb9588910,res=1073741824,res2=0}],NULL)\=1内核在iounsubmit中花费了738ms,而iougetevents只花费了15us。内核的行为与网络套接字相同-所有的工作都是在io\u submit中完成的。照片来源:Helix84 CC/by-SA/3.0Linux AIO,带套接字-批处理iou-submit的实现相当保守。除非传递的说明符是O洇DIRECT file,否则它将只阻塞并执行请求的操作。如果是网络插座,则表示:对于阻塞套接字,IOCB_CMD_PREAD将挂起,直到数据包到达。对于非阻塞套接字IOCB_CMD_PREAD将-11(EAGAIN)返回代码。这些语义与vanilla read()syscall的语义完全相同。公平地说,对于网络套接字,io峎u submit并不比旧的读/写调用更聪明。需要注意的是,传递给内核的iocb请求是按顺序计算的。虽然Linux AIO对异步操作没有帮助,但它绝对可以用于syscall批处理!如果您有一个web服务器需要从数百个网络套接字发送和接收数据,那么使用io峎submit可能是个好主意。这样可以避免数百次调用send和recv。这将提高性能—从用户空间到内核的来回跳跃不是免费的,特别是在崩溃和幽灵缓解之后。一个缓冲器多个缓冲区一个文件描述符读取()读取()许多文件描述符提交+提交输入输出提交+输入输出命令为了说明io帴submit的批处理方面,让我们创建一个小程序,将数据从一个TCP套接字转发到另一个TCP套接字。在最简单的形式中,如果没有Linux AIO,程序将具有如下这样的简单流程:如果是真的:d=sd1。读取(4096)sd2.写入(d)我们可以用linuxaio来表达同样的逻辑。代码如下所示:结构iocb cb[2]={aio_fildes=sd2,.aio_lio_操作码=IOCB_CMD_PWRITE,.aio_buf=(uint64_t)和buf[0],.aio字节=0},{.aio_fildes=sd1,.aio_lio_操作码=IOCB_CMD_PREAD,.aio_buf=(uint64_t)和buf[0],.aio_nbytes=BUF_SZ}};struct iocb*list\u iocb[2]={&cb[0],&cb[1]};同时(1){r=io提交(ctx,2,iocb清单);struct io_事件事件[2]={};r=io事件(ctx,2,2,事件,空);cb[0].aio帴nbytes=事件[1].res;}此代码将两个作业提交给io_nusubmit。首先,请求向sd2写入数据,然后从sd1读取数据。读取完成后,代码会修复写入缓冲区大小并再次循环。代码做了一个很酷的技巧-第一次写入的大小是0。我们这样做是因为我们可以将写+读合并到一个io_nusubmit中(而不是读+写)。读取完成后,我们必须修复写入缓冲区的大小。这个代码比简单的读/写版本快吗?还没有。这两个版本都有两个系统调用:read+write和iounsubmit+iogetevents。幸运的是,我们可以改进它。摆脱io事件在运行io_setup()时,内核会为进程分配两页内存。此内存块在/proc//maps中的外观如下:marek:~$cat/proc/`pidof-s aio_passwd`/maps...7f7db8f60000-7f7db8f63000 rw-s 00000000:12 2314562/[aio](删除)...[aio]内存区域(在我的例子中是12KiB)是由io\u设置分配的。这个内存范围被用作存储完成事件的环形缓冲区。在大多数情况下,没有任何理由调用真正的iogetevents系统调用。完成数据可以很容易地从环形缓冲区中检索出来,而不需要咨询内核。以下是代码的固定版本:int io\u getevents(aio_u u context_t ctx,long min_nr,long max_nr,struct io_event*事件,struct timespec*timeout){积分i=0;struct aio_-ring*ring=(struct aio\u-ring*)ctx;如果(环==NULL | |环->魔法!=魔法之环){转到do_syscall;}而(i头;如果(头==环->尾){/*没有更多的完成*/休息;}其他{/*还有另一个收获*/events[i]=ring->events[head];读取屏障();响铃->头部=(头部+1)%ring->nr;i++;}}如果(i==0&&timeout!=空超时->电视秒==0超时->电视安秒==0){/*请求的非阻塞操作。*/返回0;}如果(i&&i>=最小值){返回i;}执行系统调用:return syscall(u NR_io_getevents,ctx,min_NR-i,max_NR-i,&events[i],