1. 定义
同步IO多路复用。
select(2)
和 pselect(2)
的区别:
- 时间精度不同,
select(2)
用struct timeval
,精确到us,pselect(2)
用struct timespec
,精确到ns select(2)
会更新timeout
,提示还剩下多长时间,pselect()
不会更新参数select(2)
不会捕获信号,没有sigmask
参数
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
struct timeval {
time_t tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
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);
#include <sys/select.h>
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
pselect(): _POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600
2. 使用
2.1. 输入
监听三组相互独立的fd组:可读事件组,可写事件组,异常事件组。FD_()
函数族可以控制fd_set
。返回也是在这些地方,因此如果再循环中使用select()
时,需要每次都重新初始化希望监听的组。返回的fd在可读事件组中,表示该fd上可以立刻读出数据;在可写事件组中,表示该fd有空间可以写。
nfds
是三组中fd编号最大的值再+1。
timeout
控制阻塞时间,NULL表示一直阻塞,0表示不阻塞,立刻返回。
sigmask
不为NULL时,pselect(2)
会先将当前监听的信号组保存,替换成sigmask
指向的信号组,然后进行select
,返回后再恢复之前的信号组。也就是说,这样的调用:
ready = pselect(nfds, &readfds, &writefds, &exceptfds, timeout, &sigmask);
会等价于 原子性 的执行:
sigset_t origmask;
pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);
2.2. 输出
大于0:表示三个返回的fd组中fd的总个数,需要用FD_ISSET()
检查某个fd是否有事件返回
0: 超时
-1: 失败,并设置errno
,此时fd组和timeout
未定义,不能使用
errno:
- EBADF: 输入的fd组中有无效的fd(fd已关闭,或者已经发生了错误)。
- EINTR: 产生信号
- EINVAL:
nfds
是负数或者timeout
无效 - ENOMEM: 没有内存创建内部表
3. 为什么要有pselect(2)
UNIX网络编程给了个例子。这个程序的SIGINT
信号处理函数设置全局变量intr_flag
并返回,然后程序主逻辑检查intr_flag
是否设置,如果设置了就进行处理。如果主逻辑阻塞在select()
调用,此时产生了SIGINT
信号,select()
会返回EINTR
错误,返回之后,可以继续检查intr_flag
是否设置了。代码大致长这样:
if (intr_flag) // 1
handle_intr(); // 处理SIGINT信号
if ((nready = select(...)) < 0) { // 2
if (errno == EINTR) {
if (intr_flag)
handle_intr();
}
}
但有个问题,如果主逻辑在测试intr_flag
(1)和调用select
(2) 之间有信号发生的话,并且如果select
永远阻塞,该信号将丢失。使用pselect
就可以安全处理这种情况:
sigset_t newmask, oldmask, zeromask;
sigemptyset(&zeromask);
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
sigprocmask(SIG_BLOCK, &newmask, &oldmask); // block SIGINT
if (intr_flag) // 1
handle_intr();
if ((nready = pselect(..., &zeromask)) < 0) { // 2
if (errno == EINTR) {
if (intr_flag)
handle_intr();
}
...
}
在测试intr_flag
之前,阻塞掉SIGINT
,当调用pselect
时,将阻塞的信号集替换为空集zeromask
,解除对SIGINT
的屏蔽,pselect
返回时,又会将SIGINT
屏蔽掉,这样,SIGINT
信号只会在(1)之前和pselect()
被阻塞时(2)捕获,保证不会错过对信号的处理。
在man 2 select_tut
中有个完整些的例子,可以看看。
4. 多线程
如果正在被select()
监听的fd在另一个线程中被关闭,结果无定义。一些UNIX系统上,select()
解除阻塞,立即返回,并且表明该fd上有事件发生(但接下来对该fd的操作可能失败,因为已经关闭了。除非另一个线程在select()
返回和对fd操作之间重新打开了这个fd)。linux上,另一个线程关闭fd对select()
无影响。总体来说,别在多个线程上同时处理同一个fd。
select()
返回可读事件后,后续的读操作仍有可能阻塞,比如数据已经到了,但上层检查的时候,因为校验和错误而丢掉该数据。因此最好是配合非阻塞IO操作。
5. 例子
这个是 manual 中给的例子,监听stdin是否有输入
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int
main(void)
{
fd_set rfds;
struct timeval tv;
int retval;
/* Watch stdin (fd 0) to see when it has input. */
FD_ZERO(&rfds);
FD_SET(0, &rfds);
/* Wait up to five seconds. */
tv.tv_sec = 5;
tv.tv_usec = 0;
retval = select(1, &rfds, NULL, NULL, &tv);
/* Don't rely on the value of tv now! */
if (retval == -1)
perror("select()");
else if (retval)
printf("Data is available now.
");
/* FD_ISSET(0, &rfds) will be true. */
else
printf("No data within five seconds.
");
exit(EXIT_SUCCESS);
}