IO多路复用( 六 )

< nfds; ++n) { // 处理所有发生IO事件的fd
process_event(events[n].data.fd);
// 如果有必要,可以利用epoll_ctl继续对本fd注册下一次监听,然后重新epoll_wait
}
}

此外,epoll的手册中也有一个简单的例子 。
所有的基于IO多路复用的代码都会遵循这样的写法:注册——监听事件——处理——再注册,无限循环下去 。
epoll的优势
为什么epoll的性能比和poll要强呢?和poll每次都需要把完成的fd列表传入到内核,迫使内核每次必须从头扫描到尾 。而epoll完全是反过来的 。epoll在内核的数据被建立好了之后,每次某个被监听的fd一旦有事件发生,内核就直接标记之 。调用时,会尝试直接读取到当时已经标记好的fd列表,如果没有就会进入等待状态 。
同时,直接只返回了被触发的fd列表,这样上层应用写起来也轻松愉快,再也不用从大量注册的fd中筛选出有事件的fd了 。
简单说就是和poll的代价是"O(所有注册事件fd的数量)",而epoll的代价是"O(发生事件fd的数量)" 。于是,高性能网络服务器的场景特别适合用epoll来实现——因为大多数网络服务器都有这样的模式:同时要监听大量(几千,几万,几十万甚至更多)的网络连接,但是短时间内发生的事件非常少 。
但是,假设发生事件的fd的数量接近所有注册事件fd的数量,那么epoll的优势就没有了,其性能表现会和poll和差不多 。
epoll除了性能优势,还有一个优点——同时支持水平触发(Level )和边沿触发(Edge ) 。
水平触发和边沿触发
默认情况下,epoll使用水平触发,这与和poll的行为完全一致 。在水平触发下,epoll顶多算是一个“跑得更快的poll” 。
而一旦在注册事件时使用了标记(如上文中的例子),那么将其视为边沿触发(或者有地方叫边缘触发,一个意思) 。那么到底什么水平触发和边沿触发呢?
考虑下图中的例子 。有两个的fd——fd1和fd2 。我们设定监听f1的“水平触发读事件“,监听fd2的”边沿触发读事件“ 。我们使用在时刻t1,使用监听他们的事件 。在时刻t2时,两个fd都到了数据,于是在时刻t3,返回了两个fd进行处理 。在t4,我们故意不读取所有的数据出来,只各自读 。然后在t5重新注册两个事件并监听 。在t6时,只有fd1会返回,因为fd1里的数据没有读完,仍然处于“被触发”状态;而fd2不会被返回,因为没有新数据到达 。
水平触发和边沿触发
这个例子很明确的显示了水平触发和边沿触发的区别 。
那么为什么需要边沿触发呢?
边沿触发把如何处理数据的控制权完全交给了开发者,提供了巨大的灵活性 。比如,读取一个http的请求,开发者可以决定只读取http中的数据就停下来,然后根据业务逻辑判断是否要继续读(比如需要调用另外一个服务来决定是否继续读) 。而不是次次被尚有数据的状态烦扰;写入数据时也是如此 。比如希望将一个资源A写入到 。当的充足时,会返回这个fd是准备好的 。但是资源A此时不一定准备好 。如果使用水平触发,每次经过也总会被打扰 。在边沿触发下,开发者有机会更精细的定制这里的控制逻辑 。
但不好的一面时,边沿触发也大大的提高了编程的难度 。一不留神,可能就会miss掉处理部分数据的机会 。如果没有很好的根据来“重置”一个fd,就会造成此fd永远没有新事件产生,进而导致饿死相关的处理代码 。