select
select
可以完成非阻塞方式的工作的程序,它能够监视需要被监视的文件描述符的变化情况 — 读、写或异常
select 函数原型1
int select(int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);
主要涉及到两个结构体:fd_set
和 timeval
- 结构体
fd_set
可以理解为一个集合,这个集合中存放的是文件描述符(file description
),即文件句柄,这可以认为是常说的普通意义的文件;当然 UNIX 下任何设备、管道、FIFO等都是文件形式,所以毫无疑问,一个 socket 就是一个文件, socket 句柄就是一个文件描述符。fd_set 集合可以通过一些宏由人为来操作,比如以下代码1
2
3
4
5
6
7
8
9fd_set set;
// 将set清零
FD_ZERO(&set);
// 将fd加入set
FD_SET(fd, &set);
// 将fd从set中清除
FD_CLR(fd, &set);
// 如果fd在set中则真
FD_ISSET(fd, &set); - 结构体
timeval
是一个常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个是毫秒数
select
的各个参数所表示的含义
maxfdp
是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1readfds
是指向 fd_set 结构的指针,这个集合中应该包括文件描述符。因为要监视文件描述符的读变化的,即关心是否可以从这些文件中读取数据,如果这个集合中有一个文件可读,select 就会返回一个大于 0 的值,表示有文件可读。如果没有可读的文件,则根据timeout
参数再判断是否超时:若超出 timeout 的时间,select 返回 O;若发生错误返回负值;也可以传入 NULL 值,表示不关心任何文件的读变化writefds
是指向 fd_set 结构的指针,这个集合中应该包括文件描述符。因为要监视文件描述符的写变化的,即关心是否可以向这些文件中写入数据,如果这个集合中有一个文件可写, select 就会返回一个大于 0 的值,表示有文件可写。 如果没有可写的文件,则根据timeout
参数再判断是否超时:若超出 timeout 的时间,select 返回 O;若发生错误返回负值;也可以传入 NULL 值,表示不关心任何文件的写变化。errorfds
同上面两个参数的意图,用来监视文件错误异常timeout
是 select 的超时时间,这个参数至关重要,它可以使 select 处于 3 种状态- 若将 NULL 以形参传入,即不传入时间结构,就是将 select 置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止
- 若将时间值设为0,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值
- timeout 的值大于0,这就是等待的超时时间,即 select 在 timeout 时间内阻塞,超时时间之内有事件到来就返回,否则在超时后不管怎样一定返回,返回值同上述
- 返回值:准备就绪的描述符数,若超时则返回 0,若出错则返回 -1
poll
和 select
函数一样,poll
函数也可以用于执行多路复用 IO
poll 函数原型1
2
int poll(struct pollfd* fds, unsigned int nfds, int timeout);
pollfd 结构体
定义1
2
3
4
5struct pollfd {
int fd; // 文件描述符
short events; // 等待的事件
short revents; // 实际发生了的事件
};
每个 pollfd结构体
指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()
监视多个文件描述符。每个结构体的 events 域是监视文件描述符的事件掩码,由用户来设置这个域的属性。revents 域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域;并且 events 域中请求的任何事件都可能在 revents 域中返回。具体的事件代码和代表含义如下所示
nfds
用于标记数组fds中的struct pollfd结构元素的总数量
使用poll()
和select()
不一样,不需要显式地请求异常情况报告。
POLLIN | POLLPRI
等价于 select()的读事件
,POLLOUT | POLLWRBAND
等价于 select()的写事件
;POLLIN
等价于 POLLRDNORM | POLLRDBAND
,而 POLLOUT
则等价于 POLLWRNORM
。例如,要同时监视一个文件描述符是否可读或可写,可以设置 events 为 POLLIN | POLLOUT
。在 poll 返回时,只要检查 revents 中的标志,获得对应于文件描述符请求的 events 结构体。如果 POLLIN
事件被设置,则文件描述符可以被读取而不阻塞;如果 POLLOUT
被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的,它们可能被同时设置,表示这个文件描述符的读取和写人操作都会正常返回而不阻塞。
timeout
参数指定等待的毫秒数,无论 IO 是否准备号,poll 都会返回。timeout
指定负数值时表示无限超时,使poll()
一直挂起直到一个指定事件发生;timeout
为0指示poll
调用立即返回并列出准备好 IO 的文件描述符,但并不等待其他的事件。这种情况下,poll()
的返回值,一旦被选举出来,立即返回。成功时,poll()
返回结构体中 revents 域不为0的文件描述符个数,如果在超时前没用任何事件发生,poll()
返回0.失败时,poll()
返回-1,并设置 errno 为下列值之一
EBADF
: 一个或多个结构体中指定的文件描述符无效EFAULTfds
: 指针指向的地址超出进程的地址空间EINTR
: 请求的事件之前产生一个信号,调用可以重新发起EINVALnfds
: 参数超出PLIMIT_NOFILE
值ENOMEM
: 可用内存不足,无法完成请求
epoll
epoll
是在 Linux 2.6 内核中提出的,是之前select
和poll
的增强版本。相对于select
和poll
来说,epoll
更加灵活,没用描述符的限制。epoll
使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间之间的数据拷贝只需一次
epoll 接口
使用 epoll 必须包含下面这个头文件1
epoll 操作过程需要3个接口,分别如下1
2
3int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
接下来介绍这3个接口的功能1
int epoll_create(int size);
创建一个 epoll 句柄, size 用来告诉内核要监听的数目。这个参数不同于select()
中的第一个参数,是最大监听的 fd+1 的值。需要注意的是,当创建好 epoll 句柄后,它就会占用一个 fd 值,在 Linux 下如果查看 /proc/进程的id/fd/
,是能够看到这个 fd 值的,所以在使用 epoll 后,必须调用close()
关闭,否则可能导致 fd 耗尽
1 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); |
epoll 的事件注册函数,它不同于select()
在监听事件时告诉内核要监听数目类型的事件,而是先注册要监听的事件类型。第一个参数是epoll_create()
的返回值,第二个参数表示动作,用3个宏表示
EPOLL_CTL_ADD
,注册新的 fd 到epfd 中EPOLL_CTL_MOD
,修改已经注册的 fd 的监听事件EPOLL_CTL_DEL
,从 epfd 中删除一个fd
第3个参数是需要监听的 fd,第4个参数是告诉内核需要监听什么事,struct epoll_event
结构events 可以是以下几个宏的集合1
2
3
4struct epoll_event {
__unit32_t events; // epoll events
epoll_data_t data; // user data variable
}EPOLLIN
,表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭)EPOLLOUT
,表示响应的文件描述符可以写EPOLLPRI
,表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)EPOLLERR
,表示对应的文件描述符发送错误EPOLLHUB
,表示对应的文件描述符被挂断EPOLLET
,将 EPOLL 设为边缘触发(Edge Triggered
)模式,这是相对于公平触发(Level Triggered
)来说的EPOLLONESHOT
,只监听一次事件,当监听完这次事件后,如果还需要继续监听这个 socket 的话,需要在此把这个 socket 加入到 EPOLL 队列里
1 | int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); |
等待事件的产生,类似于select()
调用。参数 events 用来从内核得到事件的集合, maxevents 告诉内核这个 events 有多大,且 maxevents 的值不能大于创建epoll_create()
时的 size,参数 timeout 是超时事件(ms为单位,0会立即返回,-1将不确定或称永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
select、poll和epoll的区别
select
、poll
和epoll
都是多路 IO 复用的机制。多路 IO 复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select
、poll
和epoll
本质上都是同步IO
,因为它们都需要在读写事件就绪后自己负责进行读写,即是阻塞的,而异步IO
则无须自己负责进行读写,异步I/O
的实现会负责把数据从内核拷贝到用户空间。
3种多路IO复用对比
select()
和poll()
poll()
不要求开发者在计算最大文件描述符时进行 +1 操作poll()
在应付大数目的文件描述符的时候速度更快,因为对于select()
来说内核需要检查大量描述符对应的 fd_set 中的每一个比特位,比较费时select()
可以监控的文件描述符数目是固定的,相对来说也较少(1024或2048)。如果需要监控数值比较大的文件描述符,或是分布得很稀疏得较少的描述符,效率也会很低。而对于poll()
函数来说,就可以创建特定大小的数组来保存监控的描述符,而不受文件描述符值大小的影响,而且poll()
可以监控的文件数目远大于- 对于
select()
来说,所监控的 fd_set 在select()
返回之后会发生变化,所以在下一次进入select()
之前都需要重新初始化需要监控的 fd_set,poll()
函数将监控的输入和输出事件分开,允许被监控的文件数组被复用而不需要重新初始化 select()
函数的超时参数在返回时也是未定义的,考虑到可移植性,每次在超时之后在下一次进入到select()
之前都需要重新设置超时参数
select()
的优点select()
的可移植性更好,在某些 UNIX 系统上不支持poll()
select()
对于超时值提供了更好的精度,而poll()
时精度较差
epoll()
的优点- 支持一个进程打开大数目的 socket 描述符(FD)
select()
最不能忍受的是一个进程所打开的 FD 是有一定限制的,由 FD_SETSIZE 的默认值是 1024/2048。对于那些需要支持上万连接数目的 IM 服务器来说显然太少了。这时候可以选择修改这个宏然后重新编译内核。不过 epoll 则没有这个限制,它所支持的 FD 上限是最大可以打开文件的数目,这个数字一般远大于 2048。举个例子,在 lGB 内存的空间中这个数字一般是 10 万左右,具体数目可以使用cat /proc/sys/fs/file-max
查看,一般来说这个数目和系统内存关系很大。 - IO效率不随FD数目增加而线性下降
传统的 select/poll 另一个致命弱点就是当你拥有一个很大的 socket 集合,不过由于网络延迟,任一时间只有部分的 socket 是“活跃”的,但是 select/poll 每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是 epoll 不存在这个问题,它只会对“活跃”的 socket 进行操作 — 主是因为在内核中实现 epoll 是根据每个 fd 上面的 callback 函数实现的。 那么,只有“活跃”的 socket 才会主动去调用 callback 函数,其他 idle 状态 socket 则不会,在这点上,epoll 实现了一个“伪” AIO,因为这时候推动力由 Linux 内核提供 - 使用 mmap 加速内核与用户空间的信息传递
这点实际上涉及 epoll 的具体实现。无论是 select、poll 还是 epoll 都需要内核把消息通知给用户空间,如何避免不必要的内存拷贝就显得尤为重要。在这点上, epoll 是通过内核与用户空间 mmap 处于同一块内存实现的
对于 poll 来说需要将用户传入的 pollfd 数组拷贝到内核空间,因为拷贝操作和数组长度相关,时间上来看,这是一个 $O(n)$ 操作,当事件发生后,poll 将获得的数据传送到用户空间,并执行释放内存和剥离等待队列等工作,向用户空间拷贝数据与剥离等待队列等操作的时间复杂度同样是$O(n)$。
- 支持一个进程打开大数目的 socket 描述符(FD)