io_uring是Linux 5.1中引入的一套新的syscall接口,用于支持异步IO。近来这套机制颇受关注,很多人认为它代表了与内核实现高性能交互的一种模式。本文将对io_uring的原理和实现进行分析,了解其相对于原有IO机制的优势,并尝试预测其应用场景和发展趋势。
在介绍io_uring之前,需要先了解一下其实现的异步IO机制,以及io_uring之前的异步IO是如何实现的。
在linux下,目前绝大部分程序中的IO操作都是同步的,最后通过read/write系列的系统调用实现对文件的读写。同步IO的性能受限于文件类型和底层的设备性能,还有可能造成线程阻塞,在要求实时性的场景下显然不能满足需求。为此,出现了多种异步IO的实现机制,来解决实时IO的问题。异步IO是一种IO接口类型,调用这种类型的接口进行IO操作,可以在IO操作实际完成之前就返回,而不会阻塞等待。常见的异步IO实现方式有3种:
现有的内核AIO机制经过多年的开发,仍然存在大量的问题。内核维护者认为已有机制问题太多,使用场景太少,早期设计时没有考虑到异步IO的实际需求,在其基础上开发新特性已经没有意义。因此决定针对AIO机制开发中发现的一系列问题,从头开发一套新的异步IO机制,称作io_uring。根据笔者的理解,uring这个朴实的名字就是user和ring的意思,这两者也是io_uring机制的核心。io_uring的高效性就是建立在使用用户态(user-space)可访问的无锁环形队列(ring)的基础之上的。开发者Jens Axboe在Efficient IO with io_uring中详细介绍了io_uring的设计思想和使用方式,在Linux异步IO新时代:io_uring中对其内容做了概括,【译】高性能异步 IO — io_uring(Effecient IO with io_uring)是其中文翻译(建议参考原文阅读)。
作者在文章中明确列出了io_uring的设计目标:
io_uring的原理是让用户态进程与内核通过一个共享内存的无锁环形队列进行高效交互。相关的技术原理其实与DPDK/SPDK中的rte_ring以及virtio的vring是差不多的,只是这些技术不涉及用户态和内核态的共享内存。高性能网络IO框架netmap与io_uring技术原理更加接近,都是通过共享内存和无锁队列技术实现用户态和内核态高效交互。但上述的这些技术都是在特定场景或设备上应用的,io_uring第一次将这类技术应用到了通用的系统调用上。
为了最大程度的减少系统调用过程中的参数内存拷贝,io_uring采用了将内核态地址空间映射到用户态的方式。通过在用户态对io_uring fd进行mmap,可以获得io_uring相关的两个内核队列(IO请求和IO完成事件)的用户态地址。用户态程序可以直接操作这两个队列来向内核发送IO请求,接收内核完成IO的事件通知。IO请求和完成事件不需要通过系统调用传递,也就完全避免了copy_to_user/copy_from_user的开销。
io_uring使用了单生产者单消费者的无锁队列来实现用户态程序与内核对共享内存的高效并发访问,生产者只修改队尾指针,消费者只修改队头指针,不会互相阻塞。对于IO请求队列来说,用户态程序是生产者内核是消费者,完成事件队列则相反。需要注意的是由于队列是单生产者单消费者的,因此如果用户态程序需要并发访问队列,需要自己保证一致性(锁/CAS)。
使用共享内存和无锁队列最需要注意的就是保证内存操作的顺序和一致性。这部分内容在Efficient IO with io_uring中做了简单的介绍。简单的说就是要保证两点:
1. 修改队列状态时,必须保证对队列元素的写入已经完成。这时需要调用write barrier来保证之前的写入已经完成。在x86架构上这一点其实是针对编译器优化的,防止编译器将修改队列状态的指令放到队列元素写入完成之前。
2. 读取队列状态时,需要获取到最新写入和修改的值。这时需要调用read barrier来保证之前的写入都能被读取到,这主要是对缓存一致性的刷新。
内存屏障在不同架构的CPU上有不同的实现和效果,要正确使用需要对CPU和编译器有一定了解。在liburing中已经屏蔽了这些细节,因此建议一般情况下使用liburing来实现对队列的操作。
io_uring提供了io_uring_enter这个系统调用接口,用于通知内核IO请求的产生以及等待内核完成请求。但这种方式仍然需要反复调用系统调用,进行上下文切换,并在内核中唤醒异步处理逻辑去处理请求。显然这种方式会产生额外的开销,而且受限于系统调用速率,无法发挥IO设备的极限性能。为了在追求极致IO性能的场景下获得最高性能,io_uring还支持了轮询模式。轮询模式在DPDK/SPDK中有广泛应用,这种模式下会有一个线程循环访问队列,一旦发现新的请求和事件就立即处理。
对于用户态程序来说,轮询只需要一个线程持续访问请求完成事件队列即可。但这个层次的轮询只是轮询了io_uring的队列,但内核从IO设备获取完成情况仍然是基于设备通知的。通过在初始化时设置IORING_SETUP_IOPOLL标志,可以将io_uring配置为IO设备轮询模式。在这种模式下,调用io_uring_enter获取完成事件时,内核会使用轮询方式不断检查IO设备是否已经完成请求,而非等待设备通知。通过这种方式,能够尽可能快的获取设备IO完成情况,开始后续的IO操作。
同时,在内核中还支持了一个内核IO模式,通过IORING_SETUP_SQPOLL标志设置。在这个模式下,io_uring会启动一个内核线程,循环访问和处理请求队列。内核线程与用户态线程不同,不能在没有工作时无条件的无限循环等待,因此当内核线程持续运行一段时间没有发现IO请求时,就会进入睡眠。这段时间默认为1秒,可以通过参数sq_thread_idle设置。如果内核线程进入睡眠,会通过IO请求队列的flag字段IORING_SQ_NEED_WAKEUP通知用户态程序,用户态程序需要在有新的IO请求时通过带IORING_ENTER_SQ_WAKEUP标识的io_uring_enter调用来唤醒内核线程继续工作。
需要注意的是,如果IORING_SETUP_IOPOLL和IORING_SETUP_SQPOLL同时设置,内核线程会同时对io_uring的队列和设备驱动队列做轮询。在这种情况下,用户态程序又不需要调用io_uring_enter来触发内核的设备轮询了,只需要在用户态轮询完成事件队列即可,这样就可以做到对请求队列、完成事件队列、设备驱动队列全部使用轮询模式,达到最优的IO性能。当然,这种模式会产生更多的CPU开销。
上文提到,io_uring使用两个队列来传递IO请求和完成情况,这两个队列中的元素结构如下:
// Submission Queue Entry
struct io_uring_sqe {
__u8 opcode; //请求类型,例如IORING_OP_READV
__u8 flags; //
__u16 ioprio; //优先级,和ioprio_set系统调用的作用类似
__s32 fd; //需要操作的文件fd
__u64 off; //文件偏移位置
__u64 addr; //读写数据地址,如果是readv/writev请求则是iovec数组地址
__u32 len; //读写数据长度,如果是readv/writev请求则是iovec数组长度
union {
__kernel_rwf_t rw_flags; //请求相关的选项,其含义与对应的blocking syscall相同,例如preadv2
__u32 fsync_flags;
__u16 poll_events;
__u32 sync_range_flags;
__u32 msg_flags;
};
__u64 user_data; //使用者任意指定的字段,在复制到对应的cqe中,一般用于标识cqe与sqe的对应关系。
union {
__u16 buf_index;
__u64 __pad2[3];
};
};
// Completion Queue Event
struct io_uring_cqe {
__u64 user_data; //来自对应的sqe中的user_data字段
__s32 res; //请求处理结果,和普通IO操作的返回值差不多。一般成功时返回字节数,处理失败时返回-errno。
__u32 flags; //暂未使用
};
io_uring有3个系统调用接口,分别是:
原型为:
int io_uring_setup(unsigned entries, struct io_uring_params *params);
这个函数返回一个io_uring的fd,后续通过这个fd来操作io_uring。entries是创建出的io_uring中包含的sqe(请求)数量,必须是1-4096间的2的幂级数。io_uring_params是一个与内核的交互参数,用户态调用者在其中指定需要的参数,内核也在其中反馈实际创建的情况,其定义和解释如下:
struct io_uring_params {
__u32 sq_entries; /* IO请求sqe数量,内核输出 */
__u32 cq_entries; /* IO完成事件cqe数量,内核输出 */
__u32 flags; /* io_uring运行模式和配置,调用者输入 */
__u32 sq_thread_cpu;
__u32 sq_thread_idle;
__u32 resv[5]; /* 预留空间,用于对其cacheline,同时为将来扩展留下空间 */
struct io_sqring_offsets sq_off; /* sqe队列的偏移地址 */
struct io_cqring_offsets cq_off; /* cqe队列的偏移地址 */
};
struct io_sqring_offsets {
__u32 head; /* offset of ring head */
__u32 tail; /* offset of ring tail */
__u32 ring_mask; /* ring mask value */
__u32 ring_entries; /* entries in ring */
__u32 flags; /* ring flags */
__u32 dropped; /* number of sqes not submitted */
__u32 array; /* sqe index array */
__u32 resv1;
__u64 resv2;
};
这里需要关注的是io_sqring_offsets和io_cqring_offsets。这是内核分配的ring结构中需要用户态操作部分的相对偏移,用户态程序需要使用mmap将ring结构的内存映射到用户态来供后续交互:
struct app_sq_ring {
unsigned *head;
unsigned *tail;
unsigned *ring_mask;
unsigned *ring_entries;
unsigned *flags;
unsigned *dropped;
unsigned *array;
};
struct app_sq_ring app_setup_sq_ring(int ring_fd, struct io_uring_params *p)
{
struct app_sq_ring sqring;
void *ptr;
ptr = mmap(NULL, p→sq_off.array + p→sq_entries * sizeof(__u32),
PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
ring_fd, IORING_OFF_SQ_RING);
sring→head = ptr + p→sq_off.head;
sring→tail = ptr + p→sq_off.tail;
sring→ring_mask = ptr + p→sq_off.ring_mask;
sring→ring_entries = ptr + p→sq_off.ring_entries;
sring→flags = ptr + p→sq_off.flags;
sring→dropped = ptr + p→sq_off.dropped;
sring→array = ptr + p→sq_off.array;
return sring;
}
这里的一个疑问是为什么要让用户态程序再调用一次mmap,而不是在内核中就将地址映射好直接返回,这显然让接口的使用复杂度上升了很多。笔者认为这么做唯一的意义在于尽量保留了用户态程序对地址空间的控制力,可能在一些特殊场景下程序会需要特定的地址空间用于特殊用途,内核直接映射可能引入难以发现的问题。为了解决接口易用性的问题,liburing中封装了io_uring_queue_init接口,对于没有上述特殊需求的程序,直接使用这个接口即可。
原型为:
int io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t sig);
在程序向sqring,即请求队列中插入了IO请求后,需要通知内核开始处理,这时就需要调用io_uring_enter。参数中的fd是io_uring的fd,to_submit是提交的IO请求数。
min_complete可以用来阻塞等待内核完成特定数量的请求,前提是flags中设置IORING_ENTER_GETEVENTS。这个功能可以单独调用来等待内核处理完成。需要注意的是由于采用共享内存队列的方式来同步请求完成情况,因此程序也可以不使用这个接口而是直接判断cqring的状态来获取IO完成情况并处理cqring中的完成事件。
原型为:
int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args);
这个syscall用于支持一些高级的优化用法,主要有两种模式,opcode分别为:
上文中提到,由于io_uring要实现强大的功能和最优的效率,因此其接口和使用方式会比较复杂。但对于大部分不需要极致IO性能的场景和开发者来说,只使用io_uring的基本功能就能获得大部分的性能收益。当只需要基本功能时,io_uring的复杂接口中很大一部分是不会使用的,同时一部分初始化操作也是基本不变的。因此,io_uring的作者又开发了liburing来简化一般场景下io_uring的使用。使用liburing后,io_uring初始化时的大部分参数都不再需要填写,也不需要自己再做内存映射,内存屏障和队列管理等复杂易错的逻辑也都封装在liburing提供的简单接口中,大幅降低了使用难度。
在liburing中封装了io_uring的创建与销毁操作接口,几乎不需要指定任何参数即可完成创建:
struct io_uring ring;
io_uring_queue_init(ENTRIES, &ring, 0);
io_uring_queue_exit(&ring);
struct io_uring_sqe sqe;
struct io_uring_cqe cqe;
/* get an sqe and fill in a READV operation */
sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe, fd, &iovec, 1, offset);
/* tell the kernel we have an sqe ready for consumption */
io_uring_submit(&ring);
/* wait for the sqe to complete */
io_uring_wait_cqe(&ring, &cqe);
/* read and process cqe event */
app_handle_cqe(cqe);
io_uring_cqe_seen(&ring, cqe);
上面这段代码是io_uring作者提供的样例,其中体现了几个关键操作接口:
通过这些接口,开发者已经能够很容易的写出异步IO的代码。
上文已经介绍了io_uring的接口和基本使用方法,以及io_uring的技术原理。这里再讨论一下io_uring在内核中的实现。笔者认为,以前没有在内核中支持这类接口是有原因的,让用户态程序与内核共享内存、并发访问和修改同一数据结构,是一种危险行为。如果不能妥善的控制共享内存的操作权限,完整覆盖各种并发操作、特别是用户态异常操作的内核处理逻辑,就很可能让用户态程序能够通过这个机制破坏内核状态,造成严重错误和漏洞。因此,与技术原理相比,io_uring的具体实现更加重要,我们需要了解io_uring是如何保障其可靠性和安全性的。
在分析代码之前,我们先分析一下io_uring机制存在的风险以及可能的规避方式。
使用共享内存的ring进行同步,逻辑上并不复杂,但共享内存的结构如果损坏,内核在处理这个结构时就可能出现错误,造成内核崩溃等问题。结构损坏可能有几种:
1. ring的head/tail指针错误。这会导致内核处理没有设置过的请求sqe。由于sqe是内核预分配的内存,因此这个操作不会造成内核访问非法内存地址。如果sqe中的参数是非法的,内核会直接生成错误事件,如果参数合法,则会按参数执行IO。这种错误的影响和使用了错误的syscall参数是差不多的,对内核不会产生严重影响。
2. ring的mask/entries/flags被错误修改。由于对ring内的entry数组访问以及一些特性实现和这些元素是强相关的,如果这些内存被错误修改,就可能造成内核严重异常。但这些元素在创建io_uring时就已经确定了,内核可以单独为每个io_uring单独保存一份用于实际处理逻辑,而不使用共享内存中的部分。这样就可以避免用户态修改对内核产生影响。
3. sqe的内容在内核处理时被修改。这个情况比较复杂,理论上有可能做到对sqe的元素都只做单次访问,从而避免元素值变化造成的逻辑异常。但由于io_uring支持的操作类型和特性很丰富,可能很难实现这一点。因此更可行的方式是在处理sqe前直接复制一份,之后只访问复制的sqe参数。
io_uring的实现在fs/io_uring.c中,这是一个长达9300行的c文件。。。
SYSCALL_DEFINE2(io_uring_setup),即sys_io_uring_setup是io_uring_setup系统调用的实现函数。可以看到,在创建io_uring时,内核创建了两个数据结构io_ring_ctx和io_rings。其中io_rings就是和用户态共享内存的ring结构,包含了ring的head、tail指针,mask、entries以及sq_off、cq_off中的其他字段。而io_ring_ctx中则包含了那些不能被修改的参数。和我们在上一节分析的一样,内核将不应被修改的部分参数在内核专用的io_ring_ctx结构中保存了一份,内核实际访问的是io_ring_ctx中的元素,而不是和用户态共享的io_rings中的元素。这就避免了共享的关键参数被修改可能导致的问题。事实上,io_ring_ctx中还包含了sq_ring的head和cq_ring的tail这两个应该由内核控制的ring参数,内核只会根据io_ring_ctx中的这两个值来访问ring,并将修改后的值写回到io_rings中。因此用户态修改这些值不会对内核逻辑产生影响,而是会被内核不断修正。
SYSCALL_DEFINE6(io_uring_enter),即sys_io_uring_enter是io_uring_enter系统调用的实现函数。在主要实现逻辑io_submit_sqes函数中,会为每一个待处理的sqe分配一个io_kiocb结构,并将sqe中的请求参数逐个复制到io_kiocb中,后续真正实现io请求时访问的是io_kiocb,因此sqe被修改也不会对io处理逻辑产生影响。
值得注意的是,io_uring_enter中并不一定是完全异步化的处理sqe中的IO请求,只有当sqe中设置了REQ_F_FORCE_ASYNC时才会立即将对应的io_kiocb加入异步队列,否则会尝试启动文件的异步IO操作,只有当文件不支持异步模式时才会将其加入异步队列。这部分逻辑理解的不是很透彻,需要进一步阅读代码。
本文分析了io_uring的原理、接口、用法和实现。io_uring提供了复杂而强大的异步IO接口,同时又实现了liburing来屏蔽高级特性带来的接口复杂性和使用难度。io_uring展示了一种新的可能性,即通过共享内存的方式与内核进行高性能的交互,而避免大量的syscall带来的性能开销和限制。未来,这种模式也可以被用于加速其他实时性要求不高的系统调用。
文章浏览阅读1k次。servlet static变量是全局变量, 所有连接到服务器的用户共享一个变量,如 初始化时 static int num=1; 用户登录一次 num++; 张三 登录后 num++,此时num=2; 李四登录时 num=2;非静态变量 在 一个会话中 线程不安全 。servlet 在整个 生命周期 ,也就是打开服务器的时候,不管有多少用户请求,只在启动服务器时初始化一次,销毁是在_javax.servlet.filter 清除 thread local
文章浏览阅读350次。vue2 + elementUI + echarts1.侧边栏菜单选中后刷新页面, 保持状态2.侧边栏折叠/展开后刷新页面, 保持状态 a.将collapse:false保存在vuex的state中, 在页面调用 b.App.vue页面页面刷新时,如何保持原有vuex中的state信息 - 浅笑· - 博客园3.隐藏echarts滑轮两侧的字dataZoom-slider.textStyle. color :"transpare...
文章浏览阅读106次。6.clearfix:清除浮动 父元素中有子元素,并且父元素没有设置高度,子元素在父元素中浮动,结果必然是父元素的高度为0,这也就导致了父元素高度塌陷问题。图片保持比例放大或缩小填充容器,若不能完整填充容器,X轴或Y轴都有可能出现白边,图片不变形。图片保持比例放大或缩小使X轴与Y轴都铺满整个容器,但图片超出容器部分会被裁剪掉,图片不变形。总是X轴100%铺满整个容器,Y轴可能被裁剪会出现空白填不满部分,图片不变形。图片不保持比例放大或缩小使X轴与Y轴都铺满整个容器,图片可能会变形。..._csdn 静态商城
文章浏览阅读477次。单片机初步认识_单片机ir1是什么
文章浏览阅读8.1k次。小伙伴们如果经常使用微信聊天,发微信说说,看微信朋友圈。肯定曾经看到过一些字体带有不同的颜色,显得十分的华丽。面对这样漂亮的字体,小伙伴们看了之后怎么会不动心呢?俗话说心动不如行动,那么今天小编就来教大家如何发彩色字体!微信字体颜色修改方法第一步:我们先准备好html代码,如 安卓主题下载如下图输入第二步:等待我们发布成功后之 ,你发会现我们输入的安卓主题下载变成了红色了哦,如果你要其它色采我们也..._微信聊天红色字体代码多少
文章浏览阅读7.2k次。java json null或空字符串不传输到前台转自:http://blog.csdn.net/xieyuntestshow/article/details/38495863大家平时用java对象转json字符串。null或者空字符串属性是不需要生成到json字符串里面的。如下方式生成,没有使用的属性也会生成json字符串属性。JSONArray jsonarra_@jsonfield(name = "dataset_id") 如果是属性是null 会传为json数据不
文章浏览阅读1.1k次,点赞2次,收藏4次。个人网页设计、♂️个人简历制作、简单静态HTML个人网页作品、个人介绍网站模板 、等网站的设计与制作。个人网页设计网站模板采用DIV CSS布局制作,网页作品有多个页面,如 :个人介绍(文字页面)、我的作品(图片列表)、个人技能(图文页面)、在线留言(表单页面)CSS样式方面网页整体采用左右布局结构,制作了网页背景图片,导航区域每个导航背景色不同,导航背景色与页面背景呼应。 一套A+的网页应该包含 (具体可根据个人要求而定)网站布局方面:计划采用目前主流的、能兼容各大_用javascript写一个个人简介
文章浏览阅读202次。自动化部署脚本文件目录:运行主程序:./install.sh#!/bin/bashSCRIPTPATH=$(cd "$(dirname "$0")"; pwd)Install_log=$SCRIPTPATH/install_log.txtecho "***start install***" > $Install_logecho " workspace is $SCRIPTPATH" | te..._sh脚本授权
文章浏览阅读1.5w次,点赞3次,收藏37次。编程渣的艰难探索之Matlab绘图个人需求:用matlab绘制多条曲线并在指定不规则区域填色。为什么一定要用Matlab呢?因为我觉得图形便于修改,看起来较为正规,代码量较少。1 同时绘制多条曲线1.1 基本代码(真的很基础)x=0:0.005:5; %限定精度和范围(此处x在0到5之间取值,精度0.005)y1=x; %函数表达式1plot(x,y1,'black','k--','..._matlab画二维网格图填充颜色
文章浏览阅读747次。使用epel和remi第三方yum源,安装指定常用版本php1、前言目前的生产环境中,CentOS服务器安装php的过程是最繁琐且依赖包众多,安装起来比较麻烦,又耗时间。用CentOS 7系统自带的yum源安装php时,版本型号较旧,为php5.4版本的。 其实,我们所遇到的问题,好多大神也遇到过,并且想出了解决的方法了!那就是我们可以通过第三方yum源来实现指定安装不同版本的php,并且不会替换自带的yum源。2、安装epel、remi第三方yum源2.1、 安装第三方源epel源]# _remi-release
文章浏览阅读105次。在Centos7系统中,默认带的Python版本是Python2.7的,想用到Python3的新特性就需要升级.安装依赖包关于Python3.7以上的版本,需要多安装一个依赖包:> yum install -y libffi-devel否则会出现ModuleNotFoundError: No module named _ctypes的报错。安装python 3.7.3# 下载> wget https://www.python.org/ftp/python/3.7.3/._centos 7下如何升级python2.7到python3
文章浏览阅读285次。答案:BDCCBCAABBDAADA1617_计算机组成原理习题7