select、poll、epoll用法

我们先从著名的C10K问题开始探讨,由于早期在网络还不普及的时候,互联网的用户并不是很多,一台服务器同时在线100个用户估计在当时已经算是大型应用了。但是随着互联网的发展,用户群体迅速的扩大,每一个用户都必须与服务器保持TCP连接才能进行实时的数据交互。Facebook这样的网站同一时间的并发TCP连接可能会过亿。这时候问题就来了。

解决这种问题的思路主要有两个:一个是对于每一个连接处理分配一个独立的进程或线程;另一种就是用同一进程或线程来同时处理若干个连接。

第一种解决方案,即来一个TCP链接,就需要分配一个进程/线程。而进程又是操作系统最昂贵的资源,一台机器无法创建很多进程,比如C10K问题就要创建1万个进程,操作系统是无法承受的。

第二中解决方案,即每个进程/线程同时处理多个连接(IO多路复用),我们讨论的select,poll,epoll都是基于这种思路的。

  • 这种思路最简单的方法是循环挨个处理各个请求,每个连接对应一个socket,当所有的socket的有数据的时候,这种方法是可行的,但当应用读取某个socket的文件数据不ready的时候,整个应用就会阻塞在这里等待该文件句柄,即使别的文件句柄ready,也无法往下处理。
  • select要解决的就是上面的阻塞问题,思路很简单,如果我在读取文件句柄之前,先查看它的状态,ready了就进行处理,不ready就不进行处理,这样就解决了这个问题了
    1. select 使用fd_set结构体来告诉内核同时监控多个文件句柄,当其中有文件句柄的状态发生制定变化或超时,则调用返回。之后使用FD_ISSET来捉个查看是哪个文件句柄发生变化。
    2. fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以毫无疑问一个socket就是一个文件,socket句柄就是一个文件描述符。
    3. fd_set集合可以通过一些宏由人为的来操作,FD_ZERO(fd_set*):清空集合;FD_SET(int, fd_set*):将指定的文件描述符加入到集合;FD_CLR(int, fd_set*):将一个给定的文件描述符从集合中删除;FD_ISSET(int, fd_set*):判断指定的文件描述符是否可以读写。
    4. select函数原型:int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
      1. maxfdp:集合中所有文件描述符的范围,即所有文件描述符的最大值+1;
      2. readfds:指定我们要监视的读变化的文件描述符集合,即我们是否可以从这些文件中读取数据;
      3. writefds:指定我们要监视的写变化的文件描述符集合,即我们是否可以从这些文件中写数据;
      4. errorfds:用来监视文件错误异常;
      5. timeout:select 的超时时间,若将此参数设为NULL,表示阻塞,直到有文件描述符状态发生变化;若将此参数设为0秒0毫秒,表示非阻塞,直接返回,返回值为0表示没有文 件描述符发生变化,返回值为非0表示有文件描述符发生变化;若将此参数设为大于0,表示等待的时间,期间如果有事件发生则返回非0值,如果一直没有事件, 则等待此参数设置的时间,然后返回0。
    5. select可以实现小规模的连接,但当连接数很多的时候,逐个检查状态就很慢了,因此,select往往存在管理的句柄上限(FD_SETSIZE),同时,在使用上,因为只有一个字段记录关注和发生事件,每次调用之前要重新初始化fd_set结构体。
    6. select例子
      1. server.c:
        #include <sys/time.h>
        #include <netinet/in.h>
        #include <sys/select.h>
        #include <sys/types.h>
        #include <sys/socket.h>
        #include <unistd.h>
        #include <stdio.h>
        #include <stdlib.h>
        
        int main(){
            int socket1, socket2;
            struct sockaddr_in socket1addr, socket2addr;
            fd_set fds;
            struct timeval timeout = {3, 0};
            char buffer[256] = {0};
        
            socket1 = socket(PF_INET, SOCK_DGRAM, 0);
            //bzero(&socket1addr, sizeof(socket1addr));
            socket1addr.sin_family = AF_INET;
            socket1addr.sin_addr.s_addr = htonl(INADDR_ANY);
            socket1addr.sin_port = htons(36500);
            bind(socket1, (struct sockaddr*)&socket1addr, sizeof(socket1addr));
        
            socket2 = socket(PF_INET, SOCK_DGRAM, 0);
            //bzero(&socket2addr, sizeof(socket2addr));
            socket2addr.sin_family = AF_INET;
            socket2addr.sin_addr.s_addr = htonl(INADDR_ANY);
            socket2addr.sin_port = htons(36501);
            bind(socket2, (struct sockaddr*)&socket2addr, sizeof(socket2addr));
        
            while(1){
                FD_ZERO(&fds);
                FD_SET(socket1, &fds);
                FD_SET(socket2, &fds);
                int maxfdp = (socket1 > socket2) ? (socket1+1) : (socket2 + 1);
                int retval = select(maxfdp, &fds, NULL, NULL, &timeout);
                if(retval == -1){
                    printf("error\n");
                    return -1;
                }else if(retval == 0){
                    continue;
                }else{
                    struct sockaddr_in client;
                    int len = sizeof(client);
                    if(FD_ISSET(socket1, &fds)){
                        recvfrom(socket1, buffer, 256, 0, (struct sockaddr*)&client, &len);
                        printf("%u says:%s\n", ntohs(client.sin_port), buffer);
                    }
                    if(FD_ISSET(socket2, &fds)){
                        recvfrom(socket2, buffer, 256, 0, (struct sockaddr*)&client, &len);
                        printf("%u says:%s\n", ntohs(client.sin_port), buffer);
                    }
                }
                
            }
        }
        View Code
      2. client.c
        #include <stdio.h>
        #include <string.h>
        #include <sys/socket.h>
        #include <netinet/in.h>
        
        int main(int argc, char **argv)
        {
            if(argc < 2){
                printf("usage: %s port\n", argv[0]);
                return -1;
            }
            int sockfd;
            struct sockaddr_in servaddr;
        
            sockfd = socket(PF_INET, SOCK_DGRAM, 0);
        
            bzero(&servaddr, sizeof(servaddr));
            servaddr.sin_family = AF_INET;
            servaddr.sin_port = htons(atoi(argv[1]));
            servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
        
            char sendline[100];
            sprintf(sendline, "Hello, world!");
        
            sendto(sockfd, sendline, strlen(sendline), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
        
            close(sockfd);
        
            return 1;
        }
        View Code
  • poll解决了select的两个问题,通过一个pollfd数组向内核传递需要关注的事件消除文件句柄上限,同时使用不同字段分别标注关注事件和发生事件,来避免重复初始化。

    poll函数原型:int poll(struct pollfd[], nfds_t nfds, int timeout);

        1)struct pollfd的结构如下:

          struct pollfd{

            int fd;    //文件描述符

            short events;  //请求的事件

            short revents; //返回的事件

          };

         events和revents是通过对代表各种事件的标志进行逻辑或运算构建而成的。其中events是传入参数,revents是传出参数,即revents的填充是由内核来完成的。

         fd代表一个文件描述符,当fd设置为负值时,则忽略events的设置并将revents设置为0,

        2)nfds:要监视的描述符的个数,即pollfd[]的大小。

        3)timeout:指定poll在返回前没有接受事件应该等待的事件。INFTIM表示永远等待,0表示立即返回不阻塞进程,>0表示等待指定时间。

    poll函数事件的标识符值

      

常量说明
POLLIN普通或优先级带数据可读
POLLRDNORM普通数据可读
POLLRDBAND优先级带数据可读
POLLPRI高优先级数据可读
POLLOUT普通数据可写
POLLWRNORM普通数据可写
POLLWRBAND优先级带数据可写
POLLERR发生错误
POLLHUP发生挂起
POLLNVAL描述字不是一个打开的文件

    poll问题:虽然poll解决了select中的两个问题,但是它仍然需要逐个排查所有文件句柄状态,效率不高。

    poll函数例子:

    1. server.c:
       1 #include <stdio.h>
       2 #include <netinet/in.h>
       3 #include <stdlib.h>
       4 #include <sys/socket.h>
       5 #include <sys/types.h>
       6 #include <sys/poll.h>
       7 
       8 int main(){
       9     struct pollfd pollfd[2];
      10     int socket1, socket2;
      11     struct sockaddr_in socket1addr, socket2addr;
      12 
      13     socket1 = socket(PF_INET, SOCK_DGRAM, 0);
      14     socket1addr.sin_family = AF_INET;
      15     socket1addr.sin_addr.s_addr = htonl(INADDR_ANY);
      16     socket1addr.sin_port = htons(8888);
      17     bind(socket1, (struct sockaddr*)&socket1addr, sizeof(socket1addr));
      18     
      19     socket2 = socket(PF_INET, SOCK_DGRAM, 0);
      20     socket2addr.sin_family = AF_INET;
      21     socket2addr.sin_addr.s_addr = htonl(INADDR_ANY);
      22     socket2addr.sin_port = htons(8889);
      23     bind(socket2, (struct sockaddr*)&socket2addr, sizeof(socket2addr));
      24 
      25     pollfd[0].fd = socket1;
      26     pollfd[0].events = POLLIN;
      27     pollfd[1].fd = socket2;
      28     pollfd[1].events = POLLIN;
      29 
      30     while(1){
      31         int numready = poll(pollfd, 2, -1);
      32         if(numready == -1){
      33             break;
      34         }else if(numready == 0){
      35             continue;
      36         }else{
      37             char buffer[256]={0};
      38             struct sockaddr_in client;
      39             int i, len = sizeof(client);
      40             for(i=0; i<2 ; i++){
      41                 if(pollfd[i].revents & POLLIN){
      42                     recvfrom(pollfd[i].fd, buffer, 256, 0, (struct sockaddr*)&client, &len);
      43                     printf("%d says %s\n", ntohs(client.sin_port), buffer);
      44                     numready--;
      45                 }
      46             }
      47         }
      48     }
      49     int i;
      50     for(i=0; i<2; i++){
      51         close(pollfd[i].fd);
      52     }
      53 
      54     return 0;
      55 }
      View Code

       

    2. client.c:
      #include <stdio.h>
      #include <string.h>
      #include <sys/socket.h>
      #include <netinet/in.h>
      
      int main(int argc, char **argv)
      {
          if(argc < 2){
              printf("usage: %s port\n", argv[0]);
              return -1;
          }
          int sockfd;
          struct sockaddr_in servaddr;
      
          sockfd = socket(PF_INET, SOCK_DGRAM, 0);
      
          bzero(&servaddr, sizeof(servaddr));
          servaddr.sin_family = AF_INET;
          servaddr.sin_port = htons(atoi(argv[1]));
          servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
      
          char sendline[100];
          sprintf(sendline, "Hello, world!");
      
          sendto(sockfd, sendline, strlen(sendline), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
      
          close(sockfd);
      
          return 1;
      }
      View Code

 

  • epoll

    既然逐个排查所有文件句柄状态效率不高,很自然的,如果调用返回的时候只给应用提供发生了状态变化的文件句柄,这样效率就会高很多,epoll就是采用了这种设计,使用与大规模   的应用场景。

    epoll的使用:epoll用到三个函数:epoll_create、epoll_ctl、epoll_wait。

    函数原型:

      int epoll_create(int size)

        该函数生成一个epoll专用的文件描述符(即返回值),size指定生成描述符的最大范围。

      int epoll_ctl(int epfd, int op, int fd, struct epoll_event event)

        该函数用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。调用成功返回0,失败返回-1。

        epfd:由epoll_create生成的专用文件描述符

        op:要进行的操作,可能取值:EPOLL_CTL_ADD(注册),EPOLL_CTL_MOD(修改),EPOLL_CTL_DEL(删除)。

        fd:关联的文件描述符

        event:指向epoll_event的指针

      int epoll_wait(int epfd,struct epoll_event   events,int maxevents,int timeout)

        该函数用于轮询事件的发生。返回发生的事件数,-1表示有错误。

        epfd:由epoll_create生成的专用文件描述符

        epoll_event:用于回传待处理事件的数组

        maxevents:每次能处理的事件数

        timeout:等待事件发生的超时值,-1永远等待直到有事件发生。

    数据结构:

    
    typedef union epoll_data {  
        void ptr;  
        int fd;  
        __uint32_t u32;  
        __uint64_t u64;  
    } epoll_data_t;  
      
    struct epoll_event {  
        __uint32_t events;    / Epoll events /  
        epoll_data_t data;    / User data variable /  
    };  
View Code

      结构体epoll_event 被用于注册所感兴趣的事件和回传所发生待处理的事件,其中epoll_data 联合体用来保存触发事件的某个文件描述符相关的数据,例如一个client连接到服务器,      服务器通过调用accept函数可以得到于这个client对应 的socket文件描述符,可以把这文件描述符赋给epoll_data的fd字段以便后面的读写操作在这个文件描述符上进行。          epoll_event 结构体的events字段是表示感兴趣的事件和被触发的事件可能的取值为:

      EPOLLIN :表示对应的文件描述符可以读;
      EPOLLOUT:表示对应的文件描述符可以写;
      EPOLLPRI:表示对应的文件描述符有紧急的数据可读
      EPOLLERR:表示对应的文件描述符发生错误;
      EPOLLHUP:表示对应的文件描述符被挂断;
      EPOLLET:表示对应的文件描述符设定为edge模式;

    使用例子:

      server.c

      
#include <stdio.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(){
    struct epoll_event ev1, ev2, events[20];
    int epfd = epoll_create(10000);
    int socket1, socket2;
    struct sockaddr_in socket1addr, socket2addr;

    socket1 = socket(PF_INET, SOCK_DGRAM, 0);
    socket1addr.sin_family = AF_INET;
    socket1addr.sin_addr.s_addr = htonl(INADDR_ANY);
    socket1addr.sin_port = htons(10240);
    bind(socket1, (struct sockaddr*)&socket1addr, sizeof(socket1addr));

    socket2 = socket(PF_INET, SOCK_DGRAM, 0);
    socket2addr.sin_family = AF_INET;
    socket2addr.sin_addr.s_addr = htonl(INADDR_ANY);
    socket2addr.sin_port = htons(10241);
    bind(socket2, (struct sockaddr*)&socket2addr, sizeof(socket2addr));

    ev1.data.fd = socket1;
    ev1.events = EPOLLIN;
    epoll_ctl(epfd, EPOLL_CTL_ADD, socket1, &ev1);

    ev2.data.fd = socket2;
    ev2.events = EPOLLIN;
    epoll_ctl(epfd, EPOLL_CTL_ADD, socket2, &ev2);

    while(1){
        int nfds = epoll_wait(epfd, events, 20, 1000);
        int i;
        for(i=0; i<nfds; i++){
            char buffer[256];
            struct sockaddr_in client;
            int len = sizeof(client);
            if((events[i].data.fd == socket1) && (events[i].events&&EPOLLIN)){
                recvfrom(socket1, buffer, 256, 0, (struct sockaddr*)&client, &len);
                printf("%u says:%s\n", ntohs(client.sin_port), buffer);
            }else if((events[i].data.fd == socket2) && (events[i].events&&EPOLLIN)){
                recvfrom(socket2, buffer, 256, 0, (struct sockaddr*)&client, &len);
                printf("%u says:%s\n", ntohs(client.sin_port), buffer);
            }
        }
    }
}
View Code

      client.c

      
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(int argc, char **argv)
{
    if(argc < 2){
        printf("usage: %s port\n", argv[0]);
        return -1;
    }
    int sockfd;
    struct sockaddr_in servaddr;

    sockfd = socket(PF_INET, SOCK_DGRAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(atoi(argv[1]));
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    char sendline[100];
    sprintf(sendline, "Hello, world!");

    sendto(sockfd, sendline, strlen(sendline), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));

    close(sockfd);

    return 1;
}
View Code

     Epoll的ET模式与LT模式
      ET(Edge Triggered)与LT(Level Triggered)的主要区别可以从下面的例子看出
      eg:
        1. 标示管道读者的文件句柄注册到epoll中;
        2. 管道写者向管道中写入2KB的数据;
         3. 调用epoll_wait可以获得管道读者为已就绪的文件句柄;
         4. 管道读者读取1KB的数据
         5. 一次epoll_wait调用完成
       如果是ET模式,管道中剩余的1KB被挂起,再次调用epoll_wait,得不到管道读者的文件句柄,除非有新的数据写入管道。如果是LT模式,只要管道中有数据可读,每次调用epoll_wait都会触发。

      另一点区别就是设为ET模式的文件句柄必须是非阻塞的

转载于:https://www.cnblogs.com/sensal/p/5493057.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值