Bootstrap

【Linux】select,poll和epoll

        select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符fd,一旦某个描述符就绪(一般是读就绪或者写就绪),系统会通知有I/O事件发生了(不能定位是哪一个)。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

        select、poll和epoll这三个函数是Linux系统中I/O复用的系统调用函数。I/O复用使得这三个函数可以同时监听多个文件描述符(File Descriptor, FD),因为每个文件描述符相当于一个需要 I/O的“文件”,在socket中共用一个端口。

select

作用:监控多个文件描述符,就绪之后通知调用者

工作原理

 函数

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

参数:

        nfds:参数指定被监听的文件描述符的总数。他通常被设置为select监听所有文件描述符中最大值加1,因为文件描述符是从0开始计数的

         readfds、writefds和exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合

        timeout参数是用来设置select函数的超时时间。

返回值:

        ​ select成功时返回就绪(可读、可写和异常)文件描述符的总数。

        select失败时将返回-1并设置errno。

timeout参数是用来设置select函数的超时时间。它是一个timeval结构类型的指针,其定义如下:

struct timeval{
    long tv_sec;/*秒数*/
    long tv_usec;/*微秒数*/
};

注:当我们给timeout设定了特定的时间后,假设是10s,如果select在10s内调用返回(3s就返回了),那么timeval的成员tv_sec和tv_usec的值将会被替换成剩余的秒数。

内核在使用该数组fd_set的时候采用的是位图的方式,一共有16*8*8=1024个比特位。其实fd_set结构就是一个整数数组,更严格的是,是一个“位图”使用位图中对应的位来表示要监控的文件描述符,如下图所示:

为了方便操的作位图,可使用如下接口来操作fd_set:

从事件集合当中删除一个文件描述符:

void FD_CLR(int fd, fd_set *set);

判断文件描述符是否在某一个事件集合当中:

int FD_ISSET(int fd, fd_set *set);

设置文件描述符到事件集合当中:

void FD_SET(int fd, fd_set *set);

清空事件集合,将所有的比特位全部置为0:

void FD_ZERO(fd_set *set);

 select的优点和缺点

优点:

  • select遵循的是POSIX标准,说明select函数是一个跨平台的函数,既可以在Windows当中运行,也可以在Linux当中运行
  • select在带有超时时间的监控的时候,超时时间的单位可以为微妙

缺点:

  • 监控文件描述符个数的上限为1024
  • 随着文件描述符的增多,select监控效率在下降(本质是select在轮询进行监控)
  • 可读、可写、异常这些事件需要单独的添加到不同的事件集合当中
  • 当select监控成功之后,会从事件集合当中去除掉未就绪的文件描述符,这使程序下一次调用select时,还需要重新添加文件描述符
  • 在每次select进行监控的时候,都会将准备好的事件集合拷贝到内核空间,select返回的时候都会将内核空间拷贝给用户空间
  1 #include<stdio.h>                                                                        
  2 #include<unistd.h>
  3 #include<sys/select.h>
  4                                                     
  5 int main()         
  6 {        
  7     fd_set readfds;          
  8     FD_ZERO(&readfds);
  9     FD_SET(0,&readfds);
 10 
 11     while(1)                 
 12     {                             
 13         int ret = select(1,&readfds,NULL,NULL,NULL);
 14         if(ret < 0)        
 15         {
 16             perror("select");
 17             return 0;
 18         }
 19 
 20         char buf[1024] = {0};
 21         read(0,buf,sizeof(buf)-1);
 22                                                     
 23         printf("%s\n",buf);
 24     }    
 25                              
 26     return 0;        
 27 }                              

poll

        可以认为poll是一个增强版本的select,因为select的比特位操作决定了一次性最多处理的读或者写事件(文件描述符数量)只有1024个,而poll使用一个新的方式优化了这个模型。
        poll底层操作的数据结构pollfd:

struct pollfd {
    int fd;          // 需要监视的文件描述符
    short events;    // 需要内核监视的事件
    short revents;   // 实际发生的事件
};

函数

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

参数:

        fds:想让poll监控多个文件描述符,只需要在定义事件结构数组的时候,多传递几个元素
eg:struct pollfd arr[10];
  arr[0].fd = 0;
  arr[0].events = POLLIN;
nfds:事件结构数组中有效元素的个数
timeout:
  >0:带有超时时间,单位:秒
  ==0:非阻塞
  <0:阻塞
返回值:就绪文件描述符的个数

        内核修改的是revents的值,events是我们需要关注的文件描述符,revents是内核检测到文件描述符中有数据的而进行修改后的内容,

poll的优点和缺点

优点:

  • 提出了事件结构的方式,在给poll函数传递参数的时候,不需要分别添加到“事件集合”中
  • 事件结构数组的大小可以根据程序员自己进行定义,并没有上限要求
  • 不用在监控到就绪文件描述符之后,重新添加文件描述符

缺点:

  • 不支持跨平台
  • 内核对事件结构数组的监控也是采用轮询遍历的方式,即随着监控文件描述符的增多,监控效率会下降
  • 每次调用poll都需要把大量的pollf结构从用户态拷贝到内核态,poll返回的时候,会将内核空间拷贝给用户空间(从内核态到用户态会调用do_signal会有开销)
  1 #include<stdio.h>
  2 #include<poll.h>
  3 #include<unistd.h>
  4 
  5 int main()
  6 {
  7     struct pollfd arr[10];                                                               
  8 
  9     arr[0].fd = 0;
 10     arr[0].events = POLLIN;
 11 
 12     while(1)
 13     {
 14         int ret = poll(arr,1,-1);
 15         if(ret < 0)
 16         {
 17             perror("poll");
 18             return 0;
 19         }
 20         for(int i = 0;i < 10;++i)
 21         {
 22             if(arr[i].revents == POLLIN)
 23             {              
 24                 char buf[1024] = {0};
 25                 read(arr[i].fd,buf,sizeof(buf)-1);
 26                 printf("%s\n",buf);
 27             }                    
 28         }          
 29     }    
 30     return 0;              
 31 }                                          

epoll

        epoll给出了一个新的模式,直接申请一个epollfd的文件,对这些进行统一的管理,初步具有了面向对象的思维模式。可理解为event poll,epoll会把哪个流发生哪种I/O事件通知我们。所以epoll是事件驱动(每个事件关联fd)的,此时我们对这些流的操作都是有意义的。复杂度也降低到了O(1)。

        epoll通过在Linux内核中申请一个简易的文件系统。把原先的select/poll调用分成了3个部分:

  • 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源);
  • 调用epoll_ctl向epoll对象中添加这些连接的套接字;
  • 调用epoll_wait收集发生的事件的连接.

         如此一来只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这些连接的句柄数据,内核也不需要去遍历全部的连接。

 工作原理

        当调用epoll_create函数时,会在内核创建一个eventpoll结构体,在该结构体中有一个rdlist成员和rbr成员,它两分别是一个双向链表和红黑树,而调用epoll_ctl函数添加、修改、删除文件描述符对应的事件集合其实是对红黑树中的节点进行相应的添加、修改、删除操作,而所有添加到epoll的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当文件描述符准备就绪后,内核会回调ep_poll_callback函数,将准备就绪的事件集合添加到rdlist双向链表中,而当调用epoll_wait进行监控的时候,如果双向链表为空,则表明当前没有就绪的事件发生,如果不为空,则将双向链表中的内容复制到用户态,并返回将事件数量返回给用户。

函数

epoll_create

//创建一个epoll模型
int epoll_create(int size);

参数:

         size参数自从Linux2.6.8之后,size参数是被忽略的,但size的值必须设置为大于0的值。size只是给内核一个提示,告诉它事件需要多大。仅仅只是给内核一个建议,具体多大还是操作系统说了算。

返回值:

        epoll模型创建成功返回其对应的文件描述符,否则返回-1,同时错误码会被设置。
        该函数返回的文件描述符将作为其他epoll系统调用的第一个参数,以指定要访问的内核事件表。
        当不再使用时,必须调用close函数关闭epoll模型对应的文件描述符,当所有引用epoll实例的文件描述符都已关闭时,内核将销毁该实例并释放相关资源。

epoll_ctl

//向指定的epoll模型中注册事件
int epoll_ctl(int epfd,int op, int fd, struct epoll_event *event);

参数:

        epfd:epoll操作句柄
        op:告诉epoll要做什么是事
         ① EPOLL_CTL_ADD:添加一个文件描述符对应的事件结构到epoll当中
         ② EPOLL_CTL_MOD:修改一个文件描述符的事件结构
         ③ EPOLL_CTL_DEL:从epoll当中删除一个文件描述符对应的事件结构
        fd:待处理(添加、修改、删除)的文件描述符
        event:文件描述符对应的事件结构
        epoll_event结构体

 

返回值:

​         epoll_ctl成功时返回0,失败则返回-1并设置errno。

epoll_wait

//用于收益监视的事件中已经就绪的事件
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout)

 参数:

        epfd:epoll的操作句柄

        events:时间结构数组(集合),从epoll当中获取就绪的事件结构

        maxevents:最多一次获取多少个事件结构
        timeout:
          0:带有超时事件
          ==0:非阻塞
          <0:阻塞

返回值:就绪的文件描述符个数

epoll的优点和缺点

优点:

  • 事件回调机制,当文件描述符就绪之后,会调用回调函数将事件结构复制到双向链表中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
  • 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
  • 没有数量限制: 文件描述符数目无上限.

LT和ET模式

        首先,select、poll和epoll的工作模式默认情况下都是LT模式。但是epoll的工作模式有两种,分别为LT模式(水平触发模式)和 ET 模式(边缘触发模式)。

LT模式:举一个例子,张三买了很多快递,快递站的小王每隔一段时间通知张三取快递(张三没有去取快递或没取完的情况下,就会一直通知,这不就是轮询嘛)
ET模型:举一个例子,张三买了很多快递,快递站的小李会通知张三取快递,但是只通知一次(张三没有去取快递或没取完的情况下,小李在也不会通知了)

​         epoll默认是LT模式,我们要将其改为ET模式,只需要将其时间或上一个EPOLLET,在刚才的代码中有体现,可以尝试运行此服务器,测试其效果(我们测试时,只要关心监听套接字就足够了,其将accept函数部分注释掉。然后LT模式下,就会发现,当套接字有读事件就绪后,会不断的触发提示,告诉你有事件就绪;ET模式只会通知一次)

总结

;