I/O复用技术使得程序能够同时监听多个文件描述符,这对于提高程序的性能至关重要。
TCP 服务器同时要处理监听套接字和连接套接字。
服务器要同时处理 TCP 请求和 UDP 请求。
程序要同时处理多个套接字。
客户端程序要同时处理用户输入和网络连接。
服务器要同时监听多个端口。
需要指出的是,I/O 复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当 多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依处理其中的每一 个文件描述符,这使得服务器看起来好像是串行工作的。如果要提高并发处理的能力,可以 配合使用多线程或多进程等编程方法。
select
select 系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符的可读、 可写和异常等事件。
select接口
1. #include2. 3. int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 4.
应用:
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #define MAX 10 11 int socket_init(); 12 13 void poll_fds_init(struct pollfd fds//封装 对poll结构数组的初始化 14 { 15 for(int i=0;i select/poll性能总结
在用户空间创建集合或数组 来存放描述符及事件 ,每一次调用select 或 poll 时 ,需要将数组和集合当作参数传进去,每循环调用一次意味着从用户空间往内核空间拷贝一次数据结构;
内核实现 :在内核中 以 轮询 的方式实现,因此在内核中检测是否有事件就绪的时间复杂度为O(n);
函数调用完成返回后,无法确定具体哪个描述符有事件就绪,仅知道有个描述符上有事件就绪,此时便需要用户再次进行遍历查找具体是哪个描述符有事件,此时时间复杂度也为O(n);
结论:无法应对大量描述符。
epoll
epoll 的接口
epoll 是 Linux 特有的 I/O 复用函数。它在实现和使用上与 select、poll 有很大差异。为了解决大量文件描述符的问题。
首 先,epoll 使用一组函数来完成任务,而不是单个函数。
其次,epoll 把用户关心的文件描述 符上的事件放在内核里的一个事件表中。从而无需像 select 和 poll 那样每次调用都要重复传 入文件描述符或事件集。
但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这 个事件表。epoll 相关的函数如下:
内核实现:注册回调函数的方式实现。事件复杂度O(1)
返回方式:将就绪个数及具体的就绪的文件描述符信息全部返回 。时间复杂度O(1)
1. #include2. 3. int epoll_create(int size); //创建内核事件表,存放描述符及事件,数据结构:红黑树 4. 8. 9. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//用于操作内核事件表,每个描述符只添加一次 10. 34. 35. int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);//用于在一段超时时间内等待一组文件描述符上的事件,返回就绪的描述符的个数及具体的描述符信息 36. 37. epoll应用
实现服务器端:
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #define MAX 10 11 int socket_init(); //设置套接字 12 void epoll_add(int epfd,int fd) //封装一个往内核事件表添加描述符的函数 13 { 14 struct epoll_event ev; //定义事件结构体 15 ev.data.fd=fd; //写入文件描述符 16 ev.events=EPOLLIN; //写入关注事件 :读 17 if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1) //往内核时间表中加入该文件描述符信息 18 { 19 printf("epoll ctl erron"); 20 } 21 } 22 void epoll_del(int epfd,int fd) //封装一个从内核事件表删除描述符的函数 23 { 24 if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1) //从内核事件表中删除该文件描述符 25 { 26 perror("epoll ctl del errn"); 27 } 28 } 29 int main() 30 { 31 int sockfd=socket_init(); //创建监听套接字 32 assert(sockfd!=-1); 33 34 int epfd=epoll_create(MAX); //创建内核事件表 大小为MAX 35 assert(epfd!=-1); 36 37 epoll_add(epfd,sockfd); //加入监听套接字 38 39 struct epoll_event evs[MAX]; //创建事件结构存储所有就绪的事件 40 41 while(1) 42 { 43 int n=epoll_wait(epfd,evs,MAX,5000); //等待有事件就绪,可能会阻塞 44 if(n==-1) 45 { 46 printf("epoll wait errn"); 47 } 48 else if(n==0) 49 { 50 printf("time outn"); 51 } 52 else 53 { 54 for(int i=0;i 客户端
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 9 int main() 10 { 11 int sockfd=socket(AF_INET,SOCK_STREAM,0); 12 assert(sockfd!=-1); 13 14 struct sockaddr_in saddr; 15 saddr.sin_family=AF_INET; 16 saddr.sin_port=htons(6000); 17 saddr.sin_addr.s_addr=inet_addr("127.0.0.1"); 18 19 20 int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr)); 21 22 assert(res!=-1); 23 while(1) 24 { 25 char buff[128]={0}; 26 27 printf("intput:n"); 28 fgets(buff,128,stdin); 29 if(strncmp(buff,"end",3)==0) 30 { 31 break; 32 } 33 send(sockfd,buff,strlen(buff),0); 34 memset(buff,0,128); 35 recv(sockfd,buff,127,0); 36 printf("buff=%sn",buff); 37 } 38 close(sockfd); 39 40 } ~ LT 和 ET 模式
对文件描述符有两种操作模式:LT(Level Trigger,电平触发)模式和 ET(Edge Trigger,边沿触发)模式。
LT模式 :
概念:对于 LT 模式操作的文件描述符,当 检测到其上有事件发生并将此事件通知 应用程序后,应用程序可以不处理完该事件。这样,当应用程序下一次调用检测时, 还会再次向应用程序通告此事件,直到该事件被处理完。即对于数据的处理不需要一次性必须处理完。
注:select / poll / epoll 都具有这种模式。
ET 模式:
注:只有epoll 具有这种模式。
当往 epoll 内核事件表中注册一个文 件描述符上的 EPOLLET 事件时,epoll 将以高效的 ET 模式来操作该文件描述符。
概念:对于 ET 模式操作的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的 epoll_wait 调用将不再向应用程序 通知这一事件。所以 ET 模式在很大程度上降低了同一个 epoll 事件被重复触发的次数,因 此效率比 LT 模式高。
应用:
将用epoll实现的服务器端从LT模式改成ET模式
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #define MAX 10 11 int socket_init(); 12 void epoll_add(int epfd,int fd) 13 { 14 struct epoll_event ev; 15 ev.data.fd=fd; 16 ev.events=EPOLLIN|EPOLLET; // 设置 ET 模式 :按位或 ET模式 17 if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1) 18 { 19 printf("epoll ctl erron"); 20 } 21 } 22 void epoll_del(int epfd,int fd) 23 { 24 if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1) 25 { 26 perror("epoll ctl del errn"); 27 } 28 } 29 int main() 30 { 31 int sockfd=socket_init(); 32 assert(sockfd!=-1); 33 34 int epfd=epoll_create(MAX); 35 assert(epfd!=-1); 36 37 epoll_add(epfd,sockfd); 38 39 struct epoll_event evs[MAX]; 40 41 while(1) 42 { 43 int n=epoll_wait(epfd,evs,MAX,5000); 44 if(n==-1) 45 { 46 printf("epoll wait errn"); 47 } 48 else if(n==0) 49 { 50 printf("time outn"); 51 } 52 else 53 { 54 for(int i=0;i 优化:
由于ET模式下,数据存在丢失的可能,为了在只提式一次的情况下,将所有的数据全部都出来,因此需要进行以下优化。
描述符设置成非阻塞
循环处理
01 #include//描述符设置成非阻塞 02 #include //循环处理 11 int socket_init(); 12 void setnonblock(int fd)//1. 封装函数 将文件描述符设置成非阻塞模式 13 { 14 int oldfl=fcntl(fd,F_GRTFL); //获取之前文件描述符属性 15 int newfl=oldfl|O_NONBLOCK; //增添非阻塞模式属性 ,当文件没有数据读阻塞时会返回-1 16 if(fcntl(fd,F_SETFL,newfl)==-1)//给文件描述符设置增添后的新属性 15 { 14 printf("fcntl errorn"); 15 } 15 } 12 void epoll_add(int epfd,int fd) 13 { 14 struct epoll_event ev; 15 ev.data.fd=fd; 16 ev.events=EPOLLIN|EPOLLET; // 设置 ET 模式 :按位或 ET模式 17 if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1) 18 { 19 printf("epoll ctl erron"); 20 } 22 setnonblock(fd); //调用函数 给文件描述符 设置非阻塞形式 21 } 22 void epoll_del(int epfd,int fd) 23 { 24 if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1) 25 { 26 perror("epoll ctl del errn"); 27 } 28 } 29 int main() 30 { 31 int sockfd=socket_init(); 32 assert(sockfd!=-1); 33 34 int epfd=epoll_create(MAX); 35 assert(epfd!=-1); 36 37 epoll_add(epfd,sockfd); 38 39 struct epoll_event evs[MAX]; 40 41 while(1) 42 { 43 int n=epoll_wait(epfd,evs,MAX,5000); 44 if(n==-1) 45 { 46 printf("epoll wait errn"); 47 } 48 else if(n==0) 49 { 50 printf("time outn"); 51 } 52 else 53 { 54 for(int i=0;i



