Select 、Poll 和 Epoll

作用

Epoll 和 Select 的作用都是为了多I/O同步复用的问题,利用Epoll、Poll或Select函数指定内核监听多个I/O的读、写、异常事件,避免每一个I/O都指定一个处理线程,导致开销过大。

Select

需要指定最大监听描述符的值,还要传入对应监听事件的fd_set(readwriteexception),每一次Select函数触发都会将集合内容修改,所以开发者要设置缓存区来记录所有文件描述符,每次调用函数的时候都要重新初始化监听集合。

在Select触发后开发者需要去将缓存区的文件描述符与监听集合去匹配,例如描述符100在read set中,说明100有数据写入,可以立刻去读取,然后丢给worker线程去处理:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <wait.h>
#include <signal.h>
#include <errno.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>

#define MAXBUF 256

void child_process(void)
{
  sleep(2);
  char msg[MAXBUF];
  struct sockaddr_in addr = {0};
  int n, sockfd,num=1;
  srandom(getpid());
  /* Create socket and connect to server */
  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = inet_addr("127.0.0.1");

  connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));

  printf("child {%d} connected 
", getpid());
  while(1){
        int sl = (random() % 10 ) +  1;
        num++;
     	sleep(sl);
  	sprintf (msg, "Test message %d from client %d", num, getpid());
  	n = write(sockfd, msg, strlen(msg));	/* Send message */
  }

}

int main()
{
  char buffer[MAXBUF];
  int fds[5];
  struct sockaddr_in addr;
  struct sockaddr_in client;
  int addrlen, n,i,max=0;;
  int sockfd, commfd;
  fd_set rset;
  for(i=0;i<5;i++)
  {
  	if(fork() == 0)
  	{
  		child_process();
  		exit(0);
  	}
  }

  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  memset(&addr, 0, sizeof (addr));
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = INADDR_ANY;
  bind(sockfd,(struct sockaddr*)&addr ,sizeof(addr));
  listen (sockfd, 5); 

  for (i=0;i<5;i++) 
  {
    memset(&client, 0, sizeof (client));
    addrlen = sizeof(client);
    fds[i] = accept(sockfd,(struct sockaddr*)&client, &addrlen);
    if(fds[i] > max)
    	max = fds[i];
  }
  
  while(1){
	FD_ZERO(&rset);
  	for (i = 0; i< 5; i++ ) {
  		FD_SET(fds[i],&rset);
  	}

   	puts("round again");
	select(max+1, &rset, NULL, NULL, NULL);

	for(i=0;i<5;i++) {
		if (FD_ISSET(fds[i], &rset)){
			memset(buffer,0,MAXBUF);
			read(fds[i], buffer, MAXBUF);
			puts(buffer);
		}
	}	
  }
  return 0;
}

Select 要求开发者去统计最大监听范围,fd_set是一个长度为32的整数数组,每一个fd对应一个bit位。内核会遍历传入集合的每一bit到最大描述符对应的bit。同时用户进程在处理返回结果的时候仍然要遍历集合,在用户进程中处理事件的效率是O(n)。它主要的优点是跨平台比较方便windows和linux都支持该函数。

同时需要注意的是,如果正被select监听的文件描述符在另一个线程被关闭,实际上这个文件描述符不会被真正的关闭。

Select 在处理超时精度上要比Epoll和Poll高,Epoll和Poll只能处理一毫秒的精度[4]。

Poll

在使用的时候要去传入一个pollfd数组,其中包含每一个需要监听的文件描述符。pollfd中包含监听事件和返回事件,不需要每次都去构建。

struct pollfd {
      int fd;
      short events; 
      short revents;
};

与Select类似,用户在处理返回结果的时候要轮寻检查revents,看看是否和期待的事件一致。在内核中也需要遍历文件描述符的列表来找到受监视的对象,然后再遍历文件描述符列表去设置事件。它也具备Select的缺陷,即无法在其他线程中关闭正在监听的文件描述符[4]:

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

for (i=0;i<5;i++) 
{
memset(&client, 0, sizeof (client));
addrlen = sizeof(client);
pollfds[i].fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);
pollfds[i].events = POLLIN;
}
sleep(1);
while(1){
  puts("round again");
poll(pollfds, 5, 50000);

for(i=0;i<5;i++) {
	if (pollfds[i].revents & POLLIN){
		pollfds[i].revents = 0;
		memset(buffer,0,MAXBUF);
		read(pollfds[i].fd, buffer, MAXBUF);
		puts(buffer);
	}
}
}

Poll 与 Select相比:

  • 不需要统计最大监听范围
  • 更适合大值的文件描述符监听
  • 文件描述符监听集合不用每次都重建,但仍需要自己维护
  • Select移植性更好,超时监听更加灵敏

Epoll

epoll 会在内核中帮我们创建一个context用于记录要监听的文件描述符,可以在等待I/O事件的同时去添加或移除文件描述符。每次只会返回就绪的文件描述符,用户进程在处理返回结果上更加方便,时间复杂度为O(1)。但由于Epoll只在linux中支持,不支持跨平台,也是最晚出现的方法。

  struct epoll_event events[5];
  int epfd = epoll_create(10); //指定监听数量,只是建议内核初始化分配空间,并不影响监听数量上限。
  ...
  ...
  for (i=0;i<5;i++) 
  {
    static struct epoll_event ev;
    memset(&client, 0, sizeof (client));
    addrlen = sizeof(client);
    ev.data.fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);
    ev.events = EPOLLIN;
    epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev); 
  }
  
  while(1){
  	puts("round again");
  	nfds = epoll_wait(epfd, events, 5, 10000);
	
	for(i=0;i<nfds;i++) {
			memset(buffer,0,MAXBUF);
			read(events[i].data.fd, buffer, MAXBUF);
			puts(buffer);
	}
  }

因为监听的描述符记录在了内核中,所以即使用户进程被关闭了,内核也会去监视对应的对象,可以用于边缘触发等场景。

Epoll vs Select

  • 可以在等待的时候添加或移除监听对象
  • 只返回准备就绪的文件描述符
  • 用户进程处理事件效率更高(Epoll是O(1),Select是O(n))
  • Epoll只在Linux中支持,移植性差
  • Select超时监听更加灵敏
  • Epoll使用起来更复杂

Epoll vs Poll

  • 可以在等待的时候添加或移除监听对象
  • 只返回准备就绪的文件描述符
  • Epoll使用起来更复杂
  • 用户进程处理事件效率更高(Epoll是O(1),Poll是O(n))
  • Epoll使用起来更复杂

总结

Select、Epoll、Poll都是I/O多路复用的一种技术,都属于同步I/O的范畴,用户进程同时监听多个I/O事件[2]。

Select与Poll比较类似,每次调用函数都需要将监听的集合传入内核中,即要copy一次,返回的结果要轮寻匹配。不过相较Select而言Poll没有最大监听范围限制(Select在linux中最大监听范围为1024,此值可以修改),在没有变动的情况下无需每次调用时重新构建监听集合,而Select在跨平台性上和处理超时精度上是三者中最优的[4]。

Epoll是Select和Poll的优化版,它采用一个内核context来维护监听集合,如果在不需要变动的情况下,监听描述符设置只需要从用户进程向内核中copy一次。但读者要注意,每调用epoll_ctl函数去更新监听描述符的时候都是一次copy,由于不支持批量操作,每一个描述符都需要执行一次copy,所以如果是频繁的短链接,epoll的效率未必比poll好[4]。每次直接返回准备态的文件描述符集合,不需要轮寻匹配,用户进程处理事件的效率是最高的。

参考地址

原文地址:https://www.cnblogs.com/cnblogs-wangzhipeng/p/9596801.html