0%

select相关

1. 定义

同步IO多路复用。

select(2)pselect(2) 的区别:

  • 时间精度不同,select(2)struct timeval,精确到us,pselect(2)struct timespec
    ,精确到ns
  • select(2) 会更新 timeout ,提示还剩下多长时间,pselect() 不会更新参数
  • select(2) 不会捕获信号,没有 sigmask 参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* 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,返回后再恢复之前的信号组。也就是说,这样的调用:

1
ready = pselect(nfds, &readfds, &writefds, &exceptfds, timeout, &sigmask);

会等价于 原子性 的执行:

1
2
3
4
5
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是否设置了。代码大致长这样:

1
2
3
4
5
6
7
8
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就可以安全处理这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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是否有输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#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.\n");
/* FD_ISSET(0, &rfds) will be true. */
else
printf("No data within five seconds.\n");

exit(EXIT_SUCCESS);
}