Linux——I/O复用函数
一、I/O复用技术
1.定义
I/O复用技术是一种在单个线程或进程中同时管理多个输入输出(I/O)操作的方法,主要用于提高程序的并发性能。允许程序在一个线程或进程中同时监视多个I/O操作是否就绪(如数据到达、可以写入),一旦某个或多个I/O事件发生,就进行处理。
2.本质
I/O复用使得程序能够同时监听多个文件描述符,但本身也是阻塞的。如果多个描述符同时就绪,则只能按顺序处理其中的每一个文件描述符。
3.常见的I/O复用技术
(1)select
通过检测多个文件描述符的状态(可读、可写、异常)来进行I/O操作。
但在处理的文件描述符较多时效率较低。
(2)poll
类似于select,也是在指定时间内轮询一定数量的文件描述符,测试是否有就绪者。
但没有描述符数量的限制,支持较大规模的文件描述符集,更加灵活。
(3)epoll(Linux特有)
使用一组函数来完成任务,而不是单个函数。
比select和poll更高效,采用事件驱动机制,适合大量连接的服务器应用。
二、select()
1.工作原理
select函数监视最多FD_SETSIZE(通常是1024)个文件描述符,传入三个集合:可读、可写、异常。当调用select时,它会阻塞直到某些文件描述符就绪。
2.select实现TCP服务器端
(1)服务器端代码
①首先,创建一个数组,该数组用来存放程序中的文件描述符。
②然后,创建一个集合,并调用FD_ZERO 方法,将集合中的每一个位清空。
③接着,使用FD_SET方法,将数组中的每个文件描述符传入到fd_set的集合中。
④调用select()方法,返回就绪文件描述符的总数。
⑤最后,使用FD_ISSET方法,检测哪些位被设置。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>
//TCP服务器端使用select,同时处理监听套接字和连接套接字#define MAXFD 10 //定义最大的文件描述符
int socket_init();//初始化套接字void fds_init(int fds[]) //清空数组,每个元素是-1
{for(int i=0;i<MAXFD;i++){fds[i]=-1;}
}void fds_add(int fds[],int fd)//添加一个描述符
{for(int i=0;i<MAXFD;i++){if(fds[i]==-1){fds[i]=fd;break;}}
}void fds_del(int fds[],int fd)//删除一个描述符
{for(int i=0;i<MAXFD;i++){if(fds[i]==fd){fds[i]=-1;}}
}void accept_client(int sockfd,int fds[]) //接受连接
{int c=accept(sockfd,NULL,NULL);//得到连接套接字if(c<0){return ;}printf("accept c=%d\n",c);fds_add(fds,c);//将连接套接子加入数组
}void recv_data(int c,int fds[]) //接受数据
{char buff[128]={0};int n=recv(c,buff,127,0);//只要接收缓冲区有数据,select会继续执行(未读完继续读)if(n<=0) //如果对方关闭或者接受失败,则删除数组中的套接字{close(c);fds_del(fds,c);printf("client close\n");return ;}printf("bufff=%s\n",buff);send(c,"ok",2,0);
}int main()
{//创建套接字int sockfd=socket_init(); if(sockfd==-1){exit(1);}//定义一个数组,收集描述符//原因:需要数组来保存所有的文件描述符,将数组中的元素再存放到集合中,执行select后,select会修改集合中的文件描述符int fds[MAXFD];fds_init(fds);//初始化数组fds_add(fds,sockfd);//向数组中添加监听套接字fd_set fdset;//定义一个集合while(1){FD_ZERO(&fdset);//清除fdset的所有位int maxfd=-1;//定义文件描述符最大值for(int i=0;i<MAXFD;i++) //循环遍历数组,将数组中的套接字放入到集合,并置为1{if(fds[i]==-1){continue;}FD_SET(fds[i],&fdset);//将数组中的元素添加到集合中,将对应的文件标识符位置置为1if(maxfd<fds[i]) //找出最大的文件描述符{maxfd=fds[i];}}//定义时间struct timeval tv={5,0};//使用select方法,会修改fdset集合int n=select(maxfd+1,&fdset,NULL,NULL,&tv);//失败if(n==-1){printf("select err\n");}else if(n==0) //超时{printf("time out\n");}else{for(int i=0;i<MAXFD;i++){if(fds[i]==-1){continue;}if(FD_ISSET(fds[i],&fdset))//测试发现该描述符fds[i]有事件{if(fds[i]==sockfd)//accept{accept_client(sockfd,fds);}else//recv{recv_data(fds[i],fds);}}}}}}
int socket_init()
{int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字if(sockfd==-1){return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family=AF_INET; //使用ipv4协议saddr.sin_port=htons(6000);//端口号saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//ip地址int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定端口和ip地址if(res==-1){printf("bind err\n");return -1;}res=listen(sockfd,5); //创建监听队列if(res==-1){return -1;}return sockfd;
}
(2)注意点
使用数组,而不直接集合是因为select方法会对集合进行修改,不能保证下一次调用的正确性。
3.select的缺点
(1)文件描述符数量有限(受限于FD_SETSIZE)
(2)每次调用时都需要重新设置文件描述符集合,效率较低
(3)在大量连接时性能下降(因为要线性扫描所有描述符)
三、poll()
1.工作原理
poll的核心类似于select,但使用一个pollfd 数组,指定每个文件描述符和监控事件。当调用 poll时,返回准备就绪的描述符。
2.poll实现TCP服务器端
①首先,创建一个pollfd数组,该数组用来存放程序中的文件描述符。
②为该数组的数据成员(fd,events,revents)赋值
③使用&检测事件
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>
#include<poll.h>
//TCP服务器端使用poll
//poll传递的参数是结构体数组#define MAXFD 10 //定义最大的文件描述符
int socket_init();//初始化套接字void fds_init(struct pollfd fds[])//初始化结构体数组
{for(int i=0;i<MAXFD;i++){fds[i].fd=-1;fds[i].events=0;fds[i].revents=0;}
}void fds_add(struct pollfd fds[],int fd) //向结构体数组中添加描述符
{for(int i=0;i<MAXFD;i++){if(fds[i].fd==-1){fds[i].fd=fd;fds[i].events=POLLIN;//设置事件为可读fds[i].revents=0;break;}}
}void fds_del(struct pollfd fds[],int fd) //向结构体数组中删除描述符
{for(int i=0;i<MAXFD;i++){if(fds[i].fd==fd){fds[i].fd=-1;fds[i].events=0;fds[i].revents=0;break;}}
}void accept_client(int sockfd,struct pollfd fds[])//接受客户端的连接
{int c=accept(sockfd,NULL,NULL);if(c<0){return ;}printf("accept c=%d\n",c);fds_add(fds,c);//向结构体数组中添加文件描述符
}void recv_data(int c,struct pollfd fds[]) //接受客户端的数据
{char buff[128]={0};int num=recv(c,buff,127,0);//只要接收缓冲区有数据,poll会继续执行(未读完继续读)if(num<=0) //对方关闭缓冲区或者缓冲区五无数据则关闭连接套接字{close(c);fds_del(fds,c);//从结构体数组中删除描述符printf("client close\n");return;}printf("buff=%s\n",buff);send(c,"ok",2,0);}
int main()
{int sockfd=socket_init(); //创建套接字if(sockfd==-1){exit(1);}struct pollfd fds[MAXFD]; //定义结构体数组fds_init(fds);//初始化结构体数组fds_add(fds,sockfd);//向结构体数组中添加数据while(1){int n=poll(fds,MAXFD,5000);//执行pollif(n==-1) //失败{printf("poll err\n");}else if(n==0)//超时{printf("time out \n");}else{for(int i=0;i<MAXFD;i++){if(fds[i].fd==-1)//无效{continue;}if(fds[i].revents&POLLIN) //检测是否有读数据的描述符{if(fds[i].fd==sockfd) //如果是监听套接字,则建立连接{accept_client(sockfd,fds);}else //接受客户端连接{recv_data(fds[i].fd,fds);}}}}}}int socket_init()
{int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字if(sockfd==-1){return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family=AF_INET; //使用ipv4协议saddr.sin_port=htons(6000);//端口号saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//ip地址int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定端口和ip地址if(res==-1){printf("bind err\n");return -1;}res=listen(sockfd,5); //创建监听队列if(res==-1){return -1;}return sockfd;
}
3.poll的优点和缺点
(1)优点
①支持超过FD_SETSIZE的文件描述符
②不受限制的数组大小
(2)缺点
和select一样,每次调用都要重新设置数组,仍有线性扫描的问题
四、epoll()
1.工作原理
epoll采用事件驱动机制,epoll将文件描述符上的事件放在内核的事件表(红黑树实现)中,需要一个额外的文件描述符,来唯一标识内核中的这个事件表。
2.epoll实现TCP服务器端
①首先,使用epoll_create()创建epoll对象 。
②然后,使用epoll_ctl()操作内核事件表。
③接着,使用epoll_wait()等待事件发生,获取就绪队列。
事件发生后,立即获得就绪的描述符集合(无须线性扫描全部描述符)
#include<sys/epoll.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>#define MAXFD 10
//创建套接字
int socket_init()
{int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd==-1){return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family=AF_INET;saddr.sin_port=htons(6000);saddr.sin_addr.s_addr=inet_addr("127.0.0.1");int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));if(res==-1){printf("bind err\n");return -1;}res=listen(sockfd,5);if(res==-1){return -1;}return sockfd;}//添加数据到内核事件表
void epoll_add(int epfd,int fd)
{struct epoll_event ev; //定义数组结构体ev.data.fd=fd; //将数据添加到表中ev.events=EPOLLIN;//设置事件为读事件if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1){printf("epoll_ctl err\n");}
}
//从内核表中删除数据
void epoll_del(int epfd,int fd)
{if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1){printf("epoll ctl del err\n") ;}
}
//接受客户端的连接
void accept_client(int sockfd,int epfd)
{int c=accept(sockfd,NULL,NULL);if(c<=0){printf("accept err\n");return ;}printf("accept c=%d\n",c);epoll_add(epfd,c);//将c添加到内核事件表
}
//接受客户端的数据
void recv_data(int c,int epfd)
{char buff[128]={0};int n=recv(c,buff,1,0);if(n<=0){epoll_del(epfd,c); //将c从内核事件表删除close(c);printf("client close\n");return;}printf("buff=%s\n",buff);send(c,"ok",2,0);
}int main()
{int sockfd=socket_init();//创建套接字if(sockfd==-1){exit(1);}//创建内核事件表(红黑树) 就绪队列(链表)int epfd=epoll_create(MAXFD);//将监听套接字添加到内核事件表epoll_add(epfd,sockfd);struct epoll_event evs[MAXFD];//用户数组,收集就绪描述符while(1){//获取就绪描述符,会阻塞int n=epoll_wait(epfd,evs,MAXFD,5000);//从内核事件表中获取就绪描述符存放到evs中if(n==-1) //失败{printf("epoll wait err\n");exit(1);}else if(n==0) //超时{printf("time out\n");}else{for(int i=0;i<n;i++)//就绪描述符的个数=n{int fd=evs[i].data.fd;//从就绪数组中取出描述符if(fd==-1){continue; //无效}if(evs[i].events&EPOLLIN) //判断读事件是否发生 {if(fd==sockfd) //accept{accept_client(fd,epfd); }else {recv_data(fd,epfd);}} }}}
}
3.ET模式和LT模式
(1)LT模式(水平触发模式)
当检测到有事件发生但用户未处理完成时,会将此事件通知应用程序,只要缓冲区非空可以读/空间可写就会一直通知,直到应用程序读取或写入完毕。
(2)ET模式(边缘触发模式)
当检测到有事件发生但用户未处理完成时,只提醒一次,无论用户是否将事件处理完毕,都不再提醒。直到缓冲区状态再次发生变化,会提醒。
4.epoll()的优点和缺点
(1)优点
①高效处理大量连接。
②内核管理就绪事件,减少复制开销。
③支持边缘触发(edge-triggered)模式,提高性能
(2)缺点
仅在Linux系统上支持