昨天弄懂了 epoll 是干啥的,今天来研究下它是怎么实现的以及怎么用的.
# 1.select 和 poll
读了很多讲解 epoll 的文章,几乎都会提到 select 和 poll, 因为他们做的事是相同的,就是实现 IO 多路复用.
IO 多路复用简单来说就是使用一个进程同时处理多个流,在服务器上 "流" 通常是 socket. 所以 IO 多路复用的核心问题便是怎么处理这多个流,要知道是否有需要处理的流,现在需要处理哪个流.
select 和 poll 的实现便是遍历去判断每个流是否需要处理。当遇到某个流需要处理时,便唤醒处理进程.
select 的函数定义是这样子的:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); |
fd_set 是作为参数传入的,因为是系统调用,所以在调用 select 和 poll 时,会将所有监控的流的 fds 从用户内存拷贝到内核内存空间.
由于这样的实现,使得 select 和 poll 会产生以下问题:
因为需要拷贝操作和遍历,随着 fds 中的文件描述符数目增长,耗费的性能会线性增长,而且 select 监控的文件描述符有 1024 的限制.poll 解决了 1024 这一限制,但性能的问题依旧没能解决,所以最终都被 epoll 所代替了.
# 2.epoll
重点来了,因为 poll 没能解决 select 的性能问题,所以出现了 epoll.
上面提到 select 和 poll 的性能问题,主要是因为两个原因:fds 的拷贝和对 fds 的遍历,下面就来看看 epoll 如何解决这两个问题.
首先先看下 epoll 的接口:
/** 创建 epoll, 返回 epfd (epoll 会占用一个 fd) */ | |
int epoll_create(int size); | |
/** 对 epoll 监控的 fds 进行操作,op 表示操作,有增、删、改的操作,event 表示需要监听什么事件,下面具体展开 */ | |
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 不需要每次将整个 fd_set 传入,只需要将修改的告诉 epfd 对应的 epoll 就行了。而且 epoll 有一个 mmap 的内存映射机制,使得用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址,这样也避免了内存的拷贝。并且 epoll 将监控文件 fd 用红黑树的形式保存下来,进一步提高了修改监控文件集合的效率.(2.6.8 之前的内核使用 hashtable, 所以需要指定 size,2.6.8 以后 size 实际上没有什么意义了)
epoll 为了避免遍历所有监控的 fd, 引入了一个 ready_list, 保存就绪的事件,同时每个 epoll 会有一个单独的睡眠列表 (select 和 poll 在事件发生时会遍历总的睡眠列表调用回调函数). 这样大大减少了遍历操作的成本.
# 3. 总结
看完实现,我的理解大概是这样的:
把内核比作一个中转站的话,用户程序就是提货人。内核接受货物并交给用户程序进行对应操作,可能是多个.
每个提货人手里会有一份需要接受的货物清单.
select 和 poll 就是提货人把清单复印给中转站,然后中转站去挨个检查清单上的货物,若有接受到的,就检查所有等待的用户,哪个用户需要这个货物,去通知他们.
而 epoll 则为每个用户添加了一个管理员,他可以看到用户的清单,并且他能知道货物到来可能需要的用户,效率会高很多.
(上面讲的实现机制比较简单,主要参考大话 Select、Poll、Epoll, 里面十分详细~)