美文网首页
select/poll/epool linux多路复用解析

select/poll/epool linux多路复用解析

作者: ben大福 | 来源:发表于2020-07-06 18:52 被阅读0次

    Linux(实际上市Unix)的一个基本概念是Unix/Linux中一切都是文件。每个进程都有一个指向文件,套接字,设备或其他操作系统对象的文件描述符

    与许多IO源一起工作的典型系统都要经历一个初始化阶段,然后进入某种待机模式------等待客户端发送请求并响应


    image.png

    linux下有3种方案轮训一组文件描述符.

    • select
    • poll
    • 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 集合中列出的文件描述符将被监视,以查看写入操作是否会在没有阻塞的情况下完成;
    exceptfds 集合中的文件描述符将被监视,以查看是否发生了异常,或者带外数据是否可用(这些状态仅适用于套接字)。

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <sys/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,就可以知道最大的文件描述符编号。

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

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

    Select – summary:

    • 我们需要在每次调用之前构建每组集合;
    • 这个函数检查任何 bit 到更高的数字 —— O(n);
    • 我们需要遍历文件描述符来检查它是否存在于从 select() 返回的集合中;
    • select 的主要优点在于它的可移植性 —— 每个类 unix 操作系统的都有。

    Poll 系统调用

    不像 select() 低效的三个基于位掩码的文件描述符集合,poll() 采用了一个 nfds pollfd 结构的单个数组,函数原型更简单:

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

    pollfd 结构对事件和返回事件有不同的字段,所以我们不需要每次都创建它:

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

    修改上面的例子:

    
    #include <errno.h>
    #include <netinet/in.h>
    #include <poll.h>
    #include <signal.h>
    #include <stdio.h>
    #include <sys/select.h>
    #include <sys/socket.h>
    #include <sys/time.h>
    #include <sys/types.h>
    #include <sys/wait.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);
    
      struct pollfd pollfds[5];
      for (int 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);
          }
        }
      }
      return 0;
    }
    

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

    Poll vs Select

    poll() 不要求用户计算编号最高的文件描述符 +1 的值;
    poll() 对于大值文件描述符更有效。假设我们通过 select() 方法监视一个值为 900 的单个文件描述符 —— 内核将不得不检查传入集合的每个值的每一位,直到第 900 位;
    select() 的文件描述符集合是静态大小的;
    使用 select(),文件描述符集合会在返回时重建,因此每个后续调用都必须重新初始化它们。 poll() 系统调用将输入(events 字段)与输出(revents 字段)分隔开,允许在不更改的情况下重新使用该数组。
    返回时,select() 的 timeout 参数未定义。 可移植性代码需要重新初始化它,这不是pselect() 的问题;
    select() 更具可移植性,因为某些 Unix 系统不支持 poll()。

    Epoll 系统调用

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

    Epoll* 系统调用帮助我们创建和管理内核中的上下文,我们将任务分为 3 个步骤:

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

    将上面的示例改用 epoll:

    
    #include <errno.h>
    #include <netinet/in.h>
    #include <signal.h>
    #include <stdio.h>
    #include <sys/select.h>
    #include <sys/socket.h>
    #include <sys/time.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include <sys/epoll.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];
      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);
    
    struct epoll_event events[5];
    int epfd = epoll_create(10);
    int nfds;
    
    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);
        printf("ngfs = {%d}  \n", nfds);
        for(i=0;i<nfds;i++) {
                memset(buffer,0,MAXBUF);
                read(events[i].data.fd, buffer, MAXBUF);
                puts(buffer);
        }
      }
    
      return 0;
    }
    

    Epoll vs Select/Poll

    • 我们可以在等待时添加或删除文件描述符;
    • epoll_wait 仅返回具有准备文件描述符的对象;
    • epoll 有更好的性能 —— O(1) 而不是O(n);
    • epoll 可以表现为级别触发或边缘触发(请参见手册页);
    • epoll 是 Linux 特有的,因此可移植性一般。

    参考:
    https://devarea.com/linux-io-multiplexing-select-vs-poll-vs-epoll/#.XwMCHJMzZTY

    相关文章

      网友评论

          本文标题:select/poll/epool linux多路复用解析

          本文链接:https://www.haomeiwen.com/subject/cfygqktx.html