图解IO多路复用之Select实现原理

Linux上提供了IO多路复用机制的实现有多种,常见的有select、poll、epoll,下面分析一下select的多路复用的原理。

服务器端有1个监听文件描述符和若干个通信文件描述符,每当服务器端建立一个新的连接后就会生成一个通信的文件描述符,如下图所示:

select可以同时检测读缓冲区(Read buffer)、写缓冲区(Write buffer),也可以只检测某一个缓冲区数据。如果检测到读缓冲区中有数据、写缓冲区有空闲的空间,这些都可以认为文件描述符是就绪状态。

1、select()函数

select函数的主要作用是将用户态的文件描述符复制到内核中,然后由内核帮助我们检测并返回就绪的文件描述符, select的原型函数如下所示:

int select(int nfds, fd_set *readfds, fd_set *writefds, 
           fd_set *exceptfds, struct timeval * timeout);

(1)nfds

内核需要检测的集合(读缓冲区、写缓冲区、读写异常的缓冲区)中最大的文件描述符 + 1,该参数的意义在于,因为内核需要线性遍历需要检测的集合中的文件描述符,这个值是循环结束的条件。当然没有设置的时候,就使用最大的值1024,如下所示:

(2) readfds

读缓冲区的文件描述符的集合,内核只检测这个集合中文件描述符对应的读缓冲区,如下图所示:

该参数是传入传出参数,即select函数先将数据复制到内核中,然后再由内核写出到对应的内存空间上。

(3)writefds

写缓冲区文件描述符的集合,内核只检测本集合中文件描述符对应的写缓冲区,如下图所示:

该参数也是传入传出参数,如果不需要检测写缓冲区中的数据只需要将该参数可以指定为 NULL 即可。

(4)exceptfds

读写异常的文件描述符的集合,主要用于内核检测集合中文件描述符是否有异常状态,该参数也是传入传出,如果不需要检测读写异常缓冲区中的数据只需要将该参数可以指定为 NULL 即可。

(5)timeout

超时的时间,主要用来强制退出select()函数的阻塞状态,其值有如下几种情况:

设置值 含义
NULL select()函数检测不到就绪的文件描述符会一直阻塞
> 0 select()函数如果检测不到就绪的文件描述符,在超过设定的时间之后强制退出阻塞并返回0
0 select()函数检测一遍之后(无论是否有就绪的文件描述符),直接返回

在select函数的读集合、写集合、读写异常的集合参数前都带有fd_set,那么fd_set是什么呢?其实fd_set表示一个文件描述符的集合,大小是1024bit位。如下所示的fd_set图:

如果想要操作fd_set集合(如查询、赋值等操作),官方也提供一些函数来操作这个集合,常见的函数如下所示:

# 将fd_set对应的标志位设置为0         
void FD_CLR(int fd, fd_set *set); 
# 读取fd_set对应的标志位上的值 
int  FD_ISSET(int fd, fd_set *set); 
# 将fd_set对应的标志位设置为1 
void FD_SET(int fd, fd_set *set); 
# 将fd_set集合中所有的文件文件描述符对应的标志位设置为0, 
void FD_ZERO(fd_set *set);

后面遍历的文件描述符集合的时候,需要使用这些函数做操作。

2、select的工作原理

当客户端和服务端建立连接之后,服务端会给客户端生成一个通信文件描述,我们以读数据为案例分析Select的工作原理,如下图所示:

(1)在用户态上,给Seclet函数的读缓冲区集合的参数(参数 readfds )对应的位置设置成1,如下图所示:

(2)设置好读取缓冲区的参数之后,由select函数将数据复制到内核的文件描述符上

(3)内核根据传入的文件描述符对应位置上的值做判断,如果bit位上的值是1就检测对应的Socket上的读缓冲区数据,如果 bit 位上是0就不做检测,如下图所示:

(4)如果有客户端发送数据到内核中来的时候,如下图所示:

此时Socket0中会通过DMA技术复制数据到读缓冲区上,那么对应的文件描述符为就绪状态。内核根据文件描述符表依次线性执行下去,假设seclet函数设置了超时时间,如果在规定时间内还没有数据到达监听Socket上,此时内核将文件描述的bit位上的1修改成0(修改成0表示不满足就绪条件 ),如下所示:

内核修改好内核上的文件描述符之后,重新的将内核文件描述符信息写会用户态的fd_set上,如下图所示:

(4)select函数返回的值如下所示:

含义
> 0 检测的集合中有就绪的文件描述符
= 0 检测的集合中没有满足条件的文件描述符
= -1 检测的集合中出现异常

如果此时返回值大于0,表示有就绪状态的文件描述符,具体是哪个文件描述符就绪了,需要业务自己来遍历文件fd_set来判断。遍历中如果bit位上的值是1则表示当前的读缓冲区上有数据,开始读取数据操作,如下:

以上就是select的读取数据的整个流程,写数据的流程其实也是一样的,这里就不在具体的分析了。

总结:

(1)select的工作原理是将当前进程中所有文件描述符一次性的从用户态复制到内核态,随后在内核态中遍历每个文件描述符来判断是否就绪,、内核将所有就绪状态的文件描述符从内核态拷贝到用户态,最后用户态遍历判断具体哪个文件描述符已就绪并进行相应的业务处理。

(2)文件描述符是bit位的数组组成,长度有1024的限制并且文件描述符无法重用,每次循环都是重新创建。

7