Missmiaom
10/31/2019 - 12:02 PM

select vs poll vs epoll

select

select()系统调用提供了一种用于实现同步多路复用I / O的机制。

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

对select()的调用将一直阻塞,直到给定的文件描述符准备执行I / O为止,或者直到经过可选的指定超时为止

监视的文件描述符分为三组

  • 监视readfds集中列出的文件描述符,以查看是否有数据可读取。
  • 监视writefds集中列出的文件描述符,以查看写操作是否将完成而不会阻塞。
  • 监视exceptionfds集中的文件描述符,以查看是否发生了异常或带外数据是否可用(这些状态仅适用于套接字)。

给定的集合可以为NULL,在这种情况下,select()不会监视该事件。

成功返回后,将修改每个集合,以使其仅包含准备好由该集合描述的I / O类型的文件描述符。

例:

#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 \n", 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;
}

我们首先创建5个子进程,每个进程都连接到服务器并将消息发送到服务器。服务器进程使用accept(2)为每个客户端创建一个不同的文件描述符。select(2)中的第一个参数应该是三个集合中编号最高的文件描述符,加上1,因此我们检查了最大fd num

主无限循环创建一组所有文件描述符,调用选择并在返回时检查哪个文件描述符已准备好读取。为简单起见,我没有添加错误检查

返回时,select将集合更改为仅包含已准备好的文件描述符,因此我们需要在每次迭代时再次构建该集合。

我们需要告诉select编号最高的文件描述符,这是fd_set的内部实现。每个fd都用一个编码表示,因此fd_set是一个由32个整数组成的数组(32 * 32bit = 1024位)。该功能讲检查最大编码以内的所有编码。这意味着如果我们有5个文件描述符,但最大编码是900,则select将检查0到900之间的编码以找到要监听的文件描述符。有一个posix替代选择– pselect,它在等待时添加信号掩码(请参见手册页)

select 摘要:

  • 我们需要在每次监听之前建立每个集合
  • select检查最高位编码以内– O(n)的所有编码
  • 我们需要遍历文件描述符以检查它是否存在于select返回的集合中
  • select的主要优点是它具有很高的可移植性-像OS一样的每个Unix都有

poll

与select()不同,因为select()具有效率低下的三个基于位掩码的文件描述符集,poll()使用nfds pollfd结构的单个数组。原型更简单:

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

pollfd结构的事件和返回事件具有不同的字段,因此我们不必每次都构建它:

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

对于每个文件描述符,构建一个类型为pollfd的对象,并填充所需的事件。poll 返回后,请检查revents字段

要将上面的示例更改为使用poll:

 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);
        }
    }
  }

就像我们对select所做的那样,我们需要检查每个pollfd对象以查看其文件描述符是否已准备就绪,但是我们不需要在每次迭代时都构建集合

select 与 poll

  • poll()不需要用户计算编号最高的文件描述符的值+1
  • poll()对于大值文件描述符更有效。想象一下,通过select()观察值为900的单个文件描述符,内核将必须检查每个传入集合的每个位,直到第900位。
  • select()的文件描述符集是静态大小的。
  • 使用select(),文件描述符集将在返回时重建,因此每个后续调用都必须重新初始化它们。poll()系统调用将输入(event字段)与输出(revents字段)分开,从而允许集合被重复使用而无需更改。
  • 超时参数select()是未定义的回报。可移植代码需要重新初始化它。这不是pselect()的问题
  • 由于某些Unix系统不支持poll(),因此select()具有更高的可移植性。

epoll

在使用select和poll时,我们将管理用户空间中的所有内容,并在每次呼叫时发送设置以等待。要添加另一个套接字,我们需要将其添加到集合中,然后再次调用select / poll。

epoll 系统调用可帮助我们在内核中创建和管理上下文。我们将任务分为3个步骤:

  • 使用epoll_create在内核中创建上下文
  • 使用epoll_ctl向/从上下文中添加和删除文件描述符
  • 使用epoll_wait等待上下文中的事件

让我们将上面的示例更改为使用epoll:

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_event对象并将其添加到上下文中,并且在无限循环中,我们仅在上下文中等待。

epoll 与 select/poll

  • 我们可以在等待时添加和删除文件描述符
  • epoll_wait仅返回具有就绪文件描述符的对象
  • epoll具有更好的性能-O(1)代替O(n)
  • epoll可以表现为水平触发或边缘触发(请参见手册页)
  • epoll是特定于Linux的,因此不可移植