前言
在这个系列的上一篇文章中,我们介绍过网络IO中的两种IO模型——BIO与NIO,但这两种模型各有缺陷。
BIO将系统资源浪费在了线程调度上,而NIO将系统资源浪费在了用户态与内核态的切换上。
为了解决上面这两种IO模型的缺点,又提出了多路复用器的概念。
多路复用器,根据工作模式的不同,分为select/poll和epoll。
多路复用器的原理
要聊多路复用器,首先要明白“路”是指什么。
我们先来看一张图:
这张图左边是NIO的工作模式,右边是多路复用器的工作模式。
而其中的“路”就是IO通道,是这个用户程序需要监听的所有socket。
无论是NIO还是多路复用器,都需要知道在监听的所有socket中,有状态更新的是哪些,从而对这些socket进行处理。
而NIO与多路复用器的区别在于如何知道那些有状态更新的sockets。
与NIO的对比
在上一篇文章中我们知道,用户程序是通过fd
(file descriptor)与sockets打交道的,也就是说,fd是socket在用户程序中的抽象。
在NIO中,要想知道哪些socket有更新,就需要用户程序对每个socket进行状态检查,注意,是从用户程序中对每个socket进行检查。
这样的检查方式有两个缺点:
- 对每一个socket的检查都要单独调用一次系统函数。也就是说,如果应用程序有N个正在监听的socket,就会调用N次系统函数。
- 对所有监听的socket都进行了检查,但是真正有状态的更新可能很少的几个,资源被浪费在无效的检查上。
而多路复用器的出现就是为了解决第一个问题,多路复用器只需要一次系统调用,就能够知道有状态更新的socket是哪些。
select/poll
select
和poll
是同一类型的多路复用器,区别仅在于能够监听的fd上限。
下面我们以select为例,讲解一下这种多路复用器的工作模式。
要使用select多路复用器,通过系统函数select(fds)
即可。
该函数的参数就是一个fd的集合,函数的返回值是参数中有状态更新的fd集合。
所以如果应用程序所监听的socket中,有多少有状态更新,只需要调用一次系统调用,将遍历工作委托给内核。
内核在自己内部遍历所有已经注册的socket,然后将有状态更新的fds一块儿返回给应用程序。
也就是说,在应用程序的角度看,通过多路复用器,只需要O(1)的时间复杂度就能知道有哪些socket有状态更新。
而同样的事情,在NIO模型中需要O(N)的时间复杂度,其中N是所监听的所有socket数。
但这种多路复用器有它们自己的问题。
select/poll的问题
这种多路复用器主要有两个问题:
- 每次进行系统调用的时候,都要将所有正在监听的socket的fd传递给系统函数,用户态与内核态之间数据传输量会很大。
- 每次还是要全量遍历所有正在监听的socket,仍然有很多无用的系统消耗。
为了解决这两个问题,有了下面这种多路复用器。
epoll
既然已经知道上面的那种多路复用器存在的问题,那么我们来想想解决思路是什么。
既不想给内核传递需要检查的socket名单,又想让内核不遍历所有的socket,还想让内核把有状态更新的socket返回用户程序。
那么解决方案就是——让内核自己维护两个容器,一个装着正在监听的所有的socket,一个装着有状态更新的socket。
这正是epoll的工作原理!
知识储备
在我们详细介绍epoll之前,需要讲一些计算机原理方面的知识。
先提一个问题,当有数据到达网卡后,发生了什么?
答案是,当数据到达网卡后,网卡会向CPU发出IO中断。
什么是中断?中断是一个打断当前CPU的机制,中断中有一个中断函数。
CPU读到中断指令后,会暂时中断当前正在进行的任务,转而执行中断函数。
所以网卡中断CPU后,会让CPU来网卡里取出数据,并将数据放在内存中与这个客户端对应的buffer中。
epoll的底层实现
上面我们提到了,当网卡接收到数据后,在CPU的协调下,将数据存放在该客户端所对应的socket中的buffer中。
而epoll在这个中断函数中新添加了一个操作,将数据存放进内存之后,还有后续的操作。
就是将这个被更新socket放在某个容器中,这样应用程序就可以直接通过这个容器知道哪些socket有状态更新了。
并且epoll相对于select/poll,在底层实现中多了两个新东西——一个红黑树还有一个链表。
这两个容器,都存在了进程的共享空间中。
这个红黑树就是来存放要监听的所有socket的,
这个链表就是用来存放有状态更新的socket的,也就是在IO中断后,有状态更新的socket会被放进这个容器中。
epoll的系统调用
不同于select和poll只有一个系统调用,epoll的调用有三个。
分别是
epoll_create:在内核中开辟一个空间来放置红黑树,会返回一个fd,代表着红黑树的内存区域。
epoll_ctl:将要监听的socket以及它的事件注册进红黑树中。
epoll_wait:将有状态更新的socket返回给应用程序,这个系统调用相当于select。
介绍完两种多路复用器的基本原理,我们来看看如何在代码中应用。
多路复用器的实践代码(java)
虽然在操作系统的角度来看,多路复用器分成两类