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:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *timeout_ts, const sigset_t *sigmask);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
- 与select不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次。
- poll的实现机制与select类似,其对应内核中的sys_poll,只不过poll向内核传递pollfd数组,然后对pollfd中的每个描述符进行poll,相比处理fdset来说,poll效率更高。
- poll返回后,需要对pollfd中的每个元素检查其revents值,来得指事件是否发生。
epoll
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
- epoll是Linux特有的I/O复用函数。它在实现上与select、poll有很大的差异。epoll使用一组函数来完成任务,而不是单个函数
- epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无需像select和poll那样每次调用都要重复传入文件的事件放在内核里的一个事件表中。但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表;
- epoll通过epoll_create创建一个用于epoll轮询的描述符,通过epoll_ctl添加/修改/删除事件,通过epoll_wait检查事件,epoll_wait的第二个参数用于存放结果。
- epoll与select、poll不同,首先,其不用每次调用都向内核拷贝事件描述信息,在第一次调用后,事件信息就会与对应的epoll描述符关联起来。另外epoll不是通过轮询,而是通过在等待的描述符上注册回调函数,当事件发生时,回调函数负责把发生的事件存储在就绪事件链表中,最后写到用户空间。
- epoll返回后,该参数指向的缓冲区中即为发生的事件,对缓冲区中每个元素进行处理即可,而不需要像poll、select那样进行轮询检查。
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。
一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.
2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:
当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll:
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。 2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
epoll:
epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
epoll的优点:
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
select、poll、epoll 区别总结:
1、支持一个进程所能打开的最大连接数
select |
单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上FD_SETSIZE为32*64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。 |
poll |
poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的 |
epoll |
虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接 |
2、FD剧增后带来的IO效率问题
select |
因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。 |
poll |
同上 |
epoll |
因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。 |
3、 消息传递方式
select |
内核需要将消息传递到用户空间,都需要内核拷贝动作 |
poll |
同上 |
epoll |
epoll通过内核和用户空间共享一块内存来实现的。 |
总结:
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。
1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善