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 #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> struct timeval { time_t tv_sec; long tv_usec; }; 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; long tv_nsec; }; 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); }