IO多路复用之select poll epoll

夙愿已清 提交于 2020-03-24 11:31:37

参考文档:

http://blog.csdn.net/tennysonsky/article/details/45745887

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

与多线程和多进程相比,I/O 多路复用的最大优势是系统开销小,系统不需要建立新的进程或者线程,也不必维护这些线程和进程。

select

  • 监视并等待多个文件描述符的属性变化(可读、可写或错误异常)
  • select()函数监视的文件描述符分 3 类,分别是writefds、readfds、和 exceptfds
  • 调用后 select() 函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有错误异常),或者超时( timeout 指定等待时间),函数才返回
  • 当 select()函数返回后,可以通过遍历 fdset,来找到就绪的描述符
       int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

参数:

  • nfds: 要监视的文件描述符的范围,一般取监视的描述符数的最大值+1,如这里写 10, 这样的话,描述符 0,1, 2 …… 9 都会被监视,在 Linux 上最大值一般为1024
  • readfd: 监视的可读描述符集合,只要有文件描述符即将进行读操作,这个文件描述符就存储到这
  • writefds监视的可写描述符集合
  • exceptfds: 监视的错误异常描述符集合
  • timeout:超时时间,它告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数
这个参数有三种可能       
          1)永远等待下去:仅在有一个描述字准备好 I/O 时才返回。为此,把该参数设置为空指针 NULL
2)等待固定时间:在指定的固定时间内(timeval 结构中指定的秒数和微秒数)内,在有一个描述字准备好 I/O 时返回,如果时间到了,就算没有文件描述符发生变化,这个函数会返回 0。
3)根本不等待(不阻塞)检查描述字后立即返回,这称为轮询。为此,struct timeval变量的时间值指定为 0 秒 0 微秒,文件描述符属性无变化返回 0,有变化返回准备好的描述符数量。

返回值:

成功:就绪描述符的数目,超时返回 0
出错:-1

poll:

 select() 和 poll() 系统调用的本质一样,前者在 BSD UNIX 中引入的,后者在 System V 中引入的。poll() 的机制与 select() 类似,与 select() 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件描述符数量的限制但是数量过大后性能也是会下降)。poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

  • 监视并等待多个文件描述符的属性变化
  • 与select不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次   
  • poll的实现机制与select类似,其对应内核中的sys_poll,只不过poll向内核传递pollfd数组,然后对pollfd中的每个描述符进行poll,相比处理fdset来说,poll效率更高
  • poll返回后,需要对pollfd中的每个元素检查其revents值,来得指事件是否发生。
   #include <poll.h>
   int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数:

  • fds: 不同与 select() 使用三个位图来表示三个 fdset 的方式,poll() 使用一个 pollfd 的指针实现。一个 pollfd 结构体数组,其中包括了你想测试的文件描述符和事件, 事件由结构中事件域 events 来确定,调用后实际发生的时间将被填写在结构体的 revents 域。
struct pollfd{
        int fd;         /* 文件描述符 */
        short events;   /* 等待的事件 */
        short revents;  /* 实际发生了的事件 */
    }; 

events:每个结构体的 events 域是监视该文件描述符的事件掩码,由用户来设置这个域。events 等待事件的掩码取值如下:

  1. 处理输入
    • POLLIN 普通或优先级带数据可读
    • POLLRDNORM 普通数据可读
    • POLLRDBAND 优先级带数据可读
    • POLLPRI 高优先级数据可读
  2. 处理输出
    • POLLOUT 普通或优先级带数据可写
    • POLLWRNORM 普通数据可写
    • POLLWRBAND 优先级带数据可写
  3. 处理错误
    • POLLERR发生错误
    • POLLHUP发生挂起
    • POLLVAL 描述字不是一个打开的文件

poll() 处理三个级别的数据,普通 normal,优先级带 priority band,高优先级 high priority,这些都是出于流的实现。

POLLIN | POLLPRI 等价于 select() 的读事件,POLLOUT | POLLWRBAND 等价于 select() 的写事件。POLLIN 等价于 POLLRDNORM | POLLRDBAND,而 POLLOUT 则等价于 POLLWRNORM 。

  • nfds: 用来指定第一个参数数组元素个数
  • timeout: 指定等待的毫秒数,无论 I/O 是否准备好,poll() 都会返回。当等待时间为 0 时,poll() 函数立即返回,为 -1 则使 poll() 一直阻塞直到一个指定事件发生。

返回值:

  • 成功时,poll() 返回结构体中 revents 域不为 0 的文件描述符个数;如果在超时前没有任何事件发生,poll()返回 0
  • 失败时,poll() 返回 -1,并设置 errno 为下列值之一:
    • EBADF:一个或多个结构体中指定的文件描述符无效。
    • EFAULT:fds 指针指向的地址超出进程的地址空间。
    • EINTR:请求的事件之前产生一个信号,调用可以重新发起。
    • EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
    • ENOMEM:可用内存不足,无法完成请求。

epoll

 epoll 是在 2.6 内核中提出的,是之前的 select() 和 poll() 的增强版本

 #include <sys/epoll.h>
 int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); 
  • epoll是Linux特有的I/O复用函数。它在实现上与select、poll有很大的差异。epoll使用一组函数来完成任务,而不是单个函数
  • epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无需像select和poll那样每次调用都要重复传入文件的事件放在内核里的一个事件表中。但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表;
  • epoll通过epoll_create创建一个用于epoll轮询的描述符,通过epoll_ctl添加/修改/删除事件,通过epoll_wait检查事件,epoll_wait第二个参数用于存放结果
  • epollselectpoll不同,首先,其不用每次调用都向内核拷贝事件描述信息,在第一次调用后,事件信息就会与对应的epoll描述符关联起来。另外epoll不是通过轮询,而是通过在等待的描述符上注册回调函数,当事件发生时,回调函数负责把发生的事件存储在就绪事件链表中,最后写到用户空间
  • epoll返回后,该参数指向的缓冲区中即为发生的事件,对缓冲区中每个元素进行处理即可,而不需要像pollselect那样进行轮询检查
  • epoll 对文件描述符的操作有两种模式

          LT 模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait 时,会再次响应应用程序并通知此事件

ET 模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件

select、poll、epoll 总结:
  • 支持一个进程所能打开的最大连接数

select

单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上FD_SETSIZE为32*64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。

poll

poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的

epoll

FD 上限是最大可以打开文件的数目,举个栗子,在1gb内存的机器上大约是10w左右

  • FD剧增后带来的IO效率问题

select

因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。

poll

同上

epoll

因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的cpu时间。这就是回调机制带来的性能提升,但是所有socket都很活跃的情况下,可能会有性能问题。

  • 消息传递方式

select

每次调用 select(),都需要把 fd 集合从用户态拷贝到内核态

poll

同上

epoll

使用mmap加速内核与用户空间的消息传递。mmap:将一个文件或者其他对象映射进内存。mmap底层是使用红黑树加队列实现的,每次需要在操作的fd,先在红黑树中拿到,放到队列中,那么用户收到epoll_wait消息以后只需要看一下消息队列中有没有数据,有就取走

  • 水平触发与边缘触发
    • 水平触发:如果文件描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知.允许在任意时刻重复检测IO的状态,没有必要每次描述符就绪后尽可能多的执行IO.select,poll就属于水平触发.
    • 边缘触发:如果文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知.在收到一个IO事件通知后要尽可能多的执行IO操作,因为如果在一次通知中没有执行完IO那么就需要等到下一次新的IO活动到来才能获取到就绪的描述符.信号驱动式IO就属于边缘触发.

select

水平触发

poll

水平触发

epoll

既可以采用水平触发,也可以采用边缘触发

 

  • 跨平台

select

几乎在所有的平台上支持

poll

-

epoll

目前只支持linux?

 

综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。

  • 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调
  • select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!