epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select和poll。目前流行的高性能web服务器Nginx正式依赖于epoll提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。
为何这样说?下面一张图总结select,poll,epoll的区别:
他们属于不同时期产物,新技术当然是更加好用。
epoll机制
- select出现是1984年在BSD里面实现的。
- 14年之后也就是1997年才实现了poll,其实拖那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求。
- 2002, 大神 Davide Libenzi 实现了epoll。
epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,相对于select来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
Linux中提供的epoll相关函数如下:#includeint epoll_create(int size); 函数功能: 创建epoll专用文件描述符的缓冲区。 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 函数功能: 添加、删除、修改 监听文件描述符。 函数参数: int epfd epoll专用的文件描述符 int op 操作命令。EPOLL_CTL_ADD EPOLL_CTL_MOD EPOLL_CTL_DEL int fd 要操作文件描述符 struct epoll_event *event 存放监听文件描述符信息的结构体 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 函数功能: 等待事件发生。 函数参数: int epfd epoll专用的文件描述符 struct epoll_event *events : 存放产生事件的文件描述结构体。 int maxevents :最大监听的数量. int timeout :等待事件ms单位. <0 >0 ==0 返回值: 产生事件的数量。 typedef union epoll_data { void *ptr; int fd; //文件描述符 uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; epoll_data_t data; };
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
- **水平触发(LT):**默认工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件
- 边缘触发(ET): 当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。(直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时只通知一次)。
LT和ET原本应该是用于脉冲信号的,可能用它来解释更加形象。Level和Edge指的就是触发点,Level为只要处于水平,那么就一直触发,而Edge则为上升沿和下降沿的时候触发。比如:0->1 就是Edge,1->1 就是Level。
ET模式很大程度上减少了epoll事件的触发次数,因此效率比LT模式下高。
epoll基本处理流程- 应用创建Epoll对象
- 应用将关心对象的Epoll文件描述符及关注的事件以及发生的事件后的回调处理注册给Epoll对象
- 应用在阻塞点等待事件的发生,Epoll对象注册的关注集中任何对象发生改变都将结束阻塞点的等待
epoll 的使用非常简单,只有下面 3 个系统调用。
epoll_createepollctlepollwait
就这?是的,就这么简单。
- epollcreate 负责创建一个池子,一个监控和管理句柄 fd 的池子;
- epollctl 负责管理这个池子里的 fd 增、删、改;
- epollwait 就是负责打盹的,让出 CPU 调度,但是只要有“事”,立马会从这里唤醒;
Linux 下,epoll 一直被吹爆,作为高并发 IO 实现的秘密武器。其中原理其实非常朴实:epoll 的实现几乎没有做任何无效功。 我们从使用的角度切入来一步步分析下。
首先,epoll 的第一步是创建一个池子。这个使用 epoll_create 来做:
原型:
int epoll_create(int size);
示例:
epollfd = epoll_create(1024);if (epollfd == -1) { perror("epoll_create"); exit(EXIT_FAILURE);}
这个池子对我们来说是黑盒,这个黑盒是用来装 fd 的,我们暂不纠结其中细节。我们拿到了一个 epollfd ,这个 epollfd 就能唯一代表这个 epoll 池。注意,这里又有一个细节:用户可以创建多个 epoll 池。
然后,我们就要往这个 epoll 池里放 fd 了,这就要用到 epoll_ctl 了
原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
示例:
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, 11, &ev) == -1) { perror("epoll_ctl: listen_sock"); exit(EXIT_FAILURE);}
epoll函数使用案例: 应用再TCP客户端
#include总结#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //消息结构体 struct MSG_DATA { char type; //消息类型. 0表示有聊天的消息数据 1表示好友上线 2表示好友下线 char name[50]; //好友名称 int number; //在线人数的数量 unsigned char buff[100]; //发送的聊天数据消息 }; struct MSG_DATA msg_data; #define MAX_EVENTS 10 struct epoll_event ev, events[MAX_EVENTS]; int epollfd; int nfds; //文件接收端 int main(int argc,char **argv) { if(argc!=4) { printf("./app <端口号> <名称>n"); return 0; } int sockfd; //忽略 SIGPIPE 信号--方式服务器向无效的套接字写数据导致进程退出 signal(SIGPIPE,SIG_IGN); sockfd=socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in addr; addr.sin_family=AF_INET; addr.sin_port=htons(atoi(argv[2])); // 端口号0~65535 addr.sin_addr.s_addr=inet_addr(argv[1]); //IP地址 if(connect(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr_in))!=0) { printf("客户端:服务器连接失败.n"); return 0; } msg_data.type=1; strcpy(msg_data.name,argv[3]); write(sockfd,&msg_data,sizeof(struct MSG_DATA)); int cnt; int i; //创建专用文件描述符 epollfd = epoll_create(10); //添加要监听的文件描述符 ev.events = EPOLLIN; ev.data.fd = sockfd; epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev); ev.events = EPOLLIN; ev.data.fd = 0; //标准输入文件描述符 epoll_ctl(epollfd, EPOLL_CTL_ADD, 0, &ev); while(1) { //监听事件 nfds=epoll_wait(epollfd,events,MAX_EVENTS,-1); if(nfds) { for(i=0;i 0) { if(msg_data.type==0) { printf("%s:%s 在线人数:%dn",msg_data.name,msg_data.buff,msg_data.number); } else if(msg_data.type==1) { printf("%s 好友上线. 在线人数:%dn",msg_data.name,msg_data.number); } else if(msg_data.type==2) { printf("%s 好友下线. 在线人数:%dn",msg_data.name,msg_data.number); } } } else if(events[i].data.fd==0) //表示键盘上有数据输入 { gets(msg_data.buff); //读取键盘上的消息 msg_data.type=0; //表示正常消息 strcpy(msg_data.name,argv[3]); //名称 write(sockfd,&msg_data,sizeof(struct MSG_DATA)); } } } } SERVER_ERROR: close(sockfd); return 0; }
大量framework进阶学习;可请往Android核心进阶学习在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。



