Epoll的边缘触发ET为什么要搭配非阻塞I/O使用?
多路I/O复用有三种方法:select、poll、epoll;其中,select和poll默认采用水平触发的方式进行触发,而epoll可以选择水平触发和边缘触发,默认是水平触发。
水平触发LT
当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取
例子:假设我们的buffer设置为5个字节,某个socket的一次读事件的数据长度是9个字节,那么当epoll_wait()第一次通知处理程序去读之后,就只能读取5字节的数据,没读完,在使用水平触发的情况下,epoll_wait()就会再通知一次,让处理程序去读完剩下的4个字节。
同理,如果是14字节的话epoll_wait()就需要通知3次;如果是20字节的话epoll_wait()就需要通知4次......
LT的缺点:如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率
边缘触发ET
当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次
例子:假设我们的buffer设置为5个字节,某个socket的一次读事件的数据长度是9个字节,那么当epoll_wait()第一次通知处理程序去读之后,就只能读取5字节的数据,没读完,在边缘触发的情况下,epoll_wait()不会再通知第二次,而是选择忽略这次事件的剩余数据,那么当下一次事件的数据到来时,上一次没读完的数据会导致本次事件的数据出错。
因此我们程序要保证一次性将内核缓冲区的数据读取完,怎么保证呢?就是使用while循环,一直执行读操作。
所以,边缘触发的正规用法就是要使用循环!!!而这也是边缘触发一般与非阻塞I/O搭配的原因!!!
阻塞I/O
我们称阻塞文件描述符为阻塞I/O。Socket中可能阻塞的API:accept, recv,connect,send
- 当我们去对于一个
阻塞文件描述符
进行读取数据的时候,如果该文件描述符上没有数据可以进行读取,那么就会卡在调用的函数上面直到有数据可以进行读取。- 同样,如果我们对于一个
阻塞文件描述符号
进行写操作的时候,如果该缓冲区没有地方可写,那么就会卡在调用函数上直到有数据可写。
非阻塞I/O
我们称非阻塞文件描述符为非阻塞I/O。
当你去读写一个非阻塞的文件描述符时,不管可不可以读写,它都会立即返回,返回成功说明读写操作完成了,返回失败会设置相应errno状态码,根据这个errno可以进一步执行其他处理。它不会像阻塞IO那样,卡在那里不动
阻塞I/O和非阻塞I/O的应用
如上面的例子,我们的buffer的有5个字节,现在buffer里面是有数据的,那么不管调用的是阻塞I/O或非阻塞I/O都会完成读操作并成功返回;那如果buffer里面是没有数据的,调用非阻塞I/O时会返回error,我们可以通过判断返回值是不是error来看是不是没有数据可以读了;但是如果调用的是阻塞I/O,则没有数据读时,程序会一直卡在那一行等待读数据的到来。
ET+非阻塞I/O的原因
ET要搭配while使用才可以保证在一次触发的前提下一次性读完/写完一个事件的数据;若现在这个事件的数据有9个字节,buffer有5个字节,ET是使用的阻塞I/O,则在while中不停地调用read时,第三次read会阻塞住,然后程序就卡在那里,直到有新数据进入buffer;若ET使用的是非阻塞I/O,则会在第三次read的时候返回error,然后跳出while。