当前位置: 首页 > 科技观察

Io_uring,他妈的Nio!

时间:2023-03-11 21:50:17 科技观察

大家都知道BIO效率很低,网络编程中IO多路复用一般效率更高。现在,io_uring已经可以挑战NIO了,而且功能非常强大。io_uring在2019年加入了Linux内核,目前5.1+内核都可以使用这个功能。随着一步步优化,系统调用这个大家伙,调用次数越来越少。1、性能成本在哪里?在Linux的性能指标中,有us和sy两个指标,使用top命令可以很方便的看到。us表示用户进程,sy是内核使用cpu的比例。如果进程在内核态和用户态之间切换非常频繁,那么大部分效率都会浪费在切换上。内核态和用户态的切换时间一般都在微秒级以上,可以说是非常昂贵的。cpu的性能是固定的,花在没用的东西上的浪费越少,处理实际业务的效率就越高。有两个方面影响效率。导致过多上下文切换的进程或线程数。进程由内核管理和调度,进程切换只能发生在内核态。因此,如果您的代码切换线程,则必须伴随着用户模式和内核模式之间的切换。IO的编程模型导致系统态和内核态的切换过多。比如同步阻塞等待的模型需要接收数据,处理软中断(内核态),然后唤醒用户线程(用户态),处理完后进入等待状态(内核态)。关于mmap,可以参考这篇文章。《OS近距离:mmap给你想要的快!》2.BIO可以说BIO模型在线程数量上已经爆炸,旧的编程模型已经占了所有性能低下的原因。通常,一个BIO连接对应一个线程。BIO的读写操作是阻塞的,线程的整个生命周期和连接的生命周期是一样的,不能被复用。如果有1000个连接,则需要1000个线程。线程资源非常昂贵。除了占用大量内存外,还占用大量CPU调度时间。所以当连接很多的时候,BIO的效率会变得很低。BIO的编程模型也有很多缺陷。因为是阻塞编程模式,当有数据时,内核需要通知它;当没有数据时,需要在对应的socket上阻塞等待。这两个操作涉及内核态和用户态的切换。如果数据包非常频繁,则需要一直切换BIO。3.NIO提到NIO时,Java中使用的是Epoll,Netty中使用的是改进后的Epoll。都是多路复用的,不过都习惯了,所以叫NIO。使用Reactor编程模型,可以用很少的线程来处理大量的Socket连接。一旦有新的事件到来,比如有新的连接,就可以调度主线程,向下执行程序。此时可以根据订阅事件通知不断获取订阅事件。NIO是基于事件机制的。有一个名为Selector的选择器,它阻止获取感兴趣的事件列表。拿到事件列表后,就可以通过分发器进行真正的数据操作了。熟悉Netty的同学可以看出,这个模型是Netty设计的基础。在Netty中,Boss线程对应连接的处理和调度,相当于mainReactor;Work线程对应subReactor,采用多线程的方式负责读写事件的分发和处理。NIO通过Selector选择器,将频繁的wait和notify操作集中在BIO中,大大减少了内核态和用户态的切换。当网络流量比较大的时候,Selector甚至不会阻塞,它会一直在处理数据的过程中。该模型将各个组件的职责划分得更细,耦合度更低,可以有效解决C10k问题。4.io_uring但是,NIO还有大量的系统调用,即Epoll的epoll_ctl。另外,在获取到网络事件后,还需要访问socket的数据,这也是一个系统调用。虽然和BIO相比上下文切换的次数减少了很多,但是还是需要很多时间来切换。IO只负责通知发生在fd描述符上的事件。事件的获取和通知部分是非阻塞的,但是收到通知后的操作是阻塞的。即使使用多个线程来处理这些事件,它仍然是阻塞的。如果这些系统调用可以在操作系统中完成,那么这些系统调用的时间就可以节省下来,io_uring就是为了这个。如图所示,用户态和内核态共享提交队列(submissionqueue)和完成队列(completionqueue)。这两个队列通过mmap共享,高效安全。(SQ)不断给内核分配任务,然后从另一个队列(CQ)中获取结果;内核按需执行epoll(),在线程池中执行就绪任务。用户态支持轮询方式,不发生中断,也没有系统调用,可以通过轮询方式消费事件;内核模式也支持轮询模式,不会发生上下文切换。可以看出,关键设计是内核通过一块与用户共享的内存区来传递消息,可以绕过Linux的syscall机制。rocksdb和ceph等应用程序已经在尝试这些功能。随着内核io_uring的成熟,相信网络编程的效率会更上一层楼。作者简介:品味小姐姐(xjjdog),一个不让程序员走弯路的公众号。专注于基础架构和Linux。十年架构,每天百亿流量,与你探讨高并发世界,给你不一样的滋味。