高效epoll原理
来源:互联网 发布:深圳证券交易所软件 编辑:程序博客网 时间:2024/05/20 10:20
epoll简介
epoll是什么?按照man手册的说法:是为处理大批量句柄而作了改进的poll。当然,这不是2.6内核才有的,它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44),它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll常规用法
epoll相关的系统调用有3个:epoll_create, epoll_ctl和epoll_wait。在头文件<sys/epoll.h>
中被声明
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
epoll_create用于建立一个epoll fd。参数size是内核保证能够正确处理的最大文件描述符数目(现在内核使用红黑树组织epoll相关数据结构,不再使用这个参数)。
epoll_ctl可以操作上面建立的epoll fd,例如,将刚建立的socket fd加入到epoll中让其监控,或者把 epoll正在监控的某个socket fd移出epoll,不再监控它等等。
epoll_wait在调用时,在给定的timeout时间内,当在监控的这些文件描述符中的某些文件描述符上有事件发生时,就返回用户态的进程。
两种工作模式
LT(level triggered,水平触发)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET (edge-triggered,边缘触发)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。
/* 使用 epoll 写的回射服务器 将从client中接收到的数据再返回给client */ #include <iostream> #include <sys/socket.h> #include <sys/epoll.h> #include <netinet/in.h> #include <arpa/inet.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <errno.h> using namespace std; #define MAXLINE 100 #define OPEN_MAX 100 #define LISTENQ 20 #define SERV_PORT 5000 #define INFTIM 1000 void setnonblocking(int sock) { int opts; opts=fcntl(sock,F_GETFL); if(opts<0) { perror("fcntl(sock,GETFL)"); exit(1); } opts = opts|O_NONBLOCK; if(fcntl(sock,F_SETFL,opts)<0) { perror("fcntl(sock,SETFL,opts)"); exit(1); } } int main(int argc, char* argv[]) { int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber; ssize_t n; char line[MAXLINE]; socklen_t clilen; string szTemp(""); if ( 2 == argc ) { if( (portnumber = atoi(argv[1])) < 0 ) { fprintf(stderr,"Usage:%s portnumber\a\n",argv[0]); return 1; } } else { fprintf(stderr,"Usage:%s portnumber\a\n",argv[0]); return 1; } //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件 struct epoll_event ev, events[20]; //创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大 epfd = epoll_create(256); //生成用于处理accept的epoll专用的文件描述符 struct sockaddr_in clientaddr; struct sockaddr_in serveraddr; listenfd = socket(AF_INET, SOCK_STREAM, 0); //把socket设置为非阻塞方式 //setnonblocking(listenfd); //设置与要处理的事件相关的文件描述符 ev.data.fd=listenfd; //设置要处理的事件类型 ev.events=EPOLLIN|EPOLLET; //注册epoll事件 epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); bzero(&serveraddr, sizeof(serveraddr)); /*配置Server socket的相关信息 */ serveraddr.sin_family = AF_INET; char *local_addr="127.0.0.1"; inet_aton(local_addr,&(serveraddr.sin_addr));//htons(portnumber); serveraddr.sin_port=htons(portnumber); bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr)); listen(listenfd, LISTENQ); maxi = 0; for ( ; ; ) { //等待epoll事件的发生 //返回需要处理的事件数目nfds,如返回0表示已超时。 nfds=epoll_wait(epfd,events,20,500); //处理所发生的所有事件 for(i=0; i < nfds; ++i) { //如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。 if(events[i].data.fd == listenfd) { connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); if(connfd < 0) { perror("connfd < 0"); exit(1); } //setnonblocking(connfd); char *str = inet_ntoa(clientaddr.sin_addr); cout << "accapt a connection from " << str << endl; //设置用于读操作的文件描述符 ev.data.fd=connfd; //设置用于注册的读操作事件 ev.events=EPOLLIN|EPOLLET; //注册ev epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); /* 添加 */ } //如果是已经连接的用户,并且收到数据,那么进行读入。 else if(events[i].events&EPOLLIN) { cout << "EPOLLIN" << endl; if ( (sockfd = events[i].data.fd) < 0) continue; if ( (n = recv(sockfd, line, sizeof(line), 0)) < 0) { // Connection Reset:你连接的那一端已经断开了,而你却还试着在对方已断开的socketfd上读写数据! if (errno == ECONNRESET) { close(sockfd); events[i].data.fd = -1; } else std::cout<<"readline error"<<std::endl; } else if (n == 0) //读入的数据为空 { close(sockfd); events[i].data.fd = -1; } szTemp = ""; szTemp += line; szTemp = szTemp.substr(0,szTemp.find('\r')); /* remove the enter key */ memset(line,0,100); /* clear the buffer */ //line[n] = '\0'; cout << "Readin: " << szTemp << endl; //设置用于写操作的文件描述符 ev.data.fd=sockfd; //设置用于注册的写操作事件 ev.events=EPOLLOUT|EPOLLET; //修改sockfd上要处理的事件为EPOLLOUT epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); /* 修改 */ } else if(events[i].events&EPOLLOUT) // 如果有数据发送 { sockfd = events[i].data.fd; szTemp = "Server:" + szTemp + "\n"; send(sockfd, szTemp.c_str(), szTemp.size(), 0); //设置用于读操作的文件描述符 ev.data.fd=sockfd; //设置用于注册的读操作事件 ev.events=EPOLLIN|EPOLLET; //修改sockfd上要处理的事件为EPOLIN epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); /* 修改 */ } } //(over)处理所发生的所有事件 } //(over)等待epoll事件的发生 close(epfd); return 0; }
epoll为什么高效(相比select)
select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。
此外,内核使用了slab机制,为epoll提供了快速的数据结构:
epoll向内核注册了一个文件系统,用于存储上述的被监控的fd。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的fd,这些fd会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。
epoll的第三个优势在于:当我们调用epoll_ctl往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把fd放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个fd的中断到了,就把它放到准备就绪list链表里。所以,当一个fd(例如socket)上有数据到了,内核在把设备(例如网卡)上的数据copy到内核中后就来把fd(socket)插入到准备就绪list链表里了。
一颗红黑树,一张准备就绪fd链表,少量的内核cache,就帮我们解决了大并发下的fd(socket)处理问题
- 执行epoll_create时,创建了红黑树和就绪list链表。
- 执行epoll_ctl时,如果增加fd(socket),则检查在红黑树中是否存在,存在立即返回,不存在则添加到红黑树上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪list链表中插入数据。
- 执行epoll_wait时立刻返回准备就绪链表里的数据即可。
- 高效epoll原理
- epoll的高效实现原理
- epoll的高效实现原理
- epoll的高效实现原理
- epoll的高效实现原理
- epoll的高效实现原理
- 高效的epoll
- epoll为什么这么高效?
- epoll原理
- epoll原理
- epoll原理
- epoll原理
- epoll原理
- epoll原理
- linux下epoll如何实现高效处理百万句柄的--原理剖析
- epoll之一:epoll的原理
- 为什么epoll 比 select 高效
- 简单高效epoll网络模型
- 如何实现单向链表的首尾添加
- 记第一次使用git上传本地文件到github
- 【MySQL基础】视图
- Hough变换理解
- kmp算法浅析
- 高效epoll原理
- SRM588 Div1Medium KeyDungeonDiv1
- Android启动页面应该有的样子
- dpkg 被中断问题解决方法
- DAY2
- android保存数据到手机内存以及SD卡
- Matplotlib入门
- java网络编程及简单聊天
- python——文件、os模块、异常,连接mysql