说明:本文整合网络资源和man帮助文档,请酌情参考。
背景
select函数是实现IO多路复用的一种方式。
什么是IO多路复用?
举一个简单地网络服务器的例子,如果你的服务器需要和多个客户端保持连接,处理客户端的请求,属于多进程的并发问题,如果创建很多个进程来处理这些IO流,会导致CPU占有率很高。所以人们提出了I/O多路复用模型:一个线程,通过记录I/O流的状态来同时管理多个I/O。
select只是IO复用的一种方式,其他的还有:poll,epoll等。
说明
定义
1 |
|
介绍、
select()函数允许程序监视多个文件描述符,等待所监视的一个或者多个文件描述符变为“准备好”的状态。所谓的”准备好“状态是指:文件描述符不再是阻塞状态,可以用于某类IO操作了,包括可读,可写,发生异常三种。
我们使用select来监视文件描述符时,要向内核传递的信息包括:
1、我们要监视的文件描述符个数
2、每个文件描述符,我们可以监视它的一种或多种状态,包括:可读,可写,发生异常三种。
3、要等待的时间,监视是一个过程,我们希望内核监视多长时间,然后返回给我们监视结果呢?
4、监视结果包括:准备好了的文件描述符个数,对于读,写,异常,分别是哪儿个文件描述符准备好了。
参数说明
nfds:是一个整数值, 表示集合中所有文件描述符的范围,即所有文件描述符的最大值+1。在windows中不需要管这个。
linux select第一个参数的函数: 待测试的描述集的总个数。 但要注意, 待测试的描述集总是从0, 1, 2, …开始的。 所以, 假如你要检测的描述符为8, 9, 10, 那么系统实际也要监测0, 1, 2, 3, 4, 5, 6, 7, 此时真正待测试的描述符的个数为11个, 也就是max(8, 9, 10) + 1
注意:
1、果你要检测描述符8, 9, 10, 但是你把select的第一个参数定为8, 实际上只检测0到7, 所以select不会感知到8, 9, 10描述符的变化。
2、果你要检测描述符8, 9, 10, 且你把select的第一个参数定为11, 实际上会检测0-10, 但是, 如果你不把描述如0 set到描述符中, 那么select也不会感知到0描述符的变化。
所以, select感知到描述符变化的必要条件是, 第一个参数要合理, 比如定义为fdmax+1, 且把需要检测的描述符set到描述集中。
fd_set:
一个文件描述符集合保存在fd_set变量中,可读,可写,异常这三个描述符集合需要使用三个变量来保存,分别是 readfds,writefds,exceptfds。我们可以认为一个fd_set变量是由很多个二进制构成的数组,每一位表示一个文件描述符是否需要监视。
对于fd_set类型的变量,我们只能使用相关的函数来操作。1
2
3
4void FD_CLR(int fd, fd_set *set);//清除某一个被监视的文件描述符。
int FD_ISSET(int fd, fd_set *set);//测试一个文件描述符是否是集合中的一员
void FD_SET(int fd, fd_set *set);//添加一个文件描述符,将set中的某一位设置成1;
void FD_ZERO(fd_set *set);//清空集合中的文件描述符,将每一位都设置为0;
使用案例:1
2
3
4
5
6
7
8
9fd_set readfds;
int fd;
FD_ZERO(&readfds)//新定义的变量要清空一下。相当于初始化。
FD_SET(fd,&readfds);//把文件描述符fd加入到readfds中。
//select 返回
if(FD_ISSET(fd,&readset))//判断是否成功监视
{
//dosomething
}
readfds:
监视文件描述符的一个集合,我们监视其中的文件描述符是不是可读,或者更准确的说,读取是不是不阻塞了。
writefds:
监视文件描述符的一个集合,我们监视其中的文件描述符是不是可写,或者更准确的说,写入是不是不阻塞了。
exceptfds:
用来监视发生错误异常文件
timeout1
2
3
4struct timeval{
long tv_sec;//秒
long tv_usec;//微秒
}
timeout表示select返回之前的时间上限。
如果timeout==NULL,无期限等待下去,这个等待可以被一个信号中断,只有当一个描述符准备好,或者捕获到一个信号时函数才会返回。如果是捕获到信号,select返回-1,并将变量errno设置成EINTR。
如果timeout->tv_sec==0 && timeout->tv_sec==0 ,不等待直接返回,加入的描述符都会被测试,并且返回满足要求的描述符个数,这种方法通过轮询,无阻塞地获得了多个文件描述符状态。
如果timeout->tv_sec!=0 || timeout->tv_sec!=0 ,等待指定的时间。当有描述符复合条件或者超过超时时间的话,函数返回。等待总是会被信号中断。
原理
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
执行fd_set set;FD_ZERO(&set);则set用位表示是0000,0000。
若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
若再加入fd=2,fd=1,则set变为0001,0011
执行select(6,&set,0,0,0)阻塞等待
若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
返回值
成功时:返回三中描述符集合中”准备好了“的文件描述符数量。
超时:返回0
错误:返回-1,并设置 errno
EBADF:集合中包含无效的文件描述符。(文件描述符已经关闭了,或者文件描述符上已经有错误了)。
EINTR:捕获到一个信号。
EINVAL:nfds是负的或者timeout中包含的值无效。
ENOMEM:无法为内部表分配内存。
pselect
1 |
|
select和pselect有三个主要的区别:
1、select超时使用的是struct timeval,用秒和微秒计时,而pselect使用struct timespec ,用秒和纳秒。1
2
3
4struct timespec{
time_t tv_sec;//秒
long tv_nsec;//纳秒
}
2、select会更新超时参数timeout 以指示还剩下多少时间,pselect不会。
3、select没有sigmask参数.
sigmask:这个参数保存了一组内核应该打开的信号(即:从调用线程的信号掩码中删除)
当pselect的sigmask==NULL时pselect和select一样
当sigmask!=NULL时,等效于以下原子操作:
1 | sigset_t origmask; |
接收信号的程序通常只使用信号处理程序来引发全局标志。全局标志将指示事件必须被处理。在程序的主循环中。一个信号将导致select和pselect返回-1 并将erron=EINTR。
我们经常要在主循环中处理信号,主循环的某个位置将会检查全局标志,那么我们会问:如果信号在条件之后,select之前到达怎么办。答案是select会无限期阻塞。
这种情况很少见,但是这就是为什么出现了pselect。因为他是类似原子操作的。
举个栗子:
1 | static volatile sig_atomic_t got_SIGCHLD = 0; |
总结
select()可以同时监视多个描述符,如果他们没有活动,则正确地将进程置于休眠状态。Unix程序员们经常要处理多个文件描述符的I/O,他们的数据流可能是间歇性的。如果只创建read或者write会导致程序阻塞。
在我们使用select的时候,需要注意:
1、我们应该总是设置timeout=0,因为如果没有可用的数据,程序在运行时间里将无视可做。依赖超时的代码通常是不可移植,并且很难调试。
2、nfds的值一要准备且适当。
3、如果在调用完select之后,你不想检查结果,也不想做出适当的响应,那么文件描述符不需要添加到集合中。
4、select返回后,所有的文件描述符都应该被检查,看看他们是否准备好了。
5、read,recv,write,send,这几个函数不一定读/写你所请求的全部数据。如果他们读/写全部数据,是因为低流量负载和快速流。情况并非重视如此,应该处理你的函数仅管理发送或接收单个字节的情况。
6、除非你真的确信你有少量的数据要处理,否则不要一次只读一个字节,当你每次都能缓冲的时候,尽可能多的读取数据是非常低效的。
7、read,recv,write,send和select都会有返回-1的情况,并set errno的值。这些errno必须被恰当的处理。如果你的程序不会接收到任何信号,那么errno永远都不会等于EINTR,如果你的程序并不会设置非阻塞IO,那么errno就不会等于EAGAIN。
8、调用read,recv,write,send,不要使buffer的长度为0;
9、如果read,recv,write,send调用失败,并且返回的errno不是7中说的那两种情况,或者返回0,意思是“end-of-file”,这种情况下我们不应再将文件描述符传递给select。
10、每次调用select之前,timeout都用重新设置。
11、由于select()修改其文件描述符集,如果调用在循环中使用,则必须在每次调用之前重新初始化这些集。
大多数的操作系统都支持select。相比于试图用线程,进程,IPCS,信号,内存共享等方式来解决问题,select函数更有效且轻松。系统调用poll和select相似,在监视稀疏文件集合的时候更加有效。poll现在也在被广泛的使用,但没有select简便。linux专用的epoll在监视大连数据时比select和poll更加有效。
案例
案例1
下面是”man select “帮助文档中案例: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
35
36
37
38
39
int main(void)
{
fd_set rfds;//定义一个能保存文件描述符集合的变量
struct timeval tv;//定义超时时间
int retval;//保存返回值
/* Watch stdin (fd 0) to see when it has input. */
/* 监测标准输入流(fd=0)看什么时候又输入*/
FD_ZERO(&rfds);//初始化集合
FD_SET(0, &rfds);//把文件描述符0加入到监测集合中。
/* Wait up to five seconds. */
/* 设置超时时间为5s */
tv.tv_sec = 5;
tv.tv_usec = 0;
/*调用select函数,将文件描述符集合设置成读取监测 */
retval = select(1, &rfds, NULL, NULL, &tv);
/* Don't rely on the value of tv now! */
/* 这时候的tv值是不可依赖的 */
/*根据返回值类型判断select函数 */
if (retval == -1)
perror("select()");
else if (retval)
printf("Data is available now.\n");
/* FD_ISSET(0, &rfds) will be true. */
/* 因为值增加了一个fd,如果返回值>0,则说明fd=0在集合中。*/
else
printf("No data within five seconds.\n");
exit(EXIT_SUCCESS);
}
案例2
下面是”man select_tut “帮助文档中案例:
这个例子更好的说明了select函数的作用,这是一个TCP转发相关的程序,从一个端口转发到另一个端口
1 |
|
上面的程序可以应用于大多数类型的TCP连接,包括telnet服务器对OOB信号的转发。它处理了同时在两个方向上流动这一棘手问题。你可能会想,使用连个进程不是更有效吗?事实上使用两个进程会更复杂。另一个想法是使用fcntl设置非阻塞的I/O使用,这也有弊端,因为它使用非常低效的超时。
这个程序不能处理同时有多个连接的情况,但很容易扩展。你只需要为每个连接创建一个buffer。当前的程序中,新的连接会导致旧的连接被覆盖丢弃。