原创

Linux下的IO复用与epoll详解

全网最全 Java 知识点整合总目录入口猛戳-->www.gameboys.cn

Netty 全网最全示例源码地址猛戳-->https://github.com/Sniper2016/NettyStudy

前言

I/O 多路复用有很多种实现。在 linux 上,2.4 内核前主要是 select 和 poll,自 Linux 2.6 内核正式引入 epoll 以来,epoll 已经成为了目前实现高性能网络服务器的必备技术。尽管他们的使用方法不尽相同,但是本质上却没有什么区别。本文将重点探讨将放在 EPOLL 的实现与使用详解,至于多路复用的流程,请看上一章的 4 种 io 模型。

1.为什么会使用 epoll 代替 select?

1.1select 的缺陷

  • 高并发的核心解决方案是 1 个线程处理所有连接的“等待消息准备好”,但 select 预估错误了一件事,当数十万并发连接存在时,可能每一毫秒只有数百个活跃的连接,同时其余数十万连接在这一毫秒是非活跃的。内核中实现 select 是用轮询方法,即每次检测都会遍历所有 FD_SET 中的句柄,显然,select 函数执行时间与 FD_SET 中的句柄个数有一个比例关系,即 select 要检测的句柄数越多就会越费时。
  • 此外,在 Linux 内核中,select 所用到的 FD_SET 是有限的,即内核中有个参数__FD_SETSIZE 定义了每个 FD_SET 的句柄个数。

1.2.基准测试

接下来我们看张图,当并发连接为较小时,select 与 epoll 似乎并无多少差距。可是当并发连接上来以后,select 就显得力不从心了。

基准测试
基准测试

2.epoll 高效的奥秘

epoll 的三大关键要素:mmap、红黑树、链表

epoll 是通过内核与用户空间 mmap 同一块内存实现的。mmap 将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。内核可以直接看到 epoll 监听的句柄,效率高。

红黑树将存储 epoll 所监听的套接字。上面 mmap 出来的内存如何保存 epoll 所监听的套接字,必然也得有一套数据结构,epoll 在实现上采用红黑树去存储所有套接字,当添加或者删除一个套接字时(epoll_ctl),都在红黑树上去处理,红黑树本身插入和删除性能比较好,时间复杂度 O(logN)

Linux 底层 epoll 的 3 个实现函数:

int epoll_create(int size);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

epoll_create:创建一个 epoll 对象。参数 size 是内核保证能处理最大的文件句柄数,在 socket 编程里面就是处理的最大连接数。返回的 int 代表当前的句柄指针,当然创建一个 epoll 对象的时候,也会相应的消耗一个 fd,所以在使用完成的时候,一定要关闭,不然会耗费大量的文件句柄资源。

epoll_ctl:可以操作上面建立的 epoll,例如,将刚建立的 socket 加入到 epoll 中让其监控,或者把 epoll 正在监控的某个 socket 句柄移出 epoll,不再监控它等等。其中 epfd,就是创建的文件句柄指针,op 是要做的操作,例如删除,更新等,event 就是我们需要监控的事件。

epoll_wait:在调用时,在给定的 timeout 时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。

epoll 的高效就在于,当我们调用 epoll_ctl 往里塞入百万个句柄时,epoll_wait 仍然可以飞快的返回,并有效的将发生事件的句柄发送给用户。 这是由于我们在调用 epoll_create 时,内核除了帮我们在 epoll 文件系统里建了个 file 结点,在内核 cache 里建了个红黑树用于存储以后 epoll_ctl 传来的 socket 外,还会再建立一个 list 链表,用于存储准备就绪的事件, 当 epoll_wait 调用时,仅仅观察这个 list 链表里有没有数据即可。有数据就返回,没有数据就 sleep,等到 timeout 时间到后即使链表没数据也返回。所以,epoll_wait 非常高效。

那么,这个准备就绪 list 链表是怎么维护的呢?当我们执行 epoll_ctl 时,除了把 socket 放到 epoll 文件系统里 file 对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪 list 链表里。所以,当一个 socket 上有数据到了,内核在把网卡上的数据 copy 到内核中后就来把 socket 插入到准备就绪链表里了。 (当网卡里面有数据的时候,会发起硬件中断,提醒内核有数据到来可以拷贝数据。当网卡通知内核有数据的时候,会产生一个回调函数,这个回调函数是 epoll_ctl 创建的时候,向内核里面注册的。回调函数会把当前有数据的 socket(文件句柄)取出,放到 list 列表中。这样就可以把存放着数据的 socket 发送给用户态,减少遍历的时间,和数据的拷贝)

epoll 的数据结构如图:

epoll的数据结构如图:
epoll的数据结构如图:

epoll_wait 的工作流程:

  • epoll_wait 调用 ep_poll,当 rdlist 为空(无就绪 fd)时挂起当前进程,直到 rdlist 不空时进程才被唤醒。
  • 文件 fd 状态改变(buffer 由不可读变为可读或由不可写变为可写),导致相应 fd 上的回调函数 ep_poll_callback()被调用。
  • ep_poll_callback 将相应 fd 对应 epitem 加入 rdlist,导致 rdlist 不空,进程被唤醒,epoll_wait 得以继续执行。
  • ep_events_transfer 函数将 rdlist 中的 epitem 拷贝到 txlist 中,并将 rdlist 清空。
  • ep_send_events 函数(很关键),它扫描 txlist 中的每个 epitem,调用其关联 fd 对用的 poll 方法。此时对 poll 的调用仅仅是取得 fd 上较新的 events(防止之前 events 被更新),之后将取得的 events 和相应的 fd 发送到用户空间(封装在 struct epoll_event,从 epoll_wait 返回)。

总结:

Linux多路复用总结
Linux多路复用总结
正文到此结束