Linux2.6内核中epoll用法详解linux
引言编程
epoll是linux2.6内核中才有的机制,其余版本内核中是没有的,是Linux2.6内核引入的多路复用IO的一种方式,用于提升网络IO 性能的方法。在linux网络编程中,很长一段时间都是采用select来实现多事件触发处理的。Select存在以下几个方面的问题:一是每次调用时要 重复地从用户态读入参数,二是每次调用时要重复地扫描文件描述符,三是每次在调用开始时,要把当前进程放入各个文件描述符的等待队列。在调用结束后,又把 进程从各个等待队列中删除。Select采用轮询的方式来处理事件触发,当随着监听socket的文件描述符fd的数量增长时,轮询的时间也就越长,形成 效率低下。并且linux/posix_types.h中有#define __FD_SETSIZE 1024(也有说2048的)的定义,也就是说linux select能监听的最大fd数目是1024个,虽然能经过内核修改此参数,但这是治标不治本。数组
epoll的出现能够有效的解决select效率低下的问题,epoll把参数拷贝到内核态,在每次轮询时不会重复拷贝。epoll有ET和LT两种工 做模式,ET是高速模式只能以非阻塞方式进行,LT至关于快速的select,能够才有阻塞和非阻塞两种方式,epoll经过把操做拆分为 epoll_create,epoll_ctl,epoll_wait三个步骤避免重复地遍历要监视的文件描述符。服务器
Epoll介绍网络
epoll机制能够运转在两种模式下:Edge Triggered (ET)和Level Triggered (LT)。首先来看一下man手册中的一个例子:数据结构
1. 咱们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符socket
2. 这个时候从管道的另外一端被写入了2KB的数据函数
3. 调用epoll_wait(2),而且它会返回RFD,说明它已经准备好读取操做性能
4. 而后咱们读取了1KB的数据ui
5. 调用epoll_wait(2)......
Edge Triggered 工做模式:
若是咱们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)以后将有可能会挂起,因 为剩余的数据还存在于文件的输入缓冲区内,并且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工做模式才会汇报事件。所以在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄 上,由于在第2步执行了一个写操做,而后,事件将会在第3步被销毁。由于第4步的读取操做没有读空文件输入缓冲区内的数据,所以咱们在第5步调用 epoll_wait(2)完成后,是否挂起是不肯定的。epoll工做在ET模式的时候,必须使用非阻塞套接口,以免因为一个文件句柄的阻塞读/阻塞 写操做把处理多个文件描述符的任务饿死。最好如下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。
i 基于非阻塞文件句柄
ii 只有当read(2)或者write(2)返回EAGAIN时才须要挂起,等待。但这并非说每次read()时都须要循环读,直到读到产生一个 EAGAIN才认为这次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就能够肯定此时缓冲中已没有数据了,也就能够认为此事读 事件已处理完成。
Level Triggered 工做模式
相反的,以LT方式调用epoll接口的时候,它就至关于一个速度比较快的poll/select,在poll能用的地方epoll均可以用,由于他们 具备一样的职能。即便使用ET模式的epoll,在收到多个数据包的时候仍然会产生多个事件。调用者能够设定EPOLLONESHOT标志,在 epoll_wait收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。所以当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl处理文件句柄就成为调用者必须做的事情。
以上是man手册对epoll中两种模式的简要介绍,这里有必要对两种模式进行详细的介绍:
LT是缺省的工做方式,而且同时支持block和no-block socket;在这种作法中,内核会告诉调用者一个文件描述符是否就绪了,而后调用者能够对这个就绪的fd进行IO操做。若是你不做任何操做,内核仍是会 继续通知调用者的,因此,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的表明。LT模式跟select有同样的语义。就 是若是可读就触发。好比某管道原来为空,若是有一个进程写入2k数据,就会触发。若是处理进程读取1k数据,下次轮询时继续触发。该模式下,默认不可读, 只有epoll通知可读才是可读,不然不可读。
ET是高速工做方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核经过epoll告诉调用者,而后它会假设调用者知道文件描述符已经就绪,而且不会再为那个 文件描述 符发送更多的就绪通知,直到调用者作了某些操做致使那个文件描述符再也不为就绪状态了。可是请注意,若是一直不对这个fd做IO操做(从而致使它再次变成未 就绪),内核不会发送更多的通知。该模式与select有不一样的语义,只有当从不可读变为可读时才触发。上面那种状况,还有1k可读,因此不会触发,当继 续读,直到返回EAGAIN时,变为不可读,若是再次变为可读就触发。默承认读,调用者能够随便读,直到发生EAGAIN。可读时读和不读,怎么读都由调 用者本身决定,中间epoll无论。EAGAIN后不可读了,等到再次可读,epoll会再通知一次。理解ET模式最重要的就是理解状态的变化,对于监听 可读事件时,若是是socket是监听socket,那么当有新的主动链接到来为状态发生变化;对通常的socket而言,协议栈中相应的缓冲区有新的数 据为状态发生变化。可是,若是在一个时间同时接收了N个链接(N>1),可是监听socket只accept了一个链接,那么其它未 accept的链接将不会在ET模式下给监听socket发出通知,此时状态不发生变化;对于通常的socket,若是对应的缓冲区自己已经有了N字节的 数据,而只取出了小于N字节的数据,那么残存的数据不会形成状态发生变化。
Epoll的调用很简单只涉及到三个函数分别是:
1.int epoll_create(int size);
建立一个epoll的句柄,size用来告诉内核这个监听的数目最大值。这个参数不一样于select()中的第一个参数,给出最大监听的fd+1的 值。须要注意的是,当建立好epoll句柄后,它就是会占用一个fd值,因此在使用完epoll后,必须调用close()关闭,不然可能致使fd被耗 尽。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不一样与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动做,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是须要监听的fd,第四个参数是告诉内核须要监听什么事,数据结构以下:
typedef union epoll_data{ void *ptr; int fd; __uint32_t u32; __uint64_t u64 }epoll_data_t; struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
events能够是如下几个宏的集合:
EPOLLIN :表示对应的文件描述符能够读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符能够写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来讲的。
EPOLLONESHOT:只监听一次事件,当监听完此次事件以后,就会把这个fd从epoll的队列中删除。若是还须要继续监听这个socket的话,须要再次把这个fd加入到EPOLL队列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,相似于select()调用。参数events用来从内核获得事件的集合,maxevents告以内核这个events有多大,这 个maxevents的值不能大于建立epoll_create()时的size,参数timeout是超时时间(毫秒,0会当即返回,-1是永久阻 塞)。该函数返回须要处理的事件数目,如返回0表示已超时。
epoll简单例子
下面给出一个简单使用epoll的例子以加深理解
服务端代码:
#include <strings.n> #include<sys/types.h> #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> #define MAXLINE 5 #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 i, maxi, listenfd, connfd, sockfd,epfd,nfds; ssize_t n; char line[MAXLINE]; socklen_t clilen; //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件 struct epoll_event ev,events[20]; //生成用于处理accept的epoll专用的文件描述符 epfd=epoll_create(256); struct sockaddr_in clientaddr; struct sockaddr_in serveraddr; clilen=sizeof(clientaddr); listenfd = socket(AF_INET, SOCK_STREAM, 0); //把socket设置为非阻塞方式 setnonblocking(listenfd); //设置与要处理的事件相关的文件描述符 ev.data.fd=listenfd; //设置要处理的事件类型 ev.events=EPOLLIN|EPOLLET; //ev.events=EPOLLIN; //注册epoll事件 epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); bzero(&serveraddr, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; char *local_addr="127.0.0.1"; inet_aton(local_addr,&(serveraddr.sin_addr));//htons(SERV_PORT); serveraddr.sin_port=htons(SERV_PORT); bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr)); listen(listenfd, LISTENQ); maxi = 0; for ( ; ; ) { //等待epoll事件的发生 nfds=epoll_wait(epfd,events,20,500); //处理所发生的全部事件 for(i=0;i<nfds;++i) { 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); printf( "accapt a connection from %s /n",str); //设置用于读操做的文件描述符 ev.data.fd=connfd; //设置用于注测的读操做事件 ev.events=EPOLLIN|EPOLLET; //ev.events=EPOLLIN; //注册ev epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); } else if(events[i].events&EPOLLIN) //客户端socket可读事件 { recv(events[i].data.fd, line,MAXLINE,0); printf(“recv line %s /n ”, line); } } return 0; }
客户端的代码用perl写的以下:
#!/usr/bin/perl use IO::Socket; my $host = "127.0.0.1"; my $port = 5000; my socket=IO::Socket::INET−>new("host:port")ordie"createsocketerror@"; my $msg_out = "1234567890"; print socketmsg_out; print "now send over, go to sleep/n"; while (1) { sleep(1); }
同时运行服务端和客户端程序,会发现服务端在接收5字节数据以后就不会在触发EPOLLIN事件了,由于采用的是ET模式,客户端发送的10字 节数据中,只读取了5字节的数据,还有5字节数据可读,也就是状态未发生改变。因此服务端不会在触发EPOLLIN事件。而若是把ET模式改为LT模式, 那么服务端仍是会触发EPOLLIN事件,将剩余的5字节数据读取。
总结
本文主要介绍了linux epoll的使用方法,对其中的epoll的两种模式进行了详细的分析,在服务器处理中要等待用户socket链接,因为epoll的性能较高,能够有效 的处理用户请求。对于多用户链接时还要要注意在服务端accept时,有可能同时到达多个链接,因为采用ET模式,此时服务器端可能只会读取一个链接而忽 略其余链接,因此采用ET模式时应该采用while(1)这样的方式来读取链接。本文还给出了一个简单的来讲明epoll的用法,本例只是演示做用,对于 实际应用中应考虑上述多用户状况,以及采用epoll+线程池的方法。对于参考资料2中的例子,并不会按做者说的那样只输出5字节,而是在出发 EPOLLOUT以后还会出发EPOLLIN事件,也就是会出来后面的67890五个字节,对做者的这个例子研究了很久,才明白是这样的,不知道是否是没 有深入理解man的缘由,得再好好看看man.