本文共 6205 字,大约阅读时间需要 20 分钟。
IO复用之epoll()
epoll是select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符个数的限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核中的一个时间表中,这样在用户空间和内核空间的复制就只需要一次。
epoll的优点:
1.支持一个进程打开大数目的socket描述符:select能打开的描述符的数量是有一定的限制的,由FD_SETSIZE设置,默认值是1024。当然也可以选择修改宏定义来重新编译内核,但是描述符的数量增多会导致网络效率的下降。但是epoll就很好的解决了这个问题,它没有描述符数量的限制,它所支持的描述符上限是最大可以打开文件的数目,此数目远大于1024,本机为94140。可通过命令cat /proc/sys/fs/file-max来查看,一般来说,这个数目的大小和系统内存关系很大。
2.IO效率不会随着描述符数目的增加而下降:当拥有一个很大的socket集合时任一时间只有部分的socket是活跃的,但是select/poll每次调用都要线性扫描全部的集合,导致效率呈线性下降。但是epoll并不存在这个问题,它只会对活跃的socket进行操作。
3.使用mmap内存映射机制加速内核与用户空间的消息传递:内核直接将就绪队列通过mmap的方式映射到用户态,避免了拷贝内存等额外开销。
4. 内核微调
epoll的实现中每次只遍历活跃的描述符,如果是水平触发,也会遍历先前活跃的描述符,在活跃描述符较少的情况下就会很有优势。但是如果大部分描述符是活跃的,epoll的效率可能不如select或poll。
epoll相关函数:
#includeint epoll_create(int size);int epoll_ctl(int epfd,int op,struct epoll_event* ev);int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);
1.epoll_create(int size)
函数描述:返回值是一个文件描述符,即epoll是以特殊文件的方式体现给用户;size用来显示用户需要使用多少个文件描述符,该参数已经废弃,使用时只需填写一个大于0的整数即可。
功能:用来创建一个epoll实例,返回一个epoll的描述符
参数:size是用来告诉内核这个监听的数目一共有多大。当创建好epoll描述符后,它就会占用一个fd值,所以在使用完epoll后,必须关闭描述符,否则可能导致fd被耗尽。
返回值:成功,返回一个大于0的数;出错,返回-1。
2.epoll_ctl(int epfd,int op,struct epoll_event* ev)
功能:epoll的事件注册函数,用来增加或移除被epoll所监听的文件描述符,它不同于select()是在监听事件时告诉内核要舰艇什么类型的时间,而是要先注册要监听的事件类型。
参数:第一个参数epfd为epoll_creat()函数的返回值;第二个参数op表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_DEL:修改已经注册的fd的监听的事件
EPOLL_CTL_MOD:从epfd中删除一个fd
第三个参数是需要监听的fd;第四个参数是要告诉内核需要监听什么事件,其类型是一个struct epoll_event结构体:
struct epoll_event{ _uint32_t events; //epoll事件 epoll data_t data; //用户数据};
events可以是以下几个宏的集合:
EPOLLIN:表示对应的文件描述符可以读,包括对端socket正常关闭
EPOLLOUT:表示对应的文件描述符可以写
EPOLLPRI:表示对应的文件描述符有紧急的数据可读
EPOLLERR:表示对应的文件描述符发生错误
EPOLLHUP:表示对应的文件描述符被挂起
EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,与之对应的是水平触发(Level Triggered)模式
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket,需要再次把这个socket加入到EPOLL队列中
返回值:成功,返回0;出错,返回-1。
3.epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout)
功能:用来等待发生在监听描述符上的事件,类似于select()
参数:第一个参数epfd是epoll_create()函数的返回值,用来标识epoll的文件描述符;第二个参数events是一个结构体类型,用来从内核得到事件的集合;第三个参数maxevents用来告诉内核这个events有多大,通常这个maxevents的值不能大于epoll_create()时的size;第四个参数timeout表示超时时间,单位为毫秒。若设置timeout=0,则表示不等待,立即返回,若设置timeout=-1,则表示永远等待。
返回值:出错,返回-1;超时,返回0;成功,返回发生的事件的个数。
epoll工作原理:
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()函数去获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,我们只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可。
epoll精巧的使用了3个方法来实现select方法要做的事:
1.新建epoll描述符,epoll_create()
2.注册事件epoll_ctl()
3.返回活跃连接的描述符,epoll_wait()
与select相比,epoll分清了频繁调用和不频繁调用的操作。epoll_ctl()是不频繁调用,而epoll_wait()则是非常频繁调用。这比select()的效率高出很多。
LT触发模式与ET触发模式:
epoll对文件描述符的操作有两种模式:水平模式(Level Triggered)和边缘(Edge Triggered)模式。一般来说,LT模式是默认模式。其二者的区别在于:
LT模式:当epoll_wait()函数监测到描述符事件发生并将此事件通知应用程序,应用程序不立即处理该事件,当下次调用epoll_wait()函数时,会再次响应应用程序并通知此事件。使用此种模式的话,当数据可读的时候,epoll_wait()将会一直返回就绪事件。如果没有处理完全部数据,并且再次在该epoll实例上调用epoll_wait()函数监听描述符的时候,它将再次返回就绪事件,因为有数据可读。
简单理解来就是,LT是epoll的默认操作模式,当epoll_wait()函数监测到有事件发生并将通知应用程序,而应用程序不一定必须立即处理,这样,当epoll_wait()函数再次监测到此事件的时候还会通知应用程序,直到事件被处理。
ET模式:当epoll_wait()监测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件,如果不处理,下次调用epoll_wait()时,不会再次响应应用程序。使用此种模式,只能获取一次就绪通知,如果没有处理完全部数据,并且再次调用epoll_wait()函数的时候,它将会阻塞,因为就绪事件已经被释放出来了。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高很多。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件描述符的阻塞读或写操作把处理多个文件描述符的任务饿死。
而为什么ET模式要设置在非阻塞模式下工作呢?因为ET模式下的读写操作需要一直读或写直到出错(对于读,当读到的实际字节数小于请求字节数时就可以停止),而如果文件描述符不是非阻塞的,那么这个读或写势必会在最后一次阻塞,这样就不能阻塞在epoll_wait()函数上了,造成其他文件描述符的任务饥饿。
总结:LT模式是epoll缺省的工作方式,并且同时支持阻塞和非阻塞描述符。在这种做法中,内核告诉应用程序一个文件描述符是否就绪了,然后应用程序可以对这个就绪的描述符进行IO操作。如果应用程序不进行任何操作,内核还是会继续通知应用程序的,所以,这种模式下编程的时候出错的可能性要小一点。LT模式服务编写上的表现是:只要有数据没有被获取,内核就会不断通知应用程序,因此不用担心事件丢失的情况。
ET模式是告诉工作方式,只支持非阻塞描述符,它的效率要比LT更高。ET与LT的区别在于,当一个新的事件到来时,ET模式下可以从epoll_wait()调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait()函数调用中获取这个事件的。而LT模式正好相反,只要一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait()中获取这个事件。此模式效率很高,尤其是在并发、大流量的情况下,会比LT少很多epoll的系统调用,但是对编程要求极高,需要细致的处理每个请求,否则容易发生丢失事件的情况。
ET与LT的区别:
Epoll_Test1.c(LT模式)
#include#include #include #include #include #include #include #include int main(){ int epfd,nfds; //添加事件 struct epoll_event ev; //用于存放监测到的事件 struct epoll_event events[5]; //创建epoll实例 epfd = epoll_create(5); //设置事件参数 ev.data.fd = STDIN_FILENO; //设置为读事件,默认为水平触发 ev.events = EPOLLIN; //注册事件 epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&ev); while(1) { //等待事件发生,返回值是发生事件的数目 nfds = epoll_wait(epfd,events,5,-1); //遍历并处理 int i; for(i = 0;i < nfds;i++) { if(!(events[i].events & EPOLLIN)) { continue; } if(events[i].data.fd == STDIN_FILENO) { printf("Epoll Test......\n"); } } } //关闭描述符 close(epfd); return 0;}
程序会陷入死循环,因为用户输入任意数据后,数据被送入到buffer中且没有被读出,所以LT模式下每次epoll_wait()都认为buffer可读返回读就绪,导致每次都会输出“Epoll Test……”。
Epoll_Test2.c(ET模式)
#include#include #include #include #include #include #include #include int main(){ int epfd,nfds; //添加事件 struct epoll_event ev; //用于存放监测到的事件 struct epoll_event events[5]; //创建epoll实例 epfd = epoll_create(5); //设置事件参数 ev.data.fd = STDIN_FILENO; //设置为读事件,使用边缘触发 ev.events = EPOLLIN | EPOLLET; //注册事件 epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&ev); while(1) { //等待事件发生,返回值是发生事件的数目 nfds = epoll_wait(epfd,events,5,-1); //遍历并处理 int i; for(i = 0;i < nfds;i++) { if(!(events[i].events & EPOLLIN)) { continue; } if(events[i].data.fd == STDIN_FILENO) { printf("Epoll Test......\n"); } } } //关闭描述符 close(epfd); return 0;}
当我们输入一组字符时,这组字符被送到buffer中,buffer由空变为不空。此时ET返回读就绪,输出“Epoll Test……”。之后程序再次执行epoll_wait()函数,此时虽然buffer中有内容可读,但是ET并不返回描述符就绪,导致epoll_wait()阻塞。当用户再次输入一组字符时,导致buffer中的内容增多,这导致描述符状态的改变,从而使epoo_wait()返回读就绪,再次输出“Epoll Test……”。
总结:select、poll、epoll三种IO模式的比较
系统调用 | select | poll | Epoll |
事件集合 | 用户通过3个参数分别传入可读、可写及异常事件。内核通过对这些参数的在线修改来返回其中的就绪事件,这使得用户每次调用select都要重置这3个参数 | 统一处理所有事件类型,因此只需要一个事件集参数。用户通过pollfd.events传入所要监测的事件,内核通过修改pollfd.revents返回其中就绪的事件 | 内核通过一个事件表直接管理用户想要监听的所有事件,因此每次调用epoll_wait()时无需反复传入这些事件。epoll_wait()系统调用的参数events仅用来返回就绪的事件 |
应用程序索引就绪文件描述符的时间复杂度 | O(n) | O(n) | O(1) |
最大支持文件描述符数 | 1024 | 65535 | 65535 |
工作模式 | LT | LT | 支持ET高效模式 |
内核实现和工作效率 | 采用轮询方式监测就绪事件,时间复杂度为O(n) | 采用轮询方式监测就绪事件,时间复杂度为O(n) | 采用回调方式监测就绪事件为O(1) |
转载地址:http://grvm.baihongyu.com/