Linux select函数详解
一 五种I/O操作模式
linux下的I/O操作:I/O是指数据流的操作,比如说网络编程的I/O操作,串口的读写等等可以称为I/O操作。在linux系统中一共有下面五种I/O操作模式。
- 阻塞I/O(blocking I/O)
- 非阻塞I/O (nonblocking I/O)
- I/O复用 (I/O multiplexing)
- 信号驱动I/O (signal driven I/O (SIGIO))
- 异步I/O (asynchronous I/O (the POSIX aio_functions))
前四种都是同步I/O,只有最后一种是异步I/O
二 阻塞型I/O
阻塞型I/O:应用程序调用一个I/O函数,导致应用程序阻塞,直到数据准备好。数据准备好了,从内核拷贝到用户空间(或者反过来),I/O函数返回成功
linux系统默认的I/O下操作模式为阻塞模式,也是最常见的I/O操作模式。例如创建了一个套接字,想要使用非阻塞模式,那么你需要进行设置,因为默认的是阻塞模式。
以TCP通信中 recv() 函数为例:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
创建套接字,bind和listen后,以及使用accept连接上后就会使用这个recv函数去等待数据到来,等对方通过socket将数据发送到内核,内核就会通知进程有数据到来,此时这个函数就会返回,返回接收数据的字节数,否则一直会在阻塞状态等待数据的到来。
三 非阻塞型I/O
非阻塞型I/O:就是告诉内核,当我们调用IO函数时,不管数据有没有准备好,都立即返回(即使出错)
//把套接字设置成非阻塞模式
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
非阻塞模式的使用并不普遍,因为非阻塞模式会浪费大量的CPU资源
四 I/O多路复用(重点)
背景:前面讲过,阻塞模式是最常用的一种I/O模式,从socket的角度出发,一个client去连接连接一个server端,那么如果此时我们使用阻塞模式的话,如果只使用一个线程的话,此时第一个client连接过来了,调用了recv()函数,阻塞在等待消息这里,此时如果第二个客户端连接的话,因为程序一直在recv()这里等着,无法处理这个连接请求。那么如果我们使用多线程机制,对每个客户端都使用一个线程,那么如果有大量的客户连接的话,服务端就要创建大量的线程,大量的线程创建会消耗linux的资源。
在Linux中,我们可以使用select函数实现I/O端口的复用,传递给select函数的参数会告诉内核:
- 我们所关心的文件描述符
- 对每个描述符,我们所关心的状态。(我们是要想从一个文件描述符中读或者写,还是关注一个描述符中是否出现异常)
- 我们要等待多长时间。(我们可以等待无限长的时间,等待固定的一段时间,或者根本就不等待)
从 select函数返回后,内核告诉我们一下信息:
- 对我们的要求已经做好准备的描述符的个数
- 对于三种条件哪些描述符已经做好准备.(读,写,异常)
有了这些返回信息,我们再调用合适的I/O函数(通常是 read 或 write),并且这些函数不会再阻塞.
1.select函数
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int maxfd, fd_set *readfds, fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
/*
功能:实现IO的多路复用
参数:
int maxfd:指集合中所有文件描述符的范围,即文件描述符的最大值加1(默认最大值为1024)
内核只需要在我们打开的最大值的描述符以内进行轮询,已减少轮询时间和系统开销
fd_set *readfds:需要监视的可读的描述符的集合,在参数1的范围内,把需要监视的描述需要提前置位(下文详解)
fd_set *writefds:需要监视的可写的描述符的集合,要求同上
fd_set *exceptfds:需要监视的可能异常的描述符的集合,要求同上
struct timeval *timeout:下面是这个参数的结构体类型
struct timeval{
long tv_sec; //秒
long tv_usec; //微秒
}
1.timeout == NULL
等待无限长的时间
等待可以被一个信号中断,如果捕获到一个信号, select函数将返回 -1,并将变量 erro设为 EINTR。
当有一个描述符做好准备或者是捕获到一个信号时函数会返回。
2.timeout->tv_sec == 0 && timeout->tv_usec == 0
不等待,直接返回
加入描述符集的描述符都会被测试,并且立即返回
这种方法通过轮询,无阻塞地获得了多个文件描述符状态
3.timeout->tv_sec !=0 ||timeout->tv_usec!= 0
等待指定的时间(期间也会被信号中断)
当有描述符符合条件或者超过设定的时间,函数返回
返回值:
retval > 0 :当监视的相应的文件描述符集中满足条件时,比如说读文件描述符集中有数据到来时,
内核(I/O)根据状态修改文件描述符集,并返回一个大于0的数
retval = 0 :当没有满足条件的文件描述符,且设置的timeval监控时间超时,select函数会返回 0
retval < 0 :出错返回-1(信号中断等)
*/
2.描述符的集合
对于 fd_set类型的变量我们所能做的就是声明一个变量,为变量赋一个同种类型的值,或者使用以下几个宏来控制它:
#include <sys/select.h>
int FD_ZERO(int fd, fd_set *fdset); //将一个 fd_set类型变量的所有位都设为 0
int FD_SET(int fd, fd_set *fd_set); //将变量的某个位置位(置1)
int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位
int FD_CLR(int fd, fd_set *fdset); //清除变量某个位
当声明了一个文件描述符集后,必须用FD_ZERO将所有位置零。之后将我们所感兴趣的描述符所对应的位置位,操作如下:
fd_set rset;
int fd;
FD_ZERO(&rset);
FD_SET(fd, &rset);
FD_SET(stdin, &rset);
select返回后,用FD_ISSET测试给定位是否置位
if(FD_ISSET(fd, &rset)
{
;//此时调用对应的IO函数时不会阻塞
}
2.理解select模型
理解select模型的关键在于理解fd_set,实际上用sizeof(fd_set)可以获取描述符集合的大小,那么我们可以监控的描述符的数量为:8*sizeof(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被清空。
参考资料:
https://blog.csdn.net/lingfengtengfei/article/details/12392449
https://blog.csdn.net/qq_18059143/article/details/120374229