select、poll、epoll三组IO复用

int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout)//其中nfds是被监听的文件描述符总数通常设置为所有文件描述符中的最大值加一(是否可以理解为凡是小于这个描述符的都要被轮询?,加1是因为文件描述符从0开始计数),readfds、writefds、exceptfds分别指向可读可写异常事件对应的文件描述符集合(当事件发生时内核在线修改该集合以此来通知应用程序),timeout指出等待时间(能精确到微秒但是不可信)若为NULL则一直阻塞直到有事件发生,fd_set结构体能容纳的文件描述符数目由FD_SETSIZE限制(所有事件的数目)其同时限制了select能同时处理的文件描述符总量。调用成功返回就绪事件描述符总数失败返回-1。宏FD_SET(int fd,fd_set* fdset)将文件描述符fd和事件集合fd_set绑定(这个fd_set可以传给select中间的三个参数之一),宏FD_ISSET(int fd,fd_set* fdset)可以检测fd上的事件是否发生(内核在线修改这个数据机构所以select调用后可以使用该宏判断并处理相应事件),由于是内核在线修改事件集合所以每次select调用后都需要重置事件集合才能进行下一次select调用。因此select编写格式大致绑定文件描述符和事件集合,select调用,测试特定文件描述符上的事件是否发生,重新绑定文件描述符和事件集合的关系,再次调用select,伪代码如下:

int sockfd=socket(PF_INET,SOCK_STREAM,0);//以一个网络socket文件描述符为例
bind(sockfd,...);
listen(sockfd,...);
fd_set read_fds;//申明可读事件集合
FD_ZERO(&read_fds);//清零
while(1){//循环等待连接发送数据事件
          FD_SET(sockfd,&read_fds);//将文件描述符sockfd和可读事件集合read_fds绑定
          int ret=select(sockfd+1,&read_fds,NULL,NULL,NULL)//注意sockfd+1若监听多个文件描述符还需要找出哪个最大,这里只监听可读事件所以中间的可写和异常置为NULL,最后的超时时间置为NULL表示阻塞等待直到有事件就绪
          if(ret<0)
          	cout<<"select错误"<<endl;
          if(FD_ISSET(sockfd,&read_fds)){//测试read_fds的位sockfd是否被设置,若被设置则表明内核在线修改即sockfd上事件就绪
          	这里定义处理数据的逻辑代码
          }//若多个文件描述符定义为数组,这里改为循环测试文件描述符
}//可见每次循环都需要重置文件描述符和事件集合的绑定关系,当处理多个文件描述符的逻辑关键在绑定文件描述符到相应的事件集合上,且找出最大的文件描述符,且循环多次测试文件描述符

int poll(struct pollfd* fds,nfds_t nfds,int timeout)//其中fds是个pollfd结构体数组(因为是数组所以才能监听多个文件描述符),pollfd结构体作用是将文件描述符fd和事件events(一些列事件的按位或,具体事件类型可以查阅其它资料)绑定并且内核修改revents成员从而达到侦听并告知应用程序(注意select是在线修改事件集合而poll内核修改revents所以决定了poll不用每次调用后重置事件集合,因为events并没有被修改),nfds是告诉内核fds数组的大小,timeout是超时值若为-1则永远阻塞直到事件发生若为0则立即返回。伪代码实例:

 
 
 
 
int poll_two(int fd1,int fd2){
	struct pollfd poll_array[2];
	int ret;
	poll_array[0].fd = fd1;
	poll_array[1].fd = fd2;
	poll_array[0].events = POLLIN;
	poll_array[1].events = POLLIN;
	while(1){
		ret = poll(poll_array,(unsigned long)2,-1);//这里的2是指注册了事件的文件描述符个数
           	if(ret < 0){
               		cout<<"poll错误"<<strerror(errno)<<endl;
               		return -1;
           	}
    		for(int i=0;i<2;i++){//循环处理每个注册的文件描述符,注意这里的2
           		if(((poll_array[i].revents&POLLHUP) == POLLHUP) ||//检测事件是否发生一些异常,是否挂起,比如管道的写端被关闭读端收到POLLHUP事件
              	  	  ((poll_array[i].revents&POLLERR) == POLLERR) ||//是否错误
              	  	  ((poll_array[i].revents&POLLNVAL) == POLLNVAL))//文件描述符没有打开
             			return 0;
			     if(poll_array[i].events&POLLIN){
				   这里定义处理第i个文件描述符fd上的数据的逻辑代码
			  }
		   }//end_for
       }//end_while
}//可见poll索引就绪事件是扫描整个注册了事件的文件描述符(将上面的2替换你想要的数字)


epoll则有三个系统调用构成的IO复用逻辑,epoll将用户关心的文件描述符上的事件放在内核的一个事件表中,从而不像select和poll那样每次调用都重复传入事件集合(一切缘由多个系统调用将select和poll的底层逻辑分开了),因此epoll需要一个额外的文件描述符标示这个事件表,这个事件表由epoll_create函数创建

int epoll_create(int size)//其中size只是给内核一个提示告诉内核事件表需要多大现在已经不起作用了,返回事件表描述符,这个描述符将在后续中使用

int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)//该函数用来操作事件表,其中fd是需要操作的文件描述符(不是事件表描述符),op参数指定操作类型:EPOLL_CTL_ADD往fd上添加事件,EPOLL_CTL_MOD修改fd上事件,EPOLL_CTL_DEL删除fd上的注册事件。结构体epoll_event的成员events是事件类型(一些列事件的按位或,具体有哪些类型就部列出了不过值得注意的是epoll事件是poll事件前面加E),成员data又是个结构体用于存储用户数据最常用的就是将data.fd设置为需要监听的文件描述符fd,成功返回0失败返回-1

int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout)//在一段超时时间内等待一组文件描述符上的事件,成功返回事件就续的文件描述符个数,失败返回-1.其中epfd就是事件表描述符,maxevents指定最多监听多少个事件(必须大于0),timeout和poll的相同。伪代码实例:

epoll_event events[MAX_EVENT_NUMBER];//定义就绪事件集合(这里events不要和结构体epoll_event里的成员events混淆),该集合表示事件就绪后内核修改并由epoll_wait返回
int epollfd=epoll_create(5);//创建事件表
//这里将你需要监听的文件描述符的相应事件注册到事件表中
for(int i=0;i<监听的文件描述符个数;i++){//假设监听的文件描述符存于数组fd中
	epoll_event event;//定义临时事件结构体
	event.data.fd=fd[i];//描述符fd[i]写到用户数据中
	event.events=EPOLLIN;//注册fd[i]是监听事件为可读,可以是其它事件按位或
	epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);//将该监听事件注册到事件表中
}
while(1){
	int ret=epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1)//阻塞等待直到有事件就绪,events将返回就绪事件集合
	for(int i=0;i<ret;i++){//逐个处理就绪事件
		这里定义就绪事件对应的文件描述符events[i].data.fd上的数据处理逻辑
	}
}//可见epoll返回的仅仅是就绪事件集合

三组IO复用的比较:

它们都是在等待timeout时间内返回就绪事件个数,select由于没有将文件描述符和事件集合自动绑定且内核在线修改就绪事件仍在同一事件集合中进行,所以再次调用select需要重新手动绑定文件描述符和事件集合,且只有三种事件集合可以绑定;poll提供了一个封装的结构体pollfd相对来说简洁些,当事件就绪后内核修改通知也在一个额外的参数revents内进行所以再次调用poll无需重新绑定文件描述符和事件集合的关系,不过select和poll每次返回的都是整个用户注册的事件集合所以应用程序索引就绪文件描述符的复杂度为O(n)//n是注册文件描述符个数;epolll则采用独立的事件表维护注册事件,采用epoll_ctl修改事件表,epoll_wait等待就绪事件且返回的就仅仅是就绪事件集合(采用独立参数),使得底层逻辑模块独立化,这使得应用程序索引就绪文件描述符的复杂度为O(1).

poll和epoll都采用额外参数指定监听文件描述符的个数(nfds和maxevents)这两个数值都能达到系统允许打开的最大文件描述符个数(65536),而select受限与FD_SETSIZE(貌似是1024忘记了...)

select和poll采用轮询机制每次调用都扫描整个注册文件描述符集合并将其中就绪的文件描述符返回给应用程序,所以它们检测就是事件的时间复杂度是O(n)。epoll_wait则采用事件回调函数当检测到就绪文件描述符时将触发回调函数,回调函数就将该文件描述符上对应的事件插入到内核就绪事件队列,时间复杂度为O(1)。当活动连接比较多的时候epoll_wait未必比select和poll效率高,因为回调函数过于频繁。所以epoll_wait适合连接数量多且idle连接较多(即活动连接少)的情形。

关于epoll的LT模式和ET模式及EPOLLINONESHOT参见前两篇文章。


你可能感兴趣的:(三组IO复用)