前言
为什么会考虑到深入理解多路复用?采用多路复用技术能把多个信号组合起来在一条物理信道上进行传输,在远距离传输时可大大节省电缆的安装和维护费用。在Http/2,Redis等内容中,反复提到多路复用带来的效率提升,也只有了解了基础概念,才能掌握它们,一步一步来吧。
计算机如何接收数据?
计算机执行程序时,会有优先级的需求。网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。
这一步,贯穿网卡、中断、进程调度的知识,叙述阻塞recv()下,内核接收数据全过程。(不论是客户还是服务器应用程序都用recv()函数从TCP连接的另一端接收数据),进程在recv()阻塞期间,计算机收到了对端传送的数据(①)。数据经由网卡传送到内存(②),然后网卡通过中断信号通知CPU有数据到达,CPU执行中断程序(③)。此处的中断程序主要有两项功能,先将网络数据写入到对应socket的接收缓冲区里面(④),再唤醒进程A(⑤),重新将进程A放入工作队列中。
select的流程
sock1、sock2和sock3三个socket,那么在调用select之后,操作系统把进程A分别加入这三个socket的等待队列中。当任何一个socket收到数据后,中断程序将唤起进程。所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面。经由这些步骤,当进程A被唤醒后,它知道至少有一个socket接收了数据。程序只需遍历一遍socket列表,就可以得到就绪的socket。这种简单方式行之有效,在几乎所有操作系统都有对应的实现。
缺点:
每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次。
epoll细节
当某一进程调用 epoll_create 方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
结语
多路复用最重要的知识点是因为内部用了一个红黑树记录添加的socket的数据结构,用了一个双向链表接收内核触发的事件。就是因为多了这个存储,可以直接拿到就绪socket,而不用像select那样一个个检查。
推荐阅读