如果说 select 模型和 poll 模型是早期的产物,在性能上有诸多不尽人意之处,那么自 Linux 2.6 之后新增的 epoll 模型,则彻底解决了性能问题,一举使得单机承受百万并发的课题变得极为容易。
现在可以这么说,只需要一些简单的设置更改,然后配合上 epoll 的性能,实现单机百万并发轻而易举。
同时,由于 epoll 整体的优化,使得之前的几个比较耗费性能的问题不再成为羁绊,所以也成为了 Linux 平台上进行网络通讯的首选模型。
讲解之前,还是 linux man 文档镇楼:linux man epoll 4 类文档 linux man epoll 7 类文档,俩文档结合着读,会对 epoll 有个大概的了解。
和之前提到的 select 和 poll 不同的是,此二者皆属于系统调用函数,但是 epoll 则不然,他是存在于内核中的数据结构。
可以通过 epoll_create,epoll_ctl 及 epoll_wait 三个函数结合来对此数据结构进行操控。
说到 epoll_create 函数,其作用是在内核中创建一个 epoll 数据结构实例,然后将返回此实例在系统中的文件描述符。
此 epoll 数据结构的组成其实是一个链表结构,我们称之为 interest list,里面会注册连接上来的 client 的文件描述符。
其简化工作机制如下:
说道 epoll_ctl 函数,其作用则是对 epoll 实例进行增删改查操作。有些类似我们常用的 CRUD 操作。
这个函数操作的对象其实就是 epoll 数据结构,当有新的 client 连接上来的时候,他会将此 client 注册到 epoll 中的 interest list 中,此操作通过附加 EPOLL_CTL_ADD 标记来实现。
当已有的 client 掉线或者主动下线的时候,他会将下线的 client从epoll 的 interest list 中移除,此操作通过附加 EPOLL_CTL_DEL 标记来实现。
当有 client 的文件描述符有变更的时候,他会将 events 中的对应的文件描述符进行更新,此操作通过附加 EPOLL_CTL_MOD 来实现。
当 interest list 中有 client 已经准备好了,可以进行 IO 操作的时候,他会将这些 clients 拿出来,然后放到一个新的 ready list 里面。
其简化工作机制如下:
说道 epoll_wait 函数,其作用就是扫描 ready list,处理准备就绪的 client IO,其返回结果即为准备好进行 IO 的 client 的个数。通过遍历这些准备好的 client,就可以轻松进行 IO 处理了。
上面这三个函数是 epoll 操作的基本函数,但是,想要彻底理解 epoll,则需要先了解这三块内容,即:inode,链表,红黑树。
在 Linux 内核中,针对当前打开的文件,有一个 open file table,里面记录的是所有打开的文件描述符信息;同时也有一个 inode table,里面则记录的是底层的文件描述符信息。
这里假如文件描述符 B fork 了文件描述符 A,虽然在 open file table 中,我们看新增了一个文件描述符 B,但是实际上,在 inode table 中,A 和 B 的底层是一模一样的。
这里,将 inode table 中的内容理解为 Windows 中的文件属性,会更加贴切和易懂。
这样存储的好处就是,无论上层文件描述符怎么变化,由于 epoll 监控的数据永远是 inode table 的底层数据,那么我就可以一直能够监控到文件的各种变化信息,这也是 epoll 高效的基础。
简化流程如下:
数据存储这块解决了,那么针对连接上来的客户端 socket,该用什么数据结构保存进来呢?
这里用到了红黑树,由于客户端 socket 会有频繁的新增和删除操作,而红黑树这块时间复杂度仅仅为 O(logN),还是挺高效的。
有人会问为啥不用哈希表呢?当大量的连接频繁的进行接入或者断开的时候,扩容或者其他行为将会产生不少的 rehash 操作,而且还要考虑哈希冲突的情况。
虽然查询速度的确可以达到 o(1),但是 rehash 或者哈希冲突是不可控的,所以基于这些考量,我认为红黑树占优一些。
客户端 socket 怎么管理这块解决了,接下来,当有 socket 有数据需要进行读写事件处理的时候,系统会将已经就绪的 socket 添加到双向链表中,然后通过 epoll_wait 方法检测的时候。
其实检查的就是这个双向链表,由于链表中都是就绪的数据,所以避免了针对整个客户端 socket 列表进行遍历的情况,使得整体效率大大提升。
整体的操作流程为:
首先,利用 epoll_create 在内核中创建一个 epoll 对象。其实这个 epoll 对象,就是一个可以存储客户端连接的数据结构。
然后,客户端 socket 连接上来,会通过 epoll_ctl 操作将结果添加到 epoll 对象的红黑树数据结构中。
然后,一旦有 socket 有事件发生,则会通过回调函数将其添加到 ready list 双向链表中。
最后,epoll_wait 会遍历链表来处理已经准备好的 socket,然后通过预先设置的水平触发或者边缘触发来进行数据的感知操作。
从上面的细节可以看出,由于 epoll 内部监控的是底层的文件描述符信息,可以将变更的描述符直接加入到 ready list,无需用户将所有的描述符再进行传入。
同时由于 epoll_wait 扫描的是已经就绪的文件描述符,避免了很多无效的遍历查询,使得 epoll 的整体性能大大提升,可以说现在只要谈论 Linux 平台的 IO 多路复用,epoll 已经成为了不二之选。
网友评论