I/O多路复用之select、epoll的实现和区别 ,ET与LT模式

来源:互联网 发布:千里眼电脑监控软件 编辑:程序博客网 时间:2024/05/01 22:40

 概念:IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。


select:select系统调用是用来让我们的程序监视多个文件句柄的状态变化。

 1.函数原型intselect(int nfds,fd_set *readfds,fd_set

*writefds,fd_set *exceptsfds,conststruct timeval *timeout)

   返回值:就绪描述符的数目,超时返回0,出错返回-1。

   参数:nfds是需要监听的最大文件描述符+1(即文件描述的个数),第2,3,4分别对应需要检测的可读/写/异常文件描述的集合,struct timeval结构设置超时范围。

可通过以下四个宏对文件描述符进行设置:

          void FD_ZERO(fd_set *fdset);           //清空集合

          void FD_SET(int fd, fd_set *fdset);   //将一个给定的文件描述符加入集合之中

          void FD_CLR(int fd, fd_set *fdset);   //将一个给定的文件描述符从集合中删除

          int FD_ISSET(int fd, fd_set *fdset);   // 检查集合中指定的文件描述符是否可以读写 

运用select编写的一个tcp服务器:

#include<stdio.h>#include<stdlib.h>#include<sys/types.h>#include<sys/socket.h>#include<sys/time.h>#include<sys/select.h>#include<netinet/in.h>#include<arpa/inet.h>#define _SIZE_ 128int gfds[_SIZE_];int main(int argc,char* argv[]){if(argc!=3){exit(1);}int sock=socket(AF_INET,SOCK_STREAM,0);if(sock<0){perror("sock");exit(2);}int opt=1;setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));struct sockaddr_in local;local.sin_family=AF_INET;local.sin_addr.s_addr=inet_addr(argv[1]);local.sin_port=htons(atoi(argv[2]));socklen_t len=sizeof(local);if(bind(sock,(struct sockaddr*)&local,len)<0){perror("bind");exit(3);}    if(listen(sock,5)<0){perror("listen");exit(4);}//selectint i=0;for(;i<_SIZE_;i++){gfds[i]=-1;}gfds[0]=sock;while(1){int max_fd=-1;struct timeval timeout={5,0};fd_set rfds;FD_ZERO(&rfds);int j=0;for(;j<_SIZE_;j++){if(gfds[j]>=0)FD_SET(gfds[j],&rfds);  //设置读集if(max_fd<gfds[j])max_fd=gfds[j];  //更新最大文件描述符}switch(select(max_fd+1,&rfds,NULL,NULL,&timeout)){case 0:printf("timeout\n");break;case -1:printf("faild\n");break;    default:  //成功{                int k=0;for(;k<_SIZE_;k++){//没就绪if(gfds[k]<0){continue;}if(gfds[k]==sock&&FD_ISSET(gfds[k],&rfds)){struct sockaddr_in peer;socklen_t len=sizeof(peer);int new_sock=accept(gfds[k],(struct sockaddr*)&peer,&len);int m=0;for(;m<_SIZE_;m++){if(gfds[m]==-1){gfds[m]=new_sock; break;} }if(m==_SIZE_){close(new_sock);}}//ifelse if(FD_ISSET(gfds[k],&rfds)){char buf[128];int s=read(gfds[k],buf,sizeof(buf)-1);if(s==0)//对端连接关闭了,行当于读到文件结尾{printf("client is quit\n");close(gfds[k]);gfds[k]=-1;}if(s>0){buf[s-1]=0;printf("%s\n",buf);}}}}}}}



select的几大缺点:

(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

(3)select支持的文件描述符数量太小了,默认是1024


epoll:

    1. epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次

       2. epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

运用epoll编写的一个tcp服务器:

#include<stdio.h>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include<sys/epoll.h>#include<stdlib.h>#include<string.h>typedef struct epbuf{int fd;char buf[1024];}epbuf_t,*epbuf_p,**epbuf_pp;static epbuf_p alloc_epbuf(int fd){epbuf_p ptr=(epbuf_p)malloc(sizeof(epbuf_t));if(ptr==NULL){perror("malloc");exit(5);}ptr->fd=fd;return ptr;}int main(int argc,char* argv[]){if(argc!=3){printf("参数错误\n");exit(1);}int listen_sock=socket(AF_INET,SOCK_STREAM,0);if(listen_sock<0){perror("socket");exit(2);}struct sockaddr_in local;local.sin_family=AF_INET;local.sin_addr.s_addr=inet_addr(argv[2]);local.sin_port=htons(atoi(argv[1]));socklen_t len=sizeof(local);if(bind(listen_sock,(struct sockaddr*)&local,len)<0){perror("bind");exit(3);}if(listen(listen_sock,5)<0){perror("listen");exit(4);}//1.创建一个epollint epfd=epoll_create(200);struct epoll_event ev;ev.events=EPOLLIN|EPOLLET;ev.data.ptr=alloc_epbuf(listen_sock);    //2.注册epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);while(1){int nums=0;struct epoll_event evs[32];int max_evs=32;int timeout=5000;switch(nums=epoll_wait(epfd,evs,max_evs,timeout)){case 0: //超时printf("timeout\n");break;case -1:perror("epoll_wait");break;default: { int i=0;for(;i<nums;i++){int fd=((epbuf_p)(evs[i].data.ptr))->fd;if((evs[i].events&EPOLLIN)&&fd==listen_sock){struct sockaddr_in peer;socklen_t len=sizeof(peer);int new_sock=accept(fd,(struct sockaddr*)&peer,&len);if(new_sock<0){perror("accept" );continue;}ev.events=EPOLLIN;ev.data.ptr=alloc_epbuf(new_sock);epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);}//ifelse if((evs[i].events&EPOLLIN)&&fd!=listen_sock)//读事件就绪{char* buf=((epbuf_p)(evs[i].data.ptr))->buf;ssize_t s=read(fd,buf,1023);if(s>0){buf[s]=0;printf("%s\n",buf);//回写ev.events=EPOLLOUT;epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);}else if(s==0){    free(evs[i].data.ptr);evs[i].data.ptr=NULL;epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);}}//else if     else if((evs[i].events&EPOLLOUT)&&fd!=listen_sock)//写事件就绪{const char* msg="HTTP/1.0 200  OK \r\n\r\n<html><h1>HELLO WORLD!</h1></html>\n";write(fd,msg,strlen(msg));    free(evs[i].data.ptr);epoll_ctl(epfd,EPOLL_CTL_DEL,fd ,NULL);  } }} }}return 0;}

      epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

  LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

  ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

  ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。



select与epoll的区别与联系:

      epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。

     1. epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。

     2. epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd。

     3. epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目



0 0