CDEFGAB 1010110

挖了太多坑,一点点填回来

Select Poll Epoll的区别

io, linux

在 linux 没有实现 epoll 事件驱动机制之前,我们一般选择用 select 或者 poll 等 IO 多路复用的方法来实现并发服务程序。

在大数据、高并发、集群等一些名词唱的火热之年代,select 和 poll 的用武之地越来越有限了,风头已经被 epoll 占尽。

select 和 poll IO 多路复用模型

select 的缺点:

单个进程能够监视的文件描述符的数量存在最大限制,通常是 1024,当然可以更改数量,但由于 select 采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;

内核/用户空间内存拷贝问题,select 需要复制大量的句柄数据结构,产生巨大的开销;

select 返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;

select 的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行 IO,那么之后再次 select 调用还是会将这些文件描述符通知进程。

相比于 select 模型,poll 使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

拿 select 模型为例,假设我们的服务器需要支持 100 万的并发连接,则在 _FD_SETSIZE 为 1024 的情况下,则我们至少需要开辟 1K 个进程才能实现 100 万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于 select 模型的服务器程序,要达到 10 万级别的并发访问,是一个很难完成的任务。

epoll IO多路复用模型实现机制

由于 epoll 的实现机制与 select/poll 机制完全不同,上面所说的 select 的缺点在 epoll 上不复存在。

设想一下如下场景:有 100 万个客户端同时与一个服务器进程保持着 TCP 连接。而每一时刻,通常只有几百上千个 TCP 连接是活跃的。如何实现这样的高并发?

在 select/poll 时代,服务器进程每次都把这 100 万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll 一般只能处理几千的并发连接。

epoll 的设计和实现 select 完全不同。epoll 通过在 linux 内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+ 树)。把原先的 select/poll 调用分成了 3 个部分:

1)调用 epoll_create 建立一个 epoll 对象(在 epoll 文件系统中为这个句柄对象分配资源)

2)调用 epoll_ctl 向 epoll 对象中添加这 100 万个连接的套接字

3)调用 epoll_wait 收集发生的事件的连接

如此一来,要实现上面说的场景,只需要在进程启动时建立一个 epoll 对象,然后在需要的时候向这个 epoll 对象中添加或者删除连接。同时,epoll_wait 的效率也非常高,因为调用 epoll_wait 时,并没有一股脑的向操作系统复制这 100 万个连接的句柄数据,内核也不需要去遍历全部的连接。

上面的 3 个部分非常清晰,首先要调用 epoll_create 创建一个 epoll 对象。然后使用 epoll_ctl 可以操作上面建立的 epoll 对象,例如,将刚建立的 socket 加入到 epoll 中让其监控,或者把 epoll 正在监控的某个 socket 句柄移出 epoll,不再监控它等等。

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

从上面的调用方式就可以看到 epoll 比 select/poll 的优越之处:因为后者每次调用时都要传递你所要监控的所有 socket 给 select/poll 系统调用,这意味着需要将用户态的 socket 列表 copy 到内核态,如果以万计的句柄会导致每次都要 copy 几十几百 KB 的内存到内核态,非常低效。而我们调用 epoll_wait 时就相当于以往调用 select/poll,但是这时却不用传递 socket 句柄给内核,因为内核已经在 epoll_ctl 中拿到了要监控的句柄列表。