概述:
本文介绍epoll的水平触发和边缘触发模式的具体工作原理,以及处理多线程下的线程安全问题
假定读者了解epoll以及其系统调用epoll_create, epoll_ctl, epoll_wait的基本使用
struct epoll_event ev;//声明一个事件,一个事件由事件类型(可读?可写?触发模式?)和数据(data.fd指定这个事件所从属的目标文件描述符)组成 ev.events = EPOLLIN | EPOLLET;//位运算注册事件类型: EPOLLIN(可读),EPOLLOUT(可写), EPOLLET(ET模式), EPOLLLT (LT模式)
LT(水平触发)
LT是默认的工作模式,对于采用LT的文件描述符,在水平触发模式下,如果某个文件描述符上有数据可读,内核会持续通知应用程序,直到应用程序处理完数据或者缓冲区不再有数据可读为止。当调用epoll_wait检测到其上有事件发生并通知应用程序时,应用程序可以不立即处理完毕该事件。这样,当程序下一次调用epoll_wait时,epoll_wait还会向应用程序通知此事件,直到事件被处理完毕
应用场景:解析http报文,分次解析http报文
ET(边缘触发)
ET比LT效率高,对于使用ET模式的文件描述符,在边缘触发模式下,只有在文件描述符状态变化(对于EPOLLIN事件,只有在状态从未就绪变化为就绪,才叫做变化)发生时才会触发事件,所以调用epoll_wait检测到其上有事件发生,并通知应用程序,应用程序必须立即处理完毕该事件,否则会造成数据丢失,因为后续的epoll_wait调用不再重复向应用程序通知此事件
应用场景:读取大型文件,用while循环一次全部读取完毕
while (1) { char buffer[10] = {0}; int count = recv(events[i].data.fd, buffer, 10, 0); if (count < 0) {//读取完毕或当前没有数据可读或者出错 if( (errno == EAGAIN) || (errno == EWOULDBLOCK)) {//读取完毕 printf("recv finished\n"); break; } //recv出错 close(events[i].data.fd);//关闭事件的套接字 break; } else if (count == 0) {//对方断开连接 epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, &ev);//移除该事件 close(events[i].data.fd);//关闭事件的套接字 break; } else {//接收到数据 printf("recv from clientfd: %d, count: %d, data: %s", events[i].data.fd, count, buffer); } }
此外,使用ET模式的文件描述符应该是非阻塞的,否则会阻塞在epoll_wait();
总的来说,
在水平触发模式下,应用程序需要持续处理就绪的文件描述符,直到文件描述符上不再有数据可读为止。相比之下,在边缘触发模式下,应用程序只会在状态变化时收到通知,而不会持续收到通知直到处理完数据。
重点来了:
考虑一种情况:在ET模式下,一个文件描述符上有数据可读(EPOLLIN触发),线程开始处理数据,而在处理过程中又有新数据加入。当旧数据开始处理时,文件描述符仍然保持在就绪状态。但当有新的数据写入时,文件描述符会从就绪状态变为未就绪状态,然后再次变为就绪状态,触发一次新的 EPOLLIN 事件。这样可以确保即使在旧数据处理过程中有新的数据写入,应用程序也能及时地得到通知,并读取新的数据。在单线程上,此时只要程序循环读取数据就不会造成新数据的丢失,而对于多线程就需要特殊处理(详见下EPOLLONESHOT)
在 LT 模式下,新数据的写入不会改变文件描述符的状态码。如果文件描述符上有数据可读,它的状态码会一直保持就绪状态,直到所有的数据都被读取完毕才会变为未就绪。即使在读取数据的过程中有新的数据写入,文件描述符的状态码仍然会保持就绪状态,不会因为新数据的写入而改变
EPOLLONESHOT
即使使用了ET模式,一个socket上的事件还是可能被触发多次。
例如:当一个线程处理一个socket时有新数据写入,此时另外一个线程被唤醒读取这些数据,于是出现了两个线程同时操作一个socket的情况。EPOLLONESHOT解决了多个线程同时操作一个socket的问题,对于注册了EPOLLONESHOT的事件,操作系统最多触发其上注册的一个可读可写异常事件,且只触发一次。这样,一个线程在操作这个socket时,其他线程不可能有机会操作该socket。但反过来思考,注册了EPOLLONESHOT事件的socket,一旦被某个线程处理完毕,要及时修改为EPOLLIN或其他事件,以确保下次这个socket可读时,其事件能够被触发,进而让其他线程有机会继续处理