select,poll,epoll都是IO多路复用的机制。IO多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步IO,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步IO则无需自己负责进行读写,异步IO的实现会负责把数据从内核拷贝到用户空间。一、select实现1。1。基本概念 IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。 IO多路复用适用如下场合:当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用IO复用。当一个客户同时处理多个接口时,而这种情况是可能的,但很少出现。如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到IO复用。如果一个服务器即要处理TCP,又要处理UDP,一般要使用IO复用。如果一个服务器要处理多个服务或多个协议,一般要使用IO复用。 与多进程和多线程技术相比,IO多路复用技术的最大优势是系统开销小,系统不必创建进程线程,也不必维护这些进程线程,从而大大减小了系统的开销。1。2。select函数 该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下:includesysselect。hincludesystime。hintselect(intmaxfdp1,fdsetreadset,fdsetwriteset,fdsetexceptset,conststructtimevaltimeout)返回值:就绪描述符的数目,超时返回0,出错返回1 函数参数介绍如下: (1)第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此把该参数命名为maxfdp1),描述字0、1、2。。。maxfdp11均将被测试。 因为文件描述符是从0开始的。 (2)中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个条件不感兴趣,就可以把它设为空指针。structfdset可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:voidFDZERO(fdsetfdset);清空集合voidFDSET(intfd,fdsetfdset);将一个给定的文件描述符加入集合之中voidFDCLR(intfd,fdsetfdset);将一个给定的文件描述符从集合中删除intFDISSET(intfd,fdsetfdset);检查集合中指定的文件描述符是否可以读写 (3)timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。structtimeval{longtvsec;secondslongtvusec;microseconds}; 更多LInux内核视频教程文档资料免费领取后台私信【内核】自行获取。 这个参数有三种可能: (1)永远等待下去:仅在有一个描述字准备好IO时才返回。为此,把该参数设置为空指针NULL。 (2)等待一段固定时间:在有一个描述字准备好IO时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。 (3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。1。3。测试程序 写一个TCP回射程序,程序的功能是:客户端向服务器发送信息,服务器接收并原样发送给客户端,客户端显示出接收到的信息。 服务端程序如下:1includestdio。h2includestdlib。h3includestring。h4includeerrno。h5includenetinetin。h6includesyssocket。h7includesysselect。h8includesystypes。h9includenetinetin。h10include11includeunistd。h12include1314defineIPADDR127。0。0。115definePORT878716defineMAXLINE102417defineLISTENQ518defineSIZE101920typedefstructservercontextst21{22intclicnt;客户端个数23intclifds〔SIZE〕;客户端的个数24fdsetallfds;句柄集合25intmaxfd;句柄最大值26}servercontextst;27staticservercontextstssrvctxNULL;282930staticintcreateserverproc(constcharip,intport)31{32intfd;33structsockaddrinservaddr;34fdsocket(AFINET,SOCKSTREAM,0);35if(fd1){36fprintf(stderr,createsocketfail,erron:d,reason:s,37errno,strerror(errno));38return1;39}4041一个端口释放后会等待两分钟之后才能再被使用,SOREUSEADDR是让端口释放后立即就可以被再次使用。42intreuse1;43if(setsockopt(fd,SOLSOCKET,SOREUSEADDR,reuse,sizeof(reuse))1){44return1;45}4647bzero(servaddr,sizeof(servaddr));48servaddr。sinfamilyAFINET;49inetpton(AFINET,ip,servaddr。sinaddr);50servaddr。sinporthtons(port);5152if(bind(fd,(structsockaddr)servaddr,sizeof(servaddr))1){53perror(binderror:);54return1;55}5657listen(fd,LISTENQ);5859returnfd;60}6162staticintacceptclientproc(intsrvfd)63{64structsockaddrincliaddr;65socklentcliaddrlen;66cliaddrlensizeof(cliaddr);67intclifd1;6869printf(accpetclintprociscalled。);7071ACCEPT:72clifdaccept(srvfd,(structsockaddr)cliaddr,cliaddrlen);7374if(clifd1){75if(errnoEINTR){76gotoACCEPT;77}else{78fprintf(stderr,acceptfail,error:s,strerror(errno));79return1;80}81}8283fprintf(stdout,acceptanewclient:s:d,84inetntoa(cliaddr。sinaddr),cliaddr。sinport);8586将新的连接描述符添加到数组中87inti0;88for(i0;iSIZE;i){89if(ssrvctxclifds〔i〕0){90ssrvctxclifds〔i〕clifd;91ssrvctxclicnt;92break;93}94}9596if(iSIZE){97fprintf(stderr,toomanyclients。);98return1;99}100101}102103staticinthandleclientmsg(intfd,charbuf)104{105assert(buf);106printf(recvbufis:s,buf);107write(fd,buf,strlen(buf)1);108return0;109}110111staticvoidrecvclientmsg(fdsetreadfds)112{113inti0,n0;114intclifd;115charbuf〔MAXLINE〕{0};116for(i0;issrvctxclicnt;i){117clifdssrvctxclifds〔i〕;118if(clifd0){119continue;120}121判断客户端套接字是否有数据122if(FDISSET(clifd,readfds)){123接收客户端发送的信息124nread(clifd,buf,MAXLINE);125if(n0){126n0表示读取完成,客户都关闭套接字127FDCLR(clifd,ssrvctxallfds);128close(clifd);129ssrvctxclifds〔i〕1;130continue;131}132handleclientmsg(clifd,buf);133}134}135}136staticvoidhandleclientproc(intsrvfd)137{138intclifd1;139intretval0;140fdsetreadfdsssrvctxallfds;141structtimevaltv;142inti0;143144while(1){145每次调用select前都要重新设置文件描述符和时间,因为事件发生后,文件描述符和时间都被内核修改啦146FDZERO(readfds);147添加监听套接字148FDSET(srvfd,readfds);149ssrvctxmaxfdsrvfd;150151tv。tvsec30;152tv。tvusec0;153添加客户端套接字154for(i0;issrvctxclicnt;i){155clifdssrvctxclifds〔i〕;156去除无效的客户端句柄157if(clifd!1){158FDSET(clifd,readfds);159}160ssrvctxmaxfd(clifdssrvctxmaxfd?clifd:ssrvctxmaxfd);161}162163开始轮询接收处理服务端和客户端套接字164retvalselect(ssrvctxmaxfd1,readfds,NULL,NULL,tv);165if(retval1){166fprintf(stderr,selecterror:s。,strerror(errno));167return;168}169if(retval0){170fprintf(stdout,selectistimeout。);171continue;172}173if(FDISSET(srvfd,readfds)){174监听客户端请求175acceptclientproc(srvfd);176}else{177接受处理客户端消息178recvclientmsg(readfds);179}180}181}182183staticvoidserveruninit()184{185if(ssrvctx){186free(ssrvctx);187ssrvctxNULL;188}189}190191staticintserverinit()192{193ssrvctx(servercontextst)malloc(sizeof(servercontextst));194if(ssrvctxNULL){195return1;196}197198memset(ssrvctx,0,sizeof(servercontextst));199200inti0;201for(;iSIZE;i){202ssrvctxclifds〔i〕1;203}204205return0;206}207208intmain(intargc,charargv〔〕)209{210intsrvfd;211初始化服务端context212if(serverinit()0){213return1;214}215创建服务,开始监听客户端请求216srvfdcreateserverproc(IPADDR,PORT);217if(srvfd0){218fprintf(stderr,socketcreateorbindfail。);219gotoerr;220}221开始接收并处理客户端请求222handleclientproc(srvfd);223serveruninit();224return0;225err:226serveruninit();227return1;228} 客户端程序如下:1includenetinetin。h2includesyssocket。h3includestdio。h4includestring。h5includestdlib。h6includesysselect。h7includetime。h8includeunistd。h9includesystypes。h10includeerrno。h1112defineMAXLINE102413defineIPADDRESS127。0。0。114defineSERVPORT87871516definemax(a,b)(ab)?a:b1718staticvoidhandlerecvmsg(intsockfd,charbuf)19{20printf(clientrecvmsgis:s,buf);21sleep(5);22write(sockfd,buf,strlen(buf)1);23}2425staticvoidhandleconnection(intsockfd)26{27charsendline〔MAXLINE〕,recvline〔MAXLINE〕;28intmaxfdp,stdineof;29fdsetreadfds;30intn;31structtimevaltv;32intretval0;3334while(1){3536FDZERO(readfds);37FDSET(sockfd,readfds);38maxfdpsockfd;3940tv。tvsec5;41tv。tvusec0;4243retvalselect(maxfdp1,readfds,NULL,NULL,tv);4445if(retval1){46return;47}4849if(retval0){50printf(clienttimeout。);51continue;52}5354if(FDISSET(sockfd,readfds)){55nread(sockfd,recvline,MAXLINE);56if(n0){57fprintf(stderr,client:serverisclosed。);58close(sockfd);59FDCLR(sockfd,readfds);60return;61}6263handlerecvmsg(sockfd,recvline);64}65}66}6768intmain(intargc,charargv〔〕)69{70intsockfd;71structsockaddrinservaddr;7273sockfdsocket(AFINET,SOCKSTREAM,0);7475bzero(servaddr,sizeof(servaddr));76servaddr。sinfamilyAFINET;77servaddr。sinporthtons(SERVPORT);78inetpton(AFINET,IPADDRESS,servaddr。sinaddr);7980intretval0;81retvalconnect(sockfd,(structsockaddr)servaddr,sizeof(servaddr));82if(retval0){83fprintf(stderr,connectfail,error:s,strerror(errno));84return1;85}8687printf(clientsendtoserver。);88write(sockfd,helloserver,32);8990handleconnection(sockfd);9192return0;93} 4、程序结果 启动服务程序,执行三个客户程序进行测试,结果如下图所示: select的调用过程如下所示:(1)使用copyfromuser从用户空间拷贝fdset到内核空间(2)注册回调函数pollwait(3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sockpoll,sockpoll根据情况会调用到tcppoll,udppoll或者datagrampoll)(4)以tcppoll为例,其核心实现就是pollwait,也就是上面注册的回调函数。(5)pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcppoll来说,其等待队列是sksksleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。(6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fdset赋值。(7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用scheduletimeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(scheduletimeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。(8)把fdset从内核空间拷贝到用户空间。 总结: select的几大缺点:每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大select支持的文件描述符数量太小了,默认是1024二,poll实现 poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fdset结构,其他的都差不多。 1、基本知识 poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。 2。2。poll函数 函数格式如下所示:includepoll。hintpoll(structpollfdfds,unsignedintnfds,inttimeout); pollfd结构体定义如下:structpollfd{intfd;文件描述符shortevents;等待的事件shortrevents;实际发生了的事件}; 每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。合法的事件如下:POLLIN有数据可读。POLLRDNORM有普通数据可读。POLLRDBAND有优先数据可读。POLLPRI有紧迫数据可读。POLLOUT写数据不会导致阻塞。POLLWRNORM写普通数据不会导致阻塞。POLLWRBAND写优先数据不会导致阻塞。POLLMSGSIGPOLL消息可用。此外,revents域中还可能返回下列事件: POLLER指定的文件描述符发生错误。POLLHUP指定的文件描述符挂起事件。POLLNVAL指定的文件描述符非法。 这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。 使用poll()和select()不一样,你不需要显式地请求异常情况报告。 POLLINPOLLPRI等价于select()的读事件,POLLOUTPOLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORMPOLLRDBAND,而POLLOUT则等价于POLLWRNORM。例如,要同时监视一个文件描述符是否可读和可写,我们可以设置events为POLLINPOLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。 timeout参数指定等待的毫秒数,无论IO是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好IO的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。 返回值和错误代码 成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回1,并设置errno为下列值之一:EBADF一个或多个结构体中指定的文件描述符无效。EFAULTfds指针指向的地址超出进程的地址空间。EINTR请求的事件之前产生一个信号,调用可以重新发起。EINVALnfds参数超出PLIMITNOFILE值。ENOMEM可用内存不足,无法完成请求。 2。3测出程序 编写一个echoserver程序,功能是客户端向服务器发送信息,服务器接收输出并原样发送回给客户端,客户端接收到输出到终端。 服务器端程序如下:1includestdio。h2includestdlib。h3includestring。h4includeerrno。h56includenetinetin。h7includesyssocket。h8includepoll。h9includeunistd。h10includesystypes。h1112defineIPADDRESS127。0。0。113definePORT878714defineMAXLINE102415defineLISTENQ516defineOPENMAX100017defineINFTIM11819函数声明20创建套接字并进行绑定21staticintsocketbind(constcharip,intport);22IO多路复用poll23staticvoiddopoll(intlistenfd);24处理多个连接25staticvoidhandleconnection(structpollfdconnfds,intnum);2627intmain(intargc,charargv〔〕)28{29intlistenfd,connfd,sockfd;30structsockaddrincliaddr;31socklentcliaddrlen;32listenfdsocketbind(IPADDRESS,PORT);33listen(listenfd,LISTENQ);34dopoll(listenfd);35return0;36}3738staticintsocketbind(constcharip,intport)39{40intlistenfd;41structsockaddrinservaddr;42listenfdsocket(AFINET,SOCKSTREAM,0);43if(listenfd1)44{45perror(socketerror:);46exit(1);47}48bzero(servaddr,sizeof(servaddr));49servaddr。sinfamilyAFINET;50inetpton(AFINET,ip,servaddr。sinaddr);51servaddr。sinporthtons(port);52if(bind(listenfd,(structsockaddr)servaddr,sizeof(servaddr))1)53{54perror(binderror:);55exit(1);56}57returnlistenfd;58}5960staticvoiddopoll(intlistenfd)61{62intconnfd,sockfd;63structsockaddrincliaddr;64socklentcliaddrlen;65structpollfdclientfds〔OPENMAX〕;66intmaxi;67inti;68intnready;69添加监听描述符70clientfds〔0〕。fdlistenfd;71clientfds〔0〕。eventsPOLLIN;72初始化客户连接描述符73for(i1;iOPENMAX;i)74clientfds〔i〕。fd1;75maxi0;76循环处理77for(;;)78{79获取可用描述符的个数80nreadypoll(clientfds,maxi1,INFTIM);81if(nready1)82{83perror(pollerror:);84exit(1);85}86测试监听描述符是否准备好87if(clientfds〔0〕。reventsPOLLIN)88{89cliaddrlensizeof(cliaddr);90接受新的连接91if((connfdaccept(listenfd,(structsockaddr)cliaddr,cliaddrlen))1)92{93if(errnoEINTR)94continue;95else96{97perror(accepterror:);98exit(1);99}100}101fprintf(stdout,acceptanewclient:s:d,inetntoa(cliaddr。sinaddr),cliaddr。sinport);102将新的连接描述符添加到数组中103for(i1;iOPENMAX;i)104{105if(clientfds〔i〕。fd0)106{107clientfds〔i〕。fdconnfd;108break;109}110}111if(iOPENMAX)112{113fprintf(stderr,toomanyclients。);114exit(1);115}116将新的描述符添加到读描述符集合中117clientfds〔i〕。eventsPOLLIN;118记录客户连接套接字的个数119maxi(imaxi?i:maxi);120if(nready0)121continue;122}123处理客户连接124handleconnection(clientfds,maxi);125}126}127128staticvoidhandleconnection(structpollfdconnfds,intnum)129{130inti,n;131charbuf〔MAXLINE〕;132memset(buf,0,MAXLINE);133for(i1;inum;i)134{135if(connfds〔i〕。fd0)136continue;137测试客户描述符是否准备好138if(connfds〔i〕。reventsPOLLIN)139{140接收客户端发送的信息141nread(connfds〔i〕。fd,buf,MAXLINE);142if(n0)143{144close(connfds〔i〕。fd);145connfds〔i〕。fd1;146continue;147}148printf(readmsgis:);149write(STDOUTFILENO,buf,n);150向客户端发送buf151write(connfds〔i〕。fd,buf,n);152}153}154} 客户端代码如下所示:1includenetinetin。h2includesyssocket。h3includestdio。h4includestring。h5includestdlib。h6includepoll。h7includetime。h8includeunistd。h9includesystypes。h1011defineMAXLINE102412defineIPADDRESS127。0。0。113defineSERVPORT87871415definemax(a,b)(ab)?a:b1617staticvoidhandleconnection(intsockfd);1819intmain(intargc,charargv〔〕)20{21intsockfd;22structsockaddrinservaddr;23sockfdsocket(AFINET,SOCKSTREAM,0);24bzero(servaddr,sizeof(servaddr));25servaddr。sinfamilyAFINET;26servaddr。sinporthtons(SERVPORT);27inetpton(AFINET,IPADDRESS,servaddr。sinaddr);28connect(sockfd,(structsockaddr)servaddr,sizeof(servaddr));29处理连接描述符30handleconnection(sockfd);31return0;32}3334staticvoidhandleconnection(intsockfd)35{36charsendline〔MAXLINE〕,recvline〔MAXLINE〕;37intmaxfdp,stdineof;38structpollfdpfds〔2〕;39intn;40添加连接描述符41pfds〔0〕。fdsockfd;42pfds〔0〕。eventsPOLLIN;43添加标准输入描述符44pfds〔1〕。fdSTDINFILENO;45pfds〔1〕。eventsPOLLIN;46for(;;)47{48poll(pfds,2,1);49if(pfds〔0〕。reventsPOLLIN)50{51nread(sockfd,recvline,MAXLINE);52if(n0)53{54fprintf(stderr,client:serverisclosed。);55close(sockfd);56}57write(STDOUTFILENO,recvline,n);58}59测试标准输入是否准备好60if(pfds〔1〕。reventsPOLLIN)61{62nread(STDINFILENO,sendline,MAXLINE);63if(n0)64{65shutdown(sockfd,SHUTWR);66continue;67}68write(sockfd,sendline,n);69}70}71}三、epoll3。1基本知识 epoll是在2。6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。3。2epoll接口 epoll操作过程需要三个接口,分别如下:includesysepoll。hintepollcreate(intsize);intepollctl(intepfd,intop,intfd,structepolleventevent);intepollwait(intepfd,structepolleventevents,intmaxevents,inttimeout); (1)intepollcreate(intsize); 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看proc进程idfd,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。 (2)intepollctl(intepfd,intop,intfd,structepolleventevent); epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epollcreate()的返回值,第二个参数表示动作,用三个宏来表示: EPOLLCTLADD:注册新的fd到epfd中; EPOLLCTLMOD:修改已经注册的fd的监听事件; EPOLLCTLDEL:从epfd中删除一个fd; 第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,structepollevent结构如下:structepollevent{uint32tevents;Epolleventsepolldatatdata;Userdatavariable}; events可以是以下几个宏的集合: EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLOUT:表示对应的文件描述符可以写; EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); EPOLLERR:表示对应的文件描述符发生错误; EPOLLHUP:表示对应的文件描述符被挂断; EPOLLET:将EPOLL设为边缘触发(EdgeTriggered)模式,这是相对于水平触发(LevelTriggered)来说的。 EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里 (3)intepollwait(intepfd,structepolleventevents,intmaxevents,inttimeout); 等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epollcreate()时的size,参数timeout是超时时间(毫秒,0会立即返回,1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。3。3。工作模式 epoll对文件描述符的操作有两种模式:LT(leveltrigger)和ET(edgetrigger)。LT模式是默认模式,LT模式与ET模式的区别如下: LT模式:当epollwait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epollwait时,会再次响应应用程序并通知此事件。 ET模式:当epollwait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epollwait时,不会再次响应应用程序并通知此事件。 ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读阻塞写操作把处理多个文件描述符的任务饿死。3。4。测试程序 编写一个服务器回射程序echo,练习epoll过程。 服务器代码如下所示:1includestdio。h2includestdlib。h3includestring。h4includeerrno。h56includenetinetin。h7includesyssocket。h8include9includesysepoll。h10includeunistd。h11includesystypes。h1213defineIPADDRESS127。0。0。114definePORT878715defineMAXSIZE102416defineLISTENQ517defineFDSIZE100018defineEPOLLEVENTS1001920函数声明21创建套接字并进行绑定22staticintsocketbind(constcharip,intport);23IO多路复用epoll24staticvoiddoepoll(intlistenfd);25事件处理函数26staticvoid27handleevents(intepollfd,structepolleventevents,intnum,intlistenfd,charbuf);28处理接收到的连接29staticvoidhandleaccpet(intepollfd,intlistenfd);30读处理31staticvoiddoread(intepollfd,intfd,charbuf);32写处理33staticvoiddowrite(intepollfd,intfd,charbuf);34添加事件35staticvoidaddevent(intepollfd,intfd,intstate);36修改事件37staticvoidmodifyevent(intepollfd,intfd,intstate);38删除事件39staticvoiddeleteevent(intepollfd,intfd,intstate);4041intmain(intargc,charargv〔〕)42{43intlistenfd;44listenfdsocketbind(IPADDRESS,PORT);45listen(listenfd,LISTENQ);46doepoll(listenfd);47return0;48}4950staticintsocketbind(constcharip,intport)51{52intlistenfd;53structsockaddrinservaddr;54listenfdsocket(AFINET,SOCKSTREAM,0);55if(listenfd1)56{57perror(socketerror:);58exit(1);59}60bzero(servaddr,sizeof(servaddr));61servaddr。sinfamilyAFINET;62inetpton(AFINET,ip,servaddr。sinaddr);63servaddr。sinporthtons(port);64if(bind(listenfd,(structsockaddr)servaddr,sizeof(servaddr))1)65{66perror(binderror:);67exit(1);68}69returnlistenfd;70}7172staticvoiddoepoll(intlistenfd)73{74intepollfd;75structepolleventevents〔EPOLLEVENTS〕;76intret;77charbuf〔MAXSIZE〕;78memset(buf,0,MAXSIZE);79创建一个描述符80epollfdepollcreate(FDSIZE);81添加监听描述符事件82addevent(epollfd,listenfd,EPOLLIN);83for(;;)84{85获取已经准备好的描述符事件86retepollwait(epollfd,events,EPOLLEVENTS,1);87handleevents(epollfd,events,ret,listenfd,buf);88}89close(epollfd);90}9192staticvoid93handleevents(intepollfd,structepolleventevents,intnum,intlistenfd,charbuf)94{95inti;96intfd;97进行选好遍历98for(i0;inum;i)99{100fdevents〔i〕。data。fd;101根据描述符的类型和事件类型进行处理102if((fdlistenfd)(events〔i〕。eventsEPOLLIN))103handleaccpet(epollfd,listenfd);104elseif(events〔i〕。eventsEPOLLIN)105doread(epollfd,fd,buf);106elseif(events〔i〕。eventsEPOLLOUT)107dowrite(epollfd,fd,buf);108}109}110staticvoidhandleaccpet(intepollfd,intlistenfd)111{112intclifd;113structsockaddrincliaddr;114socklentcliaddrlen;115clifdaccept(listenfd,(structsockaddr)cliaddr,cliaddrlen);116if(clifd1)117perror(accpeterror:);118else119{120printf(acceptanewclient:s:d,inetntoa(cliaddr。sinaddr),cliaddr。sinport);121添加一个客户描述符和事件122addevent(epollfd,clifd,EPOLLIN);123}124}125126staticvoiddoread(intepollfd,intfd,charbuf)127{128intnread;129nreadread(fd,buf,MAXSIZE);130if(nread1)131{132perror(readerror:);133close(fd);134deleteevent(epollfd,fd,EPOLLIN);135}136elseif(nread0)137{138fprintf(stderr,clientclose。);139close(fd);140deleteevent(epollfd,fd,EPOLLIN);141}142else143{144printf(readmessageis:s,buf);145修改描述符对应的事件,由读改为写146modifyevent(epollfd,fd,EPOLLOUT);147}148}149150staticvoiddowrite(intepollfd,intfd,charbuf)151{152intnwrite;153nwritewrite(fd,buf,strlen(buf));154if(nwrite1)155{156perror(writeerror:);157close(fd);158deleteevent(epollfd,fd,EPOLLOUT);159}160else161modifyevent(epollfd,fd,EPOLLIN);162memset(buf,0,MAXSIZE);163}164165staticvoidaddevent(intepollfd,intfd,intstate)166{167structepolleventev;168ev。eventsstate;169ev。data。fdfd;170epollctl(epollfd,EPOLLCTLADD,fd,ev);171}172173staticvoiddeleteevent(intepollfd,intfd,intstate)174{175structepolleventev;176ev。eventsstate;177ev。data。fdfd;178epollctl(epollfd,EPOLLCTLDEL,fd,ev);179}180181staticvoidmodifyevent(intepollfd,intfd,intstate)182{183structepolleventev;184ev。eventsstate;185ev。data。fdfd;186epollctl(epollfd,EPOLLCTLMOD,fd,ev);187} 客户端也用epoll实现,控制STDINFILENO、STDOUTFILENO、和sockfd三个描述符,程序如下所示:1includenetinetin。h2includesyssocket。h3includestdio。h4includestring。h5includestdlib。h6includesysepoll。h7includetime。h8includeunistd。h9includesystypes。h10include1112defineMAXSIZE102413defineIPADDRESS127。0。0。114defineSERVPORT878715defineFDSIZE102416defineEPOLLEVENTS201718staticvoidhandleconnection(intsockfd);19staticvoid20handleevents(intepollfd,structepolleventevents,intnum,intsockfd,charbuf);21staticvoiddoread(intepollfd,intfd,intsockfd,charbuf);22staticvoiddoread(intepollfd,intfd,intsockfd,charbuf);23staticvoiddowrite(intepollfd,intfd,intsockfd,charbuf);24staticvoidaddevent(intepollfd,intfd,intstate);25staticvoiddeleteevent(intepollfd,intfd,intstate);26staticvoidmodifyevent(intepollfd,intfd,intstate);2728intmain(intargc,charargv〔〕)29{30intsockfd;31structsockaddrinservaddr;32sockfdsocket(AFINET,SOCKSTREAM,0);33bzero(servaddr,sizeof(servaddr));34servaddr。sinfamilyAFINET;35servaddr。sinporthtons(SERVPORT);36inetpton(AFINET,IPADDRESS,servaddr。sinaddr);37connect(sockfd,(structsockaddr)servaddr,sizeof(servaddr));38处理连接39handleconnection(sockfd);40close(sockfd);41return0;42}434445staticvoidhandleconnection(intsockfd)46{47intepollfd;48structepolleventevents〔EPOLLEVENTS〕;49charbuf〔MAXSIZE〕;50intret;51epollfdepollcreate(FDSIZE);52addevent(epollfd,STDINFILENO,EPOLLIN);53for(;;)54{55retepollwait(epollfd,events,EPOLLEVENTS,1);56handleevents(epollfd,events,ret,sockfd,buf);57}58close(epollfd);59}6061staticvoid62handleevents(intepollfd,structepolleventevents,intnum,intsockfd,charbuf)63{64intfd;65inti;66for(i0;inum;i)67{68fdevents〔i〕。data。fd;69if(events〔i〕。eventsEPOLLIN)70doread(epollfd,fd,sockfd,buf);71elseif(events〔i〕。eventsEPOLLOUT)72dowrite(epollfd,fd,sockfd,buf);73}74}7576staticvoiddoread(intepollfd,intfd,intsockfd,charbuf)77{78intnread;79nreadread(fd,buf,MAXSIZE);80if(nread1)81{82perror(readerror:);83close(fd);84}85elseif(nread0)86{87fprintf(stderr,serverclose。);88close(fd);89}90else91{92if(fdSTDINFILENO)93addevent(epollfd,sockfd,EPOLLOUT);94else95{96deleteevent(epollfd,sockfd,EPOLLIN);97addevent(epollfd,STDOUTFILENO,EPOLLOUT);98}99}100}101102staticvoiddowrite(intepollfd,intfd,intsockfd,charbuf)103{104intnwrite;105nwritewrite(fd,buf,strlen(buf));106if(nwrite1)107{108perror(writeerror:);109close(fd);110}111else112{113if(fdSTDOUTFILENO)114deleteevent(epollfd,fd,EPOLLOUT);115else116modifyevent(epollfd,fd,EPOLLIN);117}118memset(buf,0,MAXSIZE);119}120121staticvoidaddevent(intepollfd,intfd,intstate)122{123structepolleventev;124ev。eventsstate;125ev。data。fdfd;126epollctl(epollfd,EPOLLCTLADD,fd,ev);127}128129staticvoiddeleteevent(intepollfd,intfd,intstate)130{131structepolleventev;132ev。eventsstate;133ev。data。fdfd;134epollctl(epollfd,EPOLLCTLDEL,fd,ev);135}136137staticvoidmodifyevent(intepollfd,intfd,intstate)138{139structepolleventev;140ev。eventsstate;141ev。data。fdfd;142epollctl(epollfd,EPOLLCTLMOD,fd,ev);143} epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。那epoll都是怎么解决的呢?在此之前,我们先看一下epoll和select和poll的调用接口上的不同,select和poll都只提供了一个函数select或者poll函数。而epoll提供了三个函数,epollcreate,epollctl和epollwait,epollcreate是创建一个epoll句柄;epollctl是注册要监听的事件类型;epollwait则是等待事件的产生。对于第一个缺点,epoll的解决方案在epollctl函数中。每次注册新的事件到epoll句柄中时(在epollctl中指定EPOLLCTLADD),会把所有的fd拷贝进内核,而不是在epollwait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epollctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epollwait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用scheduletimeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以catprocsysfsfilemax查看,一般来说这个数目和系统内存关系很大。 总结: (1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epollwait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epollwait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在醒着的时候要遍历整个fd集合,而epoll在醒着的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。 (2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epollwait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。