select系统调用
用途:在一段指定的时间内,系统调用 select()会一直阻塞,直到一个或多个文件描述符集合成为就绪态(监听用户感兴趣的文件描述符上的可读、可写、异常等事件)
理论
该函数允许进程指示等待多个事件中的任何一个发生,并且只有在一个或者多个事件发生或者经历一段指定的时间后才能唤醒它也就是说,我们可以调用select告知内核对哪些描述符感兴趣以及等待多长时间
参数
参数 nfds、readfds、writefds 和 exceptfds 指定了 select()要检查的文件描述符集合。参数timeout 可用来设定 select()阻塞的时间上限。我们接下来详细描述这些参数的意义。
文件描述符集合
nfds
:
- 参数nfds指定被监听的文件描述符的总数。它必须设为比 3 个文件描述符集合中所包含的最大文件描述符号还要大 1(因为文件描述符是从0开始计数的)。比如现在select待测试的描述符集合是{0, 1, 4},那么maxfd就是5
- 这个参数可以让select()变得更有效率,因为此时内核就不必去检测大于这个值的文件描述符号是否属于这些文件描述符集合
参数 readfds、writefds、exceptfds
都是指向文件描述符集合的指针,所指向的数据类型是 fd_set。这些参数按照如下方式使用。
- readfds 是用来检测输入是否就绪的文件描述符集合。
- writefds 是用来检测输出是否就绪的文件描述符集合。
- exceptfds 是用来检测异常情况是否发生的文件描述符集合。
- 术语“异常情况”常常被误解为在文件描述符上出现了一些错误,这并不正确
- __exceptfds指定让内核异常条件的描述符。目前仅支持的异常条件有两个(其他的 UNIX 实现也类似)。
- 连接到处于信包模式下的伪终端主设备上的从设备发生了改变
- 流式套接字上接收到了带外数据
那么如何设置这些描述符集合呢?通常,数据类型 fd_set 以位掩码的形式来实现。但是,我们并不需要知道这些细节,因为所有关于文件描述符集合的操作都是通过四个宏来完成的:FD_ZERO(),FD_SET(),FD_CLR()以及 FD_ISSET()。
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
- FD_ZERO()将 fdset 所指向的集合初始化为空。
- FD_SET()将文件描述符 fd 添加到由 fdset 所指向的集合中。
- FD_CLR()将文件描述符 fd 从 fdset 所指向的集合中移除。
- 如果文件描述符 fd 是 fdset 所指向的集合中的成员,FD_ISSET()返回 true。
这个好像有点晦涩,没关系,没有关系,我们可以这样想象,下面一个向量代表了一个描述符集合,其中,这个向量的每个元素都是二机制数中的 0 或者 1。
a[maxfd-1], ..., a[1], a[0]
我们按照这样的思路来理解这些宏:
- FD_ZERO 用来将这个向量的所有元素都设置成 0;
- FD_SET 用来把对应套接字 fd 的元素,a[fd] 设置成 1;
- FD_CLR 用来把对应套接字 fd 的元素,a[fd] 设置成 0;
- FD_ISSET 对这个向量进行检测,判断出对应套接字的元素 a[fd] 是 0 还是 1。
其中 0 代表不需要处理,1 代表需要处理。
怎么样,是不是感觉豁然开朗了?
实际上,很多系统是用一个整型数组来表示一个描述字集合的,一个 32 位的整型数可以表示 32 个描述字,例如第一个整型数表示 0-31 描述字,第二个整型数可以表示 32-63 描述字,以此类推。
这个时候再来理解为什么描述字集合{0,1,4},对应的 maxfd 是 5,而不是 4,就比较方便了。
因为这个向量对应的是下面这样的:
a[4],a[3],a[2],a[1],a[0]
待测试的描述符个数显然是 5, 而不是 4。
三个描述符集合中的每一个都可以设置成空,这样就表示不需要内核进行相关的检测。
文件描述符集合有一个最大容量限制,由常量 FD_SETSIZE 来决定。在 Linux 上,该常量的值为 1024。
尽管 FD_*宏操作的是用户空间数据结构,select()的内核实现却能处理更大的文件
描述符集合。在 glibc 中没有什么简单的方法可以修改 FD_SETSIZE 的定义。如果我们想修改这个限制,必须修改 glibc 头文件中的定义。如果我们需要检查大量的文件描述符,那么使用 epoll 可能比 select()更加可取
typedef long int __fd_mask;
#define __FD_SETSIZE 1024
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
typedef struct{
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
} fd_set;
从上面可以知道,fd_set结构体仅包含一个整型数组,该数组的每个元素的每一位标记一个文件描述符,fd_set能容纳的文件描述符数量由__FD_SETSIZE 指定,这就限制了select能同时处理的文件描述符的总量
- 参数 readfds、writefds 和 exceptfds 所指向的结构体都是保存结果值的地方。
- 在调用 select()之前,这些参数指向的结构体必须初始化(通过 FD_ZERO()和 FD_SET()),以包含我们感兴趣的文件描述符集合。
- 之后 select()调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。
- 也就是说当 select()返回时,它们包含的就是已处于就绪态的文件描述符集合了(由于这些结构体会在调用中被修改,如果要在循环中重复调用select(),我们必须保证每次都要重新初始化它们)。之后这些结构体也可以通过FD_ISSET来检测
如果我们对某一类型的事件不感兴趣,那么响应的fd_set参数可以指定为NULL。
timeout参数
timeout参数控制着select的阻塞行为。这个参数有下面三种可能
- 永远等待下去(当为
NULL
时):仅在有一个描述符准备好IO的时候才返回。 - 等待一段时间:有一个描述符准备好IO时返回,但是不超过由该参数所指向的timeval结果中指定的s和us
- 不等待(
tv_sec和tv_usec都为0
):检查描述符之后立即返回,这称为轮询。
当 timeout 设为 NULL,或其指向的结构体字段非零时,select()将阻塞直到有下列事件发生:
- readfds、writefds 或 exceptfds 中指定的文件描述符中至少有一个成为就绪态;
- 该调用被信号处理例程中断;
- timeout 中指定的时间上限已超时。
- 在 Linux 上,如果 select()因为有一个或多个文件描述符成为就绪态而返回,且如果参数timeout 非空,那么 select()会更新 timeout 所指向的结构体以此来表示剩余的超时时间。但是,这种行为是与具体实现相关的。SUSv3 中还允许系统不去修改 timeout 所指向的结构体,且大多数 UNIX 实现都不会修改这个结构体。在循环中使用了 select()的可移植的应用程序应该总是确保 timeout 所指向的结构体在每次调用 select()之前都要得到初始化,而且在调用完成后应该忽略该结构体中返回的信息。
- SUSv3 中规定由 timeout 所指向的结构体只有在 select()调用成功返回后才有可能被修改。但是,在 Linux 上如果 select()被一个信号处理例程中断的话(因此 select()会产生 EINTR 错误码),那么该结构体也会被修改以表示剩余的超时时间(其作用相当于 select()成功返回)
返回值
返回值:如果有就绪描述符则为其数目,如果超时则为0,出错则为-1
- 返回−1 表示有错误发生。可能的错误码包括 EBADF 和 EINTR。
- EBADF 表示 readfds、writefds 或者 exceptfds 中有一个文件描述符是非法的(例如当前并没有打开)。
- EINTR表示该调用被信号处理例程中断了,如果被信号处理例程中断,select()是不会自动恢复的
- 返回 0 表示在任何文件描述符成为就绪态之前 select()调用已经超时。在这种情况下,每个返回的文件描述符集合将被清空。
- 返回一个正整数表示有 1 个或多个文件描述符已达到就绪态。
- 返回值表示于就绪态的文件描述符个数。在这种情况下,每个返回的文件描述符集合都需要检查(通过 FD_ISSET()),以此找出发生的 I/O 事件是什么。
- 如果同一个文件描述符在 readfds、writefds 和 exceptfds 中同时被指定,且它对于多个 I/O 事件都处于就绪态的话,那么就会被统计多次。换句话说,select()返回所有在 3 个集合中被标记为就绪态的文件描述符总数。
示例:调用select,告知内核只有仅在下列情况发生时才返回
- 集合{1,4,5}中的任何描述符准备好读
- 集合{2,7}中的任何描述符准备好写
- 集合{1,4}中的任何描述符有异常条件待处理
- 经过了10.2s
优缺点
使用
例子
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
int main()
{
fd_set rd;
struct timeval tv;
int ret;
FD_ZERO(&rd); /* 用select函数之前先把集合清零 */
FD_SET(0, &rd); /* 把要检测的句柄socket加入到集合里: 0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr,0就是stdin,1就是stdout,2就是stderr */
tv.tv_sec = 3;
tv.tv_usec = 500; /* 设置select等待的最大时间为3秒加500微秒 */
ret = select(1, &rd, NULL, NULL, &tv);
if(ret == 0) {
printf("select time out!\n");
}else if(ret == -1) {
perror("select函数出错\n");
}else {
printf("ret=%d\n", ret); /* ret这个返回值记录了发生状态变化的句柄的数目,由于我们只监视了stdin这一个句柄,所以这里一定ret=1,如果同时有多个句柄发生变化返回的就是句柄的总和了 */
printf("data is available!\n");
}
}
例子
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: select01 <IPaddress>");
}
int socket_fd = tcp_client(argv[1], SERV_PORT);
char recv_line[MAXLINE], send_line[MAXLINE];
int n;
fd_set readmask;
fd_set allreads;
FD_ZERO(&allreads);
FD_SET(0, &allreads);
FD_SET(socket_fd, &allreads);
for (;;) {
readmask = allreads;
int rc = select(socket_fd + 1, &readmask, NULL, NULL, NULL);
if (rc <= 0) {
error(1, errno, "select failed");
}
if (FD_ISSET(socket_fd, &readmask)) {
n = read(socket_fd, recv_line, MAXLINE);
if (n < 0) {
error(1, errno, "read error");
} else if (n == 0) {
error(1, 0, "server terminated \n");
}
recv_line[n] = 0;
fputs(recv_line, stdout);
fputs("\n", stdout);
}
if (FD_ISSET(STDIN_FILENO, &readmask)) {
if (fgets(send_line, MAXLINE, stdin) != NULL) {
int i = strlen(send_line);
if (send_line[i - 1] == '\n') {
send_line[i - 1] = 0;
}
printf("now sending %s\n", send_line);
size_t rt = write(socket_fd, send_line, strlen(send_line));
if (rt < 0) {
error(1, errno, "write failed ");
}
printf("send bytes: %zu \n", rt);
}
}
}
}
程序的 12 行通过 FD_ZERO 初始化了一个描述符集合,这个描述符读集合是空的:
接下来程序的第 13 和 14 行,分别使用 FD_SET 将描述符 0,即标准输入,以及连接套接字描述符 3 设置为待检测:
接下来的 16-51 行是循环检测,这里我们没有阻塞在 fgets 或 read 调用,而是通过 select 来检测套接字描述字有数据可读,或者标准输入有数据可读。比如,当用户通过标准输入使得标准输入描述符可读时,返回的 readmask 的值为:
这个时候 select 调用返回,可以使用 FD_ISSET 来判断哪个描述符准备好可读了。如上图所示,这个时候是标准输入可读,37-51 行程序读入后发送给对端。
如果是连接描述字准备好可读了,第 24 行判断为真,使用 read 将套接字数据读出。
我们需要注意的是,这个程序的 17-18 行非常重要,初学者很容易在这里掉坑里去。
第 17 行是每次测试完之后,重新设置待测试的描述符集合。你可以看到上面的例子,在 select 测试之前的数据是{0,3},select 测试之后就变成了{0}。
这是因为 select 调用每次完成测试之后,内核都会修改描述符集合,通过修改完的描述符集合来和应用程序交互,应用程序使用 FD_ISSET 来对每个描述符进行判断,从而知道什么样的事件发生。
第 18 行则是使用 socket_fd+1 来表示待测试的描述符基数。切记需要 +1。
例子
通过命令行参数,我们可以指定超时时间以及我们希望检查的文件描述符。
- 第一个命令行参数指定了 select()中的 timeout 参数,以秒为单位。如果这里指定了连字符(-),那么 select()的 timeout 参数就设为 NULL,表示会一直阻塞。
- 剩下的命令行参数用来指定需要检查的文件描述符个数,跟着的字符表示需要被检查的事件类型。我们这里可以指定的是 r(读就绪)和 w(写就绪)
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
static void usageError(const char * progName){
fprintf(stderr, "Usage: %s {timeout|-} fd-num[rw]...\n", progName);
fprintf(stderr, " - means infinite timeout; \n");
fprintf(stderr, " r = monitor for read\n");
fprintf(stderr, " w = monitor for write\n\n");
fprintf(stderr, " e.g.: %s - 0rw 1w\n", progName);
exit(EXIT_FAILURE);
}
int main(int argc, char *argv[])
{
fd_set readfds, writefds;
int ready, nfds, fd, numRead, j;
struct timeval timeout;
struct timeval *pto;
char buf[10]; /* Large enough to hold "rw\0" */
if (argc < 2 || strcmp(argv[1], "--help") == 0)
usageError(argv[0]);
/* Timeout for select() is specified in argv[1] */
if (strcmp(argv[1], "-") == 0) {
pto = NULL; /* Infinite timeout */
} else {
pto = &timeout;
timeout.tv_sec = atoi(argv[1]);
timeout.tv_usec = 0;
}
/* Process remaining arguments to build file descriptor sets */
nfds = 0;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
for(j = 2; j < argc; j++){
numRead = sscanf(argv[j], "%d%2[rw]", &fd, buf);
numRead = sscanf(argv[j], "%d%2[rw]", &fd, buf);
if (numRead != 2)
usageError(argv[0]);
if (fd >= FD_SETSIZE){
printf("file descriptor exceeds limit (%d)\n", FD_SETSIZE);
exit(EXIT_FAILURE);
}
if (fd >= nfds){
nfds = fd + 1;
}
if (strchr(buf, 'r') != NULL){
FD_SET(fd, &readfds);
}
if (strchr(buf, 'w') != NULL){
FD_SET(fd, &writefds);
}
}
/* We've built all of the arguments; now call select() */
ready = select(nfds, &readfds, &writefds, NULL, pto);
/* Ignore exceptional events */
if (ready == -1){
perror("select");
exit(EXIT_FAILURE);
}
/* Display results of select() */
printf("ready = %d\n", ready);
for (fd = 0; fd < nfds; fd++)
printf("%d: %s%s\n", fd, FD_ISSET(fd, &readfds) ? "r" : "",
FD_ISSET(fd, &writefds) ? "w" : "");
if (pto != NULL)
printf("timeout after select(): %ld.%03ld\n",
(long) timeout.tv_sec, (long) timeout.tv_usec / 1000);
exit(EXIT_SUCCESS);
}
使用1:
- 请求检查文件描述符 0 上的输入,超时时间定为 10 秒
- 上面的输出告诉我们 select()确定了有一个文件描述符已处于就绪态。 文件描述符 0 已经准备好读取数据了
*我们也可以看到 timeout 已经被修改了。- 最后一行输出只有 shell 提示符$,这是因为 t_select 程序并没有读取让文件描述符 0 处于读就绪态的换行符,因此这个字符由shell 读取,结果就是打印出了另一个 shell 提示符
使用2:
- 再次检查文件描述符 0 的输入状态,但这一次将超时时间设为 0 秒。
- select()调用立刻返回,且发现没有文件描述符处于就绪态。
使用3:
- 检查文件描述符 0 上是否有输入,以及文件描述符 1 上是否有输出。
在这种情况下,我们将参数 timeout 设为 NULL(第一个命令行参数为连字符-),表示一直阻塞下去。
- select()调用立刻返回,并告诉我们文件描述符 1 上有输出。
封装
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
int Select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout)
{
int n;
if ( (n = select(nfds, readfds, writefds, exceptfds, timeout)) < 0){
printf("select error: %s", strerror(errno));
exit(1);
}
return(n); /* can return 0 on timeout */
}
总结:套接字描述符就绪条件
当我们说select测试返回,某个套接字准备好可读,表示什么样的事件发生呢?
- 第一种情况是套接字接收缓冲区有数据可读,如果我们使用read去执行读操作,肯定不会被阻塞,而是会直接读到这部分数据。
- 第二种情况是对方发送了FIN,使用read函数执行读操作,不会被阻塞,直接返回0
- 第三种情况是针对一个监听套接字而言的,有已经完成的连接建立,此时使用accept函数去执行不会阻塞,直接返回已经完成的连接
- 第四种情况是套接字有错误待处理,使用read函数值执行读操作,不阻塞,而且返回-1
总之一句话就是,内核通知我们套接字有数据可读了,使用read函数不会阻塞
select检测套接字可写,完全是基于套接字本身的特性来说的,具体来说有如下几种情况:
- 第一种是套接字发送缓冲区足够大,如果我们使用非阻塞套接字1进行write操作,将不会被阻塞,直接返回
- 第二种是连接的写半边已经关闭,如果继续进行写操作将会产生SIGPIPE信号
- 第三种是套接字上有错误待垂,使用write函数去执行读操作,不阻塞,而且返回-1
总结成一句话就是,内核通知我们套接字可以往里写了,使用 write 函数就不会阻塞。
poll系统调用
select方法是多个UNIX平台支持的非常常见的IO多路复用技术,它通过描述符集合来表示检测的IO对象,通过三个不同的描述符来描述IO事件:可读、可写和异常。但是select有一个缺点,那就是所支持的文件描述符个数是有限的。在Linux系统中,select的默认值最大为1024
那有没有别的IO多路复用技术可以突破文件描述符个数的限制呢?当然有,即poll函数
文件描述符何时就绪
正确使用select和poll需要理解什么情况下文件描述符会表示为就绪态。
SUSv3规定:如果对IO函数的调用不会阻塞,而不论该文件是否能够传输数据,此时文件描述符(未指定 O_NONBLOCK 标志)被定为是就绪的。select和poll只会告诉我们IO操作是否会阻塞,而不是告诉我们到底能否成传输数据。
按照这个思路,让我们考虑一下这些系统调用在不同类型的文件描述符上所做的操作。我们将这些信息在表格中以两列来显示
- select()这一列表示文件描述符是否被标记为可读(r),可写(w)还是有异常情况(x)
- poll()这一列表示在 revents 字段中返回的位掩码。在这些表格中,我们忽略
POLLRDNORM、POLLWRNORM、POLLRDBAND 以及 POLLWRBAND。尽管在很
多情况下这些标志会在 revents 中返回(如果在 events 字段中指定过这些标志),但它们相对于 POLLIN、POLLOUT、POLLHUP 以及 POLLERR 来说,并没有提供更多有用的信息
普通文件
代表普通文件的文件描述符总是被select标记为可读可写。对于poll来说,则会在revents字段中返回POLLIN和POLLOUT标志,原因如下:
- read()总是会立即返回数据、文件结尾符或者错误
- write()总是会立即传输错误或者因为出现错误而失败
终端和伪终端
在终端和伪终端上select()和 poll()的行为表现总结如下:
条件或者事件 | select() | poll() |
---|---|---|
有输入 | r | POLLIN |
可输出 | w | POLLOUT |
伪终端对端调用 close()后 | rw | 见下文 |
处于信包模式下的伪终端主设备检测到从设备端状态改变 | x | POLLPRI |
当伪终端对的其中一端处于关闭状态时,另一端由 poll()返回的 revents 将取决于具体实现。在 Linux 上至少会设置 POLLHUP 标志。但是,在其他实现上将返回各种不同的标志来表示这个事件—比如,POLLHUP、POLLERR 或者 POLLIN。此外,在一些实现中,设定什么样的标志取决于被检查的是伪终端主设备还是从设备。
管道和 FIFO
下表总结了了管道或 FIFO 的读端细节。
- “管道中有数据?”这一列表示管道中是否至少有1 字节数据可读。
- 在这个表格中,我们假设已经在 events 字段中指定了 POLLIN 标志
- 在其他一些 UNIX 实现中,如果管道的写端是关闭状态,那么 poll()不会返回 POLLHUP,而会返回 POLLIN 标志(因为 read()遇到文件结尾符会立刻返回)。可移植性高的程序应该检查这两个标志从而得知 read()是否会阻塞。
下表总结了管道写端的细节
- 在这个表格中,我们假设已经在 events 字段中指定了POLLOUT 标志。
- “有PIPE_BUF个字节的空间吗?”这一列表示管道中是否有足够剩余空间能够以原子的方式写入PIPE_BUF个字节而不阻塞。这是Linux判定管道是否可写就绪的标准方法。其他一些Unix实现中也采用相同的标准;还有一些实现中认为只有可以写入1个字节,那么管道就是写就绪的
- 在其他一些 UNIX 实现中,如果管道的读端关闭,那么 poll()并不会返回 POLLERR 标志,相反,要么会返回 POLLOUT,要么返回 POLLHUP。可移植的程序需要检查这些标志,以此来判断 write()是否会阻塞。
套接字
下表总结了select和poll在套接字上的行为表项。对于poll这一列,我们假设events已经指定了(POLLIN | POLLOUT | POLLPRI)标志位。对于 select()这一列,我们假设需要检查文件描述符的输入、输出以及异常情况是否发生。(即,文件描述符在所有传递给 select()的 3 个集合中都有指定)。该表只涵盖了常见的情况,并不包含所有可能出现的情况。
Linux 专有的 POLLRDHUP 标志(从 Linux 2.6.17 以来就一直存在)需要做进一步的解释。其实,这个标志的实际形式是 EPOLLRDHUP—主要被设计用于 epoll API 的边缘触发模式下。当流式套接字连接的远端关闭了写连接时会返回该标志。使用这个标志能让采用了epoll边缘触发模式的应用程序使用更简洁的代码来判断远端是否已经关闭。(另一种可选的方法是,在应用程序中设定 POLLIN 标志,然后执行一次 read(),如果返回 0 则表示远端已经关闭了)。
(1)满足下列四个条件中的任何一个时,一个套接字准备好读
- socket内核接收缓冲区中的数据字节数 >= 接收缓冲区低水位标记
SO_RECLOWAT
的当前大小(对于TCP/UDP套接字默认1):读取这样的套接字时不会阻塞并返回大于0的值。 - socket通信的对方关闭连接(也就是接收了FIN的TCP连接):读取时不会阻塞而且返回0(也就是EOF)
- 监听socket上的已经完成连接数不为0:accept不会阻塞
- socket错误待处理:读取时不阻塞而且返回-1(也就是返回一个错误),同时将errno设置成确切的错误。这些待处理错误可以通过
SO_ERROR
套接字选项调用getsockopt
获取并清除
(2)满足下列四个条件中的任何一个时,一个套接字准备好写
- 非阻塞socket发送缓冲区中可用空间字节数>=套接字发送缓冲区低水位标记
SO_SNDLOWAT
的当前大小(对于TCP/UDP套接字默认2048):写时不阻塞而且返回写入的字节数 - 非阻塞
connect
套接字已建立连接:写时不阻塞而且返回写入的字节数 - 非阻塞套接字不需要连接(比如UDP套接字):写时不阻塞而且返回写入的字节数
- socket的写操作被关闭:写这样的套接字回
SIGPIPE
- 套接字错误待处理:读取时不阻塞而且返回-1(也就是返回一个错误),同时将errno设置成确切的错误。这些待处理错误可以通过
SO_ERROR
套接字选项调用getsockopt
获取并清除
(3)网络编程中,socket能处理的异常情况只有一种:socket上接收到带外数据
注意:当某个套接字上发生错误时,它将有select
标记为可读又可写。
比较select和poll
实现细节
在Linux内核层面,select()和poll()都使用了相同的内核poll例程集合。这些poll例程有别于系统调用poll()本身。每个例程都返回有关单个文件描述符就绪的信息。这个就绪信息以位掩码的形式返回。其值与poll()系统调用中返回的revents字段中的比特值相关。poll()系统调用的实现包括为每个文件描述符调用内核poll例程,并将结果信息填到对应的revents字段中去。
为了实现select,我们使用一组宏将内核poll例程返回的信息转换为由select()返回的与之对应的事件类型。
这些宏定义展现了 select()和 poll()所返回的信息之间的语义关系。唯一一点我们需要额外增加的是,如果被检测的文件描述符当中有一个关闭了,poll()会在revents字段中返回POLLNVAL,而select会返回-1并将错误码设为EBADF
API之间的区别
- select()所使用的数据结构fd_set对于被检测的文件描述符数量有一个上限限制(FD_SETSIZE)。在 Linux 下,这个上限值默认为 1024,修改这个上限需要重新编译应用程序。与之相反,poll()对于被检查的文件描述符数量本质上是没有限制的
- 由于select()的参数fd_set同时也是保存调用结构的地方,如果要在循环中重复调用select的话,我们必须每次都要重新初始化fd_set。而poll()通过独立的两个字段(针对输入)和 revents(针对输出)来处理,从而避免每次都要重新初始化参数
- select()提供的超时精度(微秒)比 poll()提供的超时精度(毫秒)高。(这两个系统调用的超时精度都受软件时钟粒度的限制。
- 如果其中一个被检查的文件描述符关闭了,通过在对应的 revents 字段中设定POLLNVAL 标记,poll()会准确告诉我们是哪一个文件描述符关闭了。与之相反,select()只会返回−1,并设错误码为 EBADF。通过在描述符上执行 I/O 系统调用并检查错误码,让我们自己来判断哪个文件描述符关闭了。通常这些区别都不重要,因为应用程序一般都会自己跟踪已经关闭的文件描述符。
可移植性
历史上,select()比 poll()使用得更加广泛。如今这两个接口都在 SUSv3 中标准化了,且都广泛存在于现代的 UNIX 实现中。
性能
当如满足如下两条中任意一条时,poll()和select()将具有相似的性能表现
- 待检查的文件描述符范围较小(即,最大的文件描述符号较低)
- 有大量的文件描述符待检查,但是它们分布得很密集。(即,大部分或所有的文件描述符号都在 0 到某个上限之间)
然而,如果被检查的文件描述符集合很稀疏的话,select()和 poll()的性能差异将变得非常明显。比如,最大文件描述符符号N是个很大的整数,但在0到N之间只有1个或者几个文件描述符要被检测。此时,poll()的性能优于select。我们可以通过传递给这两个系统调用的参数来理解其中的原因。在select中,我们传递一个或者多个文件描述符集合,以及比待检查的集合中最大的文件描述符符号还要大1的nfds。不管我们是否要检测0到nfds-1之间的所有文件描述符,nfds的值都不变。无论哪种情况,内核都必须在每个集合中检测nfds个元素,以此查明到底需要检测哪个文件描述符。与之相反,当使用poll时,只需要指定我们感兴趣的文件描述符即可,内核只会去检查这些指定的文件描述符。
Linux 2.4 版中 poll()和 select()在稀疏的描述符集合中性能表现差异很大。在 2.6 版内核中通过一些优化手段,这个性能差异已经被极大地缩小了。
select()和 poll()存在的问题
系统调用select和poll是用来同时检查多个文件描述符就绪状态的方法,它们是可移植的、长期存在而且被广泛使用的。但是当检查大量的文件描述符时,这两个API都会遇到一些问题
- 每次调用select()或者poll(),内核都必须检查所有被指定的文件描述符,看它们是否出于就绪态。当检查大量出于密集范围内的文件描述符时,该操作耗费的时间将大大超过接下来的操作
- 每次调用select()或者poll()后,程序都必须传递一个表示所有需要被检测的文件描述符的数据结构到内核,内核检测过描述符之后,修改这个数据结构并返回给程序(对于 select()来说,我们还必须在每次调用前初始化这个数据结构)。对于poll()来说,随着待检查的文件描述符数量的增加,传递给内核的数据结构大小也会随之增加。当检查大量文件描述符时,从用户空间到内核空间来回拷贝这个数据结构将占用大量的CPU时间。对于 select()来说,这个数据结构的大小固定为 FD_ SETSIZE,与待检查的文件描述符数量无关
- select和poll调用完成后,程序必须检查返回的数据结构中的每个元素,以此查明哪个文件描述符出于就绪态了
上述要点产生的结果是随着待检查的文件描述符数量的增加,select和poll所占用的CPU时间也会随之增加,对于需要检测大量文件描述符的程序来说,这就产生了问题
select和poll糟糕的性能延展性源自这些API的局限性:通常,程序重复调用这些系统调用所检测的文件描述符集合是相同的,可是内核并不会在每次调用成功后就记录下它们。
信号驱动IO和epoll都可以使得内核记录下进程感兴趣的文件描述符,通过这种机制消除了select和poll的性能问题。这种解决方案可根据发生的 I/O事件来延展,而与被检查的文件描述符个数无关。结果就是,当需要检查大量的文件描述符时,信号驱动 I/O 和 epoll 能提供更好的性能表现
总结
I/O 多路复用机制中的 select()和 poll()能够同时监视多个文件描述符,以查看哪个文件描述符上可执行 I/O 操作。在这两个系统调用中,我们传递一个待监视的文件描述符列表给内核,之后内核返回一个修改过的列表以表明哪些文件描述符处于就绪态了。在每一次调用中都要传递完整的文件描述符列表,并且在调用返回后还要检查它们,这个事实表明当需要监视大量的文件描述符时,select()和 poll()的性能表现将变得很差