使用epoll多路复用编程时,会用epoll_wait阻塞等待事件的发生,对应有边沿触发和水平触发两种工作模式。
一、水平触发(EPOLLLT)
水平触发:只要缓冲区有数据,epoll_wait就会一直被触发,直到缓冲区为空;
水平触发(EPOLLLT)是epoll默认的工作模式,其优缺点如下:
优点:保证了数据的完整输出;
缺点:当数据较大时,需要不断从用户态和内核态切换,消耗了大量的系统资源,影响服务器性能;
应用场景:应用较少,一般用于连接请求较少及客户端发送的数据量较少的服务器,可一次性接收所有数据。此时,若使用边沿触发,会多调用一次accept/read等来判断数据是否为空。
二、边沿触发(EPOLLET)
边沿触发:只有所监听的事件状态改变或者有事件发生时,epoll_wait才会被触发;
epoll边沿触发时,假设一个客户端发送100字节的数据,而服务器设定read每次读取20字节,那么一次触发只能读取20个字节,然后内核调用epoll_wait直到下一次事件发生,才会继续从剩下的80字节读取20个字节,由此可见,这种模式其工作效率非常低且无法保证数据的完整性,因此边沿触发不会单独使用。
边沿触发通常与非阻塞IO一起使用,其工作模式为:epoll_wait触发一次,在while(1)循环内非阻塞IO读取数据,直到缓冲区数据为空(保证了数据的完整性),内核才会继续调用epoll_wait等待事件发生。
边沿触发(EPOLLET)+非阻塞IO的优缺点如下:
优点:每次epoll_wait只用触发一次,就可以读取缓冲区的所有数据,工作效率高,大大提升了服务器性能;
缺点:数据量很小时,至少需要调用两次非阻塞IO函数,而边沿触发只用调用一次。
三、什么时候用边沿触发?什么时候用水平触发?
我的答案是:任何情况都应该优先选择“边沿触发(EPOLLET)+非阻塞IO”模式。
理由如下:根据以上水平触发和边沿触发的分析,毋庸置疑,当服务端连接请求多且数据量大的时候,应该选择“边沿触发+非阻塞IO”模式,因为只用触发一次epoll_wait,就可以读取缓冲区的所有数据。那么连接请求少而且数据量也小的时候偶,为什么也优先选择边沿触发+非阻塞IO呢?在我看来,既然数据量小,那么服务端性能要求自然也不高,即使非阻塞IO读取数据多了一次判断数据为空的情况,但是其影响也不大,而且边沿触发也能满足接收大量数据的情况。
四、水平触发是否需要使用非阻塞IO?
答案是:不管水平触发还是边沿触发,都要使用非阻塞IO。
理由如下:假设水平触发使用阻塞read读取数据,且设定一次性读取20字节,现在假设客户端只发送了10个字节,那么服务端内核就会阻塞在read调用中,等待客户端再发送至少10个字节的数据,才能返回继续执行程序。但是服务端已经阻塞在系统调用read中了,无法再调用epoll_wait来监听该客户端的下一次就绪事件,也就无法接受数据,read也不可能再达到20字节了,从而就形成死锁,因此水平触发也要使用非阻塞IO。
五、服务器项目中遇到的问题
写服务器的第一个版本时,使用的是“边沿触发(EPOLLET)+非阻塞IO”模式,但是只调用了一次IO,没有循环遍历直到数据为空。这样就产生了一个问题,如下:如果给了1000个连接请求,在60S时间内,但是实际接收的连接数不到一半。这是因为每次触发只调用一次IO时,一次只能accept一个连接请求,那么需要不断的连接请求触发,才能继续accept连接,效率非常低。但是使用了while循环遍历直至数据为空之后,同样的测试,服务器能接收全部的连接请求,其原因就是一次触发就可以处理该次触发所接收的所有连接请求,大大减少了epoll_wait系统调用,减小了内核资源消耗。