前言
epoll的触发模式是个引发讨论非常多的话题,网络上这方面总结的文章也很多,首先从名字上就不是很统一,LT模式常被称为水平触发、电平触发、条件触发,而ET模式常被称为边缘触发、边沿触发等,这些都是从英文翻译过来的,只不过翻译的时候有些差异,LT全称 level-triggered,ET全称 edge-triggered。
虽然这个知识点热度很高,但很多人对于它的理解总是差那么一点,特别是在面试的时候,很多面试者总是处于一种回忆和背诵的状态,其实这两种模式真的不需要去死记硬背,下面说说我个人对这两种模式的理解和记忆方法。
名称的记忆
每次提到ET(边沿触发)首先映入我脑海的是大学里《数字逻辑电路》这门课程,里面会提到低电平、高电平,当电平从低到高时会有一个上升沿,而电平从高到低时会有一个下降沿,这个“沿”就是边沿触发时提到的“边沿”,跟马路边的马路牙子是同一种概念,也就是指状态变化的时候。提起上升沿和下降沿我还是印象很深的,当时我可是占用了好几节课的时间用Verilog语言写了一个显示“HELLO WORLD”的仿真波形,依靠的就是电平变化中的“沿”。
状态变化
LT模式和ET模式可以类比电平变化来学习,但是在实际应用中概念却不是完全一样的,在epoll的应用中涉及到关于IO的读写,而读写的状态变化有哪些呢?可读、不可读、可写、不可写,其实就是这四种状态而已,以socket为例。
可读:socket上有数据
不可读:socket上没有数据了
可写:socket上有空间可写
不可写:socket上无空间可写
对于水平触发模式,一个事件只要有,就会一直触发。
对于边缘触发模式,只有一个事件从无到有才会触发。
LT模式
对于读事件 EPOLLIN
,只要socket上有未读完的数据,EPOLLIN
就会一直触发;对于写事件 EPOLLOUT
,只要socket可写(一说指的是 TCP 窗口一直不饱和,我觉得是TCP缓冲区未满时,这一点还需验证),EPOLLOUT
就会一直触发。
在这种模式下,大家会认为读数据会简单一些,因为即使数据没有读完,那么下次调用epoll_wait()时,它还会通知你在上没读完的文件描述符上继续读,也就是人们常说的这种模式不用担心会丢失数据。
而写数据时,因为使用 LT 模式会一直触发 EPOLLOUT
事件,那么如果代码实现依赖于可写事件触发去发送数据,一定要在数据发送完之后移除检测可写事件,避免没有数据发送时无意义的触发。
ET模式
对于读事件 EPOLLIN
,只有socket上的数据从无到有,EPOLLIN
才会触发;对于写事件 EPOLLOUT
,只有在socket写缓冲区从不可写变为可写,EPOLLOUT
才会触发(刚刚添加事件完成调用epoll_wait时或者缓冲区从满到不满)
这种模式听起来清爽了很多,只有状态变化时才会通知,通知的次数少了自然也会引发一些问题,比如触发读事件后必须把数据收取干净,因为你不一定有下一次机会再收取数据了,即使不采用一次读取干净的方式,也要把这个激活状态记下来,后续接着处理,否则如果数据残留到下一次消息来到时就会造成延迟现象。
这种模式下写事件触发后,后续就不会再触发了,如果还需要下一次的写事件触发来驱动发送数据,就需要再次注册一次检测可写事件。
数据的读取和发送
关于数据的读比较好理解,无论是LT模式还是ET模式,监听到读事件从socket开始读数据就好了,只不过读的逻辑有些差异,LT模式下,读事件触发后,可以按需收取想要的字节数,不用把本次接收到的数据收取干净,ET模式下,读事件触发后通常需要数据一次性收取干净。
而数据的写不太容易理解,因为数据的读是对端发来数据导致的,而数据的写其实是自己的逻辑层触发的,所以在通过网络发数据时通常都不会去注册监可写事件,一般都是调用 send
或者 write
函数直接发送,如果发送过程中, 函数返回 -1
,并且错误码是 EWOULDBLOCK 表明发送失败,此时才会注册监听可写事件,并将剩余的服务存入自定义的发送缓冲区中,等可写事件触发后再接着将发送缓冲区中剩余的数据发送出去。
代码实践
基础代码
以下为一个epoll触发模式测试的基础代码,也不算太长,直接拿来就可以测试:
1 |
|
简单说下这段代码的测试方法,可以使用 g++ testepoll.cpp -o epoll
进行编译,编译后通过 ./epoll
运行为LT模式,通过 ./epoll et
模式运行为ET模式,我们用编译好的epoll程序作为服务器,使用nc命令来模拟一个客户端。
测试分类
- 编译后直接
./epoll
,然后在另一个命令行窗口用nc -v 127.0.0.1 51741
命令模拟一次连接,此时./epoll
会产生大量的EPOLLOUT event triggered for client fd ...
,那是因为在LT模式下,EPOLLOUT
会被一直触发。
1 | albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll |
- 注释包含
EPOLLOUT event triggered for client fd
输出内容的第152行代码,编译后./epoll
运行,然后在另一个命令行窗口用nc -v 127.0.0.1 51741
模拟一次连接后,输入abcd回车,可以看到服务器./epoll
输出内容,EPOLLIN
被触发多次,每次读取一个字节。
1 | albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll |
- 还原刚才注释的那行代码,编译后执行
./epoll et
启动服务器,然后在另一个命令行窗口用nc -v 127.0.0.1 51741
模拟一次连接后,然后在另一个命令行窗口用nc -v 127.0.0.1 51741
模拟一次连接,服务器窗口显示触发了EPOLLOUT
事件
1 | albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll et |
在此基础上,从刚刚运行 nc
命令的窗口中输入回车、输入回车、输出回车,那么epoll服务器窗口看到的是触发了三次EPOLLIN
事件,每次收到一个回车:
1 | albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll et |
但是如果在nc模拟的客户端里输出abcd回车,那么在epoll服务器窗口触发一次EPOLLIN
事件接收到一个a之后便再也不会触发EPOLLIN
了,即使你在nc客户端在此输入也没有用,那是因为在接受的缓冲区中一直还有数据,新数据来时没有出现缓冲区从空到有数据的情况,所以在ET模式下也注意这种情况。
1 | albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll et |
怎么解决ET触发了一次就不再触发了
改代码呗,ET模式在连接后触发一次EPOLLOUT
,接收到数据时触发一次EPOLLIN
,如果数据没收完,以后这两个事件就再也不会被触发了,要想改变这种情况可以再次注册一下这两个事件,时机可以选择接收到数据的时候,所以可以修改这部分代码:
1 | else |
添加再次注册的逻辑:
1 | else |
这次以 ./epoll et
方式启动服务器,使用 nc -v 127.0.0.1 51741
模拟客户端,输入abc回车发现,epoll服务器输出显示触发的事件变了:
1 | albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll et |
总结
- LT模式会一直触发
EPOLLOUT
,当缓冲区有数据时会一直触发EPOLLIN
- ET模式会在连接建立后触发一次
EPOLLOUT
,当收到数据时会触发一次EPOLLIN
- LT模式触发
EPOLLIN
时可以按需读取数据,残留了数据还会再次通知读取 - ET模式触发
EPOLLIN
时必须把数据读取完,否则即使来了新的数据也不会再次通知了 - LT模式的
EPOLLOUT
会一直触发,所以发送完数据记得删除,否则会产生大量不必要的通知 - ET模式的
EPOLLOUT
事件若数据未发送完需再次注册,否则不会再有发送的机会 - 通常发送网络数据时不会依赖
EPOLLOUT
事件,只有在缓冲区满发送失败时会注册这个事件,期待被通知后再次发送
即使是在灿烂的阳光下也会有黑暗的角落,不能因为角落的阴暗就忽略阳光下的美好,我们要做的不是把黑暗面放大,而是要做阳光的传递者,哪怕是一面面镜子,通过反射来照亮那星星点点的黑暗,认清自己,不与黑暗为伍,那绝不是你自甘堕落的借口。
两千光束已然出发~
2022-4-4 18:25:50