谈谈网络IO复用的理解

很多时候,我们自己所理解的东西往往并不系统,但是可以通过写Post的方式来梳理自己的知识,本篇文章主要梳理了一下网络IO中select,poll以及epoll的相关知识。

Part1: 什么是网络I/O?

相信很多开发者都不知道什么是网络I/O,可能听说过,但是并不了解,好像是一套理论上的东西,确实,因为在实际开发中很少会直接接触到自己去设计一个网络I/O模型的这种很底层的需求,但是理解好了,可以帮助我们更好的开发出高性能的应用和服务。

当一个应用程序发起一个I/O操作,而该操作是去读或者写一个远程服务器上的文件(连接)时,这个过程就产生了一次网络I/0操作。

这个反映在我们普通的开发过程中,可以在这些我们常见的场景中得到体现:

  • 处理或者发起HTTP/HTTPS请求
  • 处理Websocket长连接
  • 处理RPC请求

我们想想,当一个服务器能够支撑起单点百万QPS的时候,需要消耗多少CPU和内存呢? 有没有上限呢? 诸如此类的问题会引导我们往底层网络去理解操作系统如何来处理这么多的网络I/O请求

那么答案可能大家都已经知道了,就是我们平常所说的网络I/O模型

Part2: 网络I/O的本质是什么?

我们平时所说的HTTP连接,Rpc连接或者ws长连接,其实在我们所熟悉的Unix或者Linux中,都有一个称之为文件描述符的东西(FD),在linux中则万物都可以称之为FD。假设一个web服务器,每次通过系统调用accept来接收一个请求的时候,内核就会创建一个新的文件描述符来表示这个连接。

Part3: 如何处理网络I/O?

那么假设一个web服务器有成百上千万的请求,那么就会有成百上千万的文件描述符。这个时候假设其中有一个连接有数据流进来,需要进行处理,那怎么快速地定位到该数据呢?

最简单的就是进行遍历:

for conn := range openConnections {
    if hasEvent(conn) {
        handleEvent(conn)
    }
}

很明显,这个操作对于成百上千万的连接集合,这个性能无疑是很差的,因为你想监听一个连接的动态,就必须把所有的fd全部遍历一遍,这样的操作会浪费很多的CPU。

那有什么改进方法呢?

其实啊,我们可以换一种思路,我们不要通过内核主动去找那个文件描述符有新的数据变化,而是让每个文件描述符自己来通知内核说他们有更新。这种基于事件通知的方法是不是更加高效了呢?

Part4:对select/poll/epoll的理解

这里就涉及到另外三个系统调用方法了,分别是:poll,select以及epoll。其中epoll是linux独有的,而前面两个是任何Unix操作系统都具备的。对于selectpoll,其实他们的功能大致相同,

对于select来讲,内核通过轮询的方式来遍历所有的fd,看哪个fd有数据进来了,再通知对应的进程去处理进来的数据。

对于poll来讲,基本上是:

  1. 像内核传递一组需要监听的文件描述符列表
  2. 内核通过轮询的方式监听这些文件描述符的状态,并通知到你的应用程序

但是这也只是告诉了内核需要监听哪些文件,而内核自己依然傻傻地去遍历一遍所有地操作符,并从中找到需要监听的那些文件描述符,从本质上来说虽然大幅度过滤了一些不必要的监听操作,但是啊,效率上还是有点过不去,毕竟仍然还是全局遍历嘛。

所以,为了解决这个效率问题,epoll模型就出来了,所谓的epoll可以理解为event-poll,是一种基于事件的通知方式,当某个fd有数据更新了,才会通知内核说我这个socket有数据进来了,需要处理,此时内核基于这个事件通知(回调)到具体的应用程序来处理这个fd的数据,这个过程不需要进行轮询,也就将时间复杂度从O(n)降低为O(1)。

select来说,每个进程能监听到的fd的数量是有限的,因为这是系统定义的,可以通过命令cat /proc/sys/fs/file-max查看。而poll采用的是链表方式来存储fd,所以没有最大fd数量的限制,但是由于是链表,所以每次都需要遍历整个链表,不管这个链表上的某个fd是否需要进行更新,所以这个效率是很浪费的。而epoll则完全不同了,它像内核注册了fd的回调方法,当该fd有就绪时内核就会执行回调方法通知到具体的应用程序来处理,所以它也没有最大fd的限制,也不会造成遍历带来的性能浪费。所以啊,从效率上上,epoll的效率要远大于selectpoll

像上面提到的三种模型,select,poll以及epoll,其实啊,简单地理解,IO多路复用就是内核可以同时监听多个fd,当某个fd处于就绪状态,即已经接收好了预备数据,就能通知到某个进程来处理这些数据。但是我们知道,这个等待fd就绪地过程是一直占据着地,也就是说,这个过程是同步的,这样也就说明着三种模型本质上就是一种同步IO模型。整个fd从等待到就绪再到后续的处理写数据,都是串行的,也就是说他们是同步的IO操作。相反,异步IO就好理解了,异步IO中fd就不需要自己管理读写,待数据接收完成,只需要内核将数据拷贝到相应的进程中去就可以了。这是同步IO和异步IO的区别,如下图所示:

我们说了,epoll采用的是基于事件的方式来通知内核执行回调方法,从而激活当前fd。在激活的过程中有两种方式,一种是默认模式,我们称之为EPOLLLT,还有一种是边缘触发模式,我们称之为EPOLLET.LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。这样做的目的是为了更加高效专注地让某个进程只处理自己需要的fd数据。

Part5:如何采取采用哪种模型

就如同我们上述所说,epoll在设计上和select/poll相比,有着巨大的优势,但是epoll本身的事件回调也会对效率有所影响,其实啊,这三种模型在不同的场景下都有各自的优势,比方说,在连接数不多的情况下,selectpoll的性能并不会比epoll差,当然在大量的连接场景下,epoll还是具备明显优势的。

赞赏我吗