63.1 整体概览
- IO多路复用(
select()
以及poll()
系统调用) - 信号驱动IO
- Linux专有的
epoll
编程接口
实际上IO多路复用、信号驱动IO以及epoll都是用来实现一个目标的技术——同时检查多个文件描述符,看它们是否准备好了执行IO操作。
文件描述符就绪状态的转化是通过一些IO事件来触发的,比如输入数据到达,套接字连接建立完成,或者是之前满载的套接字发送缓冲区在TCP将队列中的数据传送到对端之后有了剩余空间。
63.1.1 水平触发和边沿触发
水平触发:如果文件描述符上可以非阻塞地执行IO系统调用,此时认为它已经就绪。
通俗说:只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知;当文件描述符关联的内核写缓冲区不满,有空间可以写入,就一直发出可写信号进行通知。
边沿触发:如果文件描述符自上次状态检查以来有了新的I/O 活动(比如新的输入),此时需要触发通知。
通俗说:当文件描述符关联的读内核缓冲区由空转化为非空的时候,则发出可读信号进行通知;当文件描述符关联的内核写缓冲区由满转化为不满的时候,则发出可写信号进行通知。
特别地,当采用边沿触发模式时:
- 在接收到一个I/O 事件通知后,程序在某个时刻应该在相应的文件描述符上应该尽可能多地执行I/O。
- 每个被检查的文件描述符通常都应该置为非阻塞模式。
63.1.2 在备选的IO模型中采用非阻塞IO
63.2 IO多路复用
63.2.1 select()系统调用
系统调用select()
会一直阻塞,直到一个或多个文件描述符集合成为就绪态。
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
// 成功返回就绪的文件描述符个数,超时返回0,失败返回-1并设置errno
文件描述符集合
参数readfds
、writefds
以及exceptfds
都是指向文件描述符集合的指针,所指向的数据类型是fd_set
。
readfds
是用来检测输入是否就绪的文件描述符集合。writefds
是用来检测输出是否就绪的文件描述符集合。exceptfds
是用来检测异常情况是否发生的文件描述符集合。
通常,数据类型fd_set
以位掩码的形式来实现,关于文件描述符集合的操作都是通过四个宏来完成的。
#include <sys/select.h>
void FD_ZERO(fd_set *fdset); // 将fdset所指向的集合初始化为空
void FD_SET(int fd, fd_set *fdset); // 将文件描述符fd添加到由fdset所指向的集合中
void FD_CLR(int fd, fd_set *fdset); // 将文件描述符fd从fdset指向的集合中移除
int FD_ISSET(int fd, fd_set *fdset); // 如果文件描述符是fdset所指向的集合中的成员,FD_ISSET返回true
文件描述符集合有一个最大容量限制,由常量FD_SETSIZE
决定,在Linux上,该常量的值为1024。
参数readfds
, writefds
, exceptfds
都是“值-结果”参数,select
会修改它们,如果在循环中调用select
,每次都要重新初始化它们。
参数nfds
必须设为比3个文件描述符集合中所包含的最大文件描述符号还要大1。该参数能提高select
的效率,因为此时内核就不用去检查大于这个值的文件描述符号是否属于这些文件描述符集合。
timeout参数
struct timeval {
time_t tv_sec; /* Seconds */
suseconds_t tv_usec; /* Microseconds */
};
timeout
为NULL
时,select
会一直阻塞。- 如果结构体
timeval
的两个域都为0时,select
不会阻塞,它只是简单地轮询指定的文件描述符集合,看看其中是否有就绪的文件描述符并立刻返回。 - 否则,
timeout
将为select()
指定一个等待时间的上限值。
select的返回值
- 返回-1表示发生错误,并设置
errno
。比如返回EBADF
表示3个文件描述符集合中有一个文件描述符是非法的(例如当前并没有打开)。 - 返回0表示在任何文件描述符成为就绪态之前
select()
调用已经超时。此时每个返回的文件描述符集合都会被清空。 - 返回一个正整数表示处于就绪态的文件描述符个数。(所有在 3 个集合中被标记为就绪态的文件描述符总数)
63.2.2 poll()系统调用
在select()
中,我们提供三个集合,每个集合中标明我们感兴趣的文件描述符。而在poll()
中我们提供一列文件描述符,并在每个文件描述符上标明我们呢感兴趣的事件。
#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
// 成功返回就绪文件描述符的个数,超时返回0,失败返回-1并设置errno
-
参数
fds
列出了我们需要poll()
来检查的文件描述符。struct pollfd { int fd; // file descriptor short events; // requested events bit mask short revents; // returned evevnts bit mask }
如果我们对某个特定的文件描述符不感兴趣了,只需要将
events
设为0或一个负值,这样可以关闭对单个文件描述符的检查,而不需要重新建立整个fds
列表。 -
参数
nfds
指定了数组fds
中元素的个数。
-
timeout
参数决定了poll()
的阻塞行为:timeout == -1
,poll()
会一直阻塞直到有文件描述符就绪。timeout == 0
,poll()
不会阻塞,只是执行一次检查看看哪个文件描述符处于就绪态。timeout > 0
,poll()
至多阻塞timeout
毫秒,直到有文件描述符处于就绪态。
-
poll()
的返回值:- 返回-1表示出错。
- 返回0表示超时(指定时间内没有文件描述符就绪)。
- 返回一个正整数表示就绪的文件描述符个数,即数组
fds
中events
字段非0的pollfd
结构体数量。
62.2.3 文件描述符何时就绪
对于普通文件,select()
标记为可读和可写,poll()
会在revents
字段中返回POLLIN
和POLLOUT
。
对于套接字如下:
POLLEDHUP
这个标志的实际形式是EPOLLRDHUP
——主要被设计用于epoll API 的边缘触发模式下。当流式套接字连接的远端关闭了写连接时会返回该标志。使用这个标志能让采用了 epoll 边缘触发模式的应用程序使用更简洁的代码来判断远端是否已经关闭。(另一种可选的方法是,在应用程序中设定POLLIN
标志,然后执行一次read()
,如果返回0 则表示远端已经关闭了。)
注意:当套接字对端close()
关闭整个连接后,read()
也返回0。对端shutdown()
只是关闭对端的写端(发送方向)。
63.2.4 比较select()和poll()
-
实现细节:在Linux内核层面,
select()
和poll()
都使用了相同的内核poll例程集合。每个poll例程都返回有关单个文件描述符就绪的信息。 -
API之间的区别:
select()
使用的数据类型fd_set
对于检查的文件描述符个数有上限FD_SETSIZE,Linux默认1024;而poll()
对检查的文件描述符数量上本质没有限制。select()
的参数fd_set
同时也是保存调用结果的地方,如果要在循环中重复调用select()
的话,我们必须每次都要重新初始化fd_set
;而poll()
通过两个独立的字段来处理输入(events
)和输出(revents
)。
-
性能:当如满足如下两条中任意一条时,poll()和select()将具有相似的性能表现。
- 当待检查的文件描述符范围较小(即,最大的文件描述符号较低)。
- 有大量的文件描述符待检查,但是它们分布得很密集(即,大部分文件描述符号都在0 到某个上限之间)。
如果被检查的文件描述符集合很稀疏的话,
poll()
的性能表现将优于select()
。因为在select()
中,不管我们是否要检查范围0
到nfds−1
之间的所有文件描述符,nfds
的值都不变。无论哪种情况,内核都必须在每个集合中检查nfds
个元素,以此来查明到底需要检查哪个文件描述符。与之相反,当使用poll()
时,只需要指定我们感兴趣的文件描述符即可,内核只会去检查这些指定的文件描述符。
63.2.5 select()和poll()存在的问题
- 每次调用
select()
或poll()
,内核都必须检查所有被指定的文件描述符,看它们是否处于就绪态。 - 每次调用
select()
或poll()
时,程序都必须传递一个表示所有需要被检查的文件描述符的数据结构到内核,内核检查过描述符后,修改这个数据结构并返回给程序。(此外,对于select()
来说,我们还必须在每次调用前初始化这个数据结构。)对于poll()
来说,随着待检查的文件描述符数量的增加,传递给内核的数据结构大小也会随之增加。 select()
或poll()
调用完成后,程序必须检查返回的数据结构中的每个元素,以此查明哪个文件描述符处于就绪态了。