边缘触发(Edge Triggered,简称ET)和 水平触发(Level Triggered,简称LT)是epoll两种不同的工作模式,它们在处理I/O事件时有不同的行为。
1:水平触发(LT)模式
1:只要条件满足,就会持续触发事件。例如,如果读取缓冲区有数据,epoll_wait 会持续返回EPOLLIN事件,直到缓冲区为空 。
2:编程模型相对简单,因为应用程序不需要担心是否读取或写入了所有数据 。
3:可能会导致性能问题,因为即使数据已经被读取,只要缓冲区非空,就会持续触发事件。
2:边缘触发(ET)模式
1:仅在状态发生变化时触发事件。例如,如果数据从未可读变为可读,epoll_wait 会触发一次EPOLLIN事件。
2:如果数据继续可读,除非有新数据到达,否则不会再次触发EPOLLIN 。
3:适用于高并发场景,因为它可以减少不必要的事件通知,提高效率 。
4:需要应用程序在每次通知后处理所有可用数据,否则可能会导致事件丢失 。
5:必须使用非阻塞I/O,因为如果读取或写入操作未能一次性处理完所有数据,内核不会再次通知应用程序,直到有新数据到达。
3:举个例子
1. 我们已经把一个 tcp socket 添加到 epoll 描述符。
2. 这个时候 socket 的另一端被写入了 2KB 的数据。
3. 调用 epoll_wait, 并且它会返回. 说明它已经准备好读取操作。
4. 然后调用 read, 只读取了 1KB 的数据。
5. 继续调用 epoll_wait......。
水平触发 Level Triggered 工作模式
epoll 默认状态下就是 LT 工作模式
1:当 epoll 检测到 socket 上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分。
2:如上面的例子, 由于只读了 1K 数据, 缓冲区中还剩 1K 数据, 在第二次调用epoll_wait 时, epoll_wait 仍然会立刻返回并通知 socket 读事件就绪。
3:直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回。
4:支持阻塞读写和非阻塞读写。
边缘触发 Edge Triggered 工作模式
将 socket 添加到 epoll 描述符的时候使用了 EPOLLET 标志, epoll 进入 ET 工作模式。
4:ET vs LT
1:LT 是 epoll 的默认行为。
2:使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序员一次响应就绪过程中就把所有的数据都处理完。
3:相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的。
4:另一方面, ET 的代码复杂程度更高了。
5:理解 ET 模式和非阻塞fd
使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞. 这个不是接口上的要求, 而是 "工程实践" 上的要求。
假设这样的场景: 服务器接收到一个 10k 的请求, 会向客户端返回一个应答数据.。如果客户端收不到应答, 不会发送第二个 10k 请求。
如果服务端写的代码是阻塞式的 read, 并且一次只 read 1k 数据的话(read 不能保证一次就把所有的数据都读出来, 参考 man 手册的说明, 可能被信号打断), 剩下的 9k 数据就会待在缓冲区中直到下一次客户端再给服务器写数据。epoll_wait 才能返回。
问题:
1:服务器只读到 1k 个数据, 要 10k 读完才会给客户端返回响应数据。
2:客户端要读到服务器的响应, 才会发送下一个请求。
3:客户端发送了下一个请求, epoll_wait 才会返回, 才能去读缓冲区中剩余的数据。
所以, 为了解决上述问题(阻塞 read 不一定能一下把完整的请求读完), 于是就可以使用非阻塞轮训的方式来读缓冲区, 保证一定能把完整的请求都读出来。
而如果是 LT 没这个问题。只要缓冲区中的数据没读完, 就能够让 epoll_wait 返回文件描述符读就绪。
这个例子在下一篇博客的Reactor中会有体现!
6:epoll 的使用场景
1:epoll 的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll 的性能可能适得其反。
2:对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用 epoll。
例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网 APP 的入口服务器,这样的服务器就很适合 epoll。
3:如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用epoll 就并不合适。具体要根据需求和场景特点来决定使用哪种 IO 模型。