关键区别就在于是否做重复的工作!!!
假设我现在select和epoll分别维护200个fd.
select执行过程
while True:
try:
# Get the list sockets which are ready to be read through select
read_sockets, write_sockets, error_sockets = select.select(self.conn_list, [], [])
except BaseException as e:
print e
break
在每一次while循环中, select完成如下事情:
把200个fd逐个添加到Linux内核中: 调用copy_from_user拷贝fd_set(200个fd)到Linux kernel, // 这一步如果一直重复很浪费时间
遍历所有的200个fd, 注册回调函数. 回调函数的作用就是fd有事件来的时候会唤醒处在阻塞状态的select进程. 如果遍历到的这个fd当时有事件的话, 会立刻返回当前fd可读还是可写的状态掩码, 这个状态就被添加的返回的列表中. // 这一步因为要遍历200个fd并且执行注册函数的操作, 也挺花费时间的!
遍历结束后如果没有fd可读可写, 就阻塞select所在进程持续timeout秒的时间. 阻塞过程中如果有可读可写事件随时会被唤醒.
timeout过去了, 重新遍历fd,判断有没有就绪的fd.
备注: 此处可以发现, 如果用select想要提高效率, 就得把fd_set拷贝到kernel后长期存着, 不要再每个while的循环里重复获取了. select按照这点优化一下基本上就是epoll的样子!
epoll执行过程
把200个fd逐个添加到Linux内核中(只做一次): epoll首先会给用户进程提供一个文件描述符ep_fd, 后续这个fd有事情就会通知用户进程来处理. 然后在kernel中维护了一个fd_set(用红黑树实现, 增删改查都是logN)和一个rd_llist(就绪链表). 用户态进程可以调用Epoll Control API来增减kernel中的fd_set里面的fd(用epitem作为结点, 所以epitem=fd). API具体是EPOLL_CTL_ADD添加, EPOLL_CTL_DEL删除. // 拷贝过一次就行了, 不会像select一样timeout之后还要再来走一遍这个添加过程.
注册回调函数到每个fd的等待队列上(只做一次): 这一步其实和select也没啥区别.
收集fd的事件到就绪链表: 所有拷贝到kernel并装入红黑树的fd, 如果有事件来临, 会调用ep_poll_callback函数, 导致这个fd被从黑红树中取出, 收集到内核专门的就绪链表rd_llist中. 每次执行epoll(), 其实执行的是epoll_wait()函数, 这个函数只是进去这个链表结构看一下len(rd_llist), 有的话拷贝到用户进程的内存中, 并且通知用户进程来事情了, 没有的话就继续阻塞timeout秒. 等阻塞时间读完了, epoll_wait()也只是再看一下rd_llist的长度, 没有数据的话继续blocked.
备注: 可以看出epoll其实就等于select阻塞而不要设置timeout的效果.
有高人总结: " 如此,一颗红黑树,一张准备就绪fd链表,少量的内核cache,就帮我们解决了大并发下的fd(socket)处理问题。"
参考资料
eventpoll.c源代码
https://github.com/torvalds/linux/blob/master/fs/eventpoll.c
博客对eventpoll的解析
https://blog.csdn.net/Eunice_fan1207/article/details/99674021
Python epoll API
https://docs.python.org/2/library/select.html#epoll-objects