IO多路复用底层原理分析

前言

最近一直忙着找实习以及小论文的实验,导致最近半个月都没有汲取到新的知识,也就这两天空闲的时候才能继续看看Netty。其实网上关于Netty的文章很多,但是能够从底层原理去解释的却不多,我们都知道Netty底层是通过IO多路复用来实现的,那么你们有没有考虑过在底层上IO多路复用又是是如何实现的?

这篇就从底层的select函数以及文件驱动poll函数来分享一下我的认识,若如有误的地方,大家可以给个issue。

IO多路复用

首先来了解一下什么是IO多路复用呢。可以拆开来理解,IO多路可以简单的理解为多个I/O流,复用即多个I/O流共用了一个线程,换一种说法单个线程来管理跟踪多个I/O流。

多路复用有了简单的了解后,现在问一下自己如果让你自己来实现一个IO多路复用你会怎么实现?那简单,写一个死循环一直去遍历流,如果流有读、写、异常的事件就返回该流:

while true {
        for(i in stream[]) {
            if(i has data)
                read until unavailable
        }
}

这种做法的缺陷也非常的明显,如果没有准备就绪的流,那么会浪费CPU时间。那么有没有一种方法当所有的流没有准备就绪时,让线程阻塞起来,直到有就绪的流出现,让线程再执行返回呢?下面就来看看select函数是如何做的。

select函数

正如前面说的一样,为了不浪费CPU的时间,可以采用select来实现IO多路复用。

当所有的流都没有准备就绪时,会把当前线程阻塞掉;当有一个或多个流的I/O事件就绪时,就从阻塞状态中醒来,然后轮询一遍所有的流,处理已经准备好的I/O事件。轮询的过程可以参照如下:

while true {
        select(streams[])//会被阻塞
        for(i in stream[]) {
            if(i has data)
                read until unavailable
        }
}

那么有没有人有疑惑,select居然这么强大,它的底层原理是如何实现的呢?这也是我写这篇博客的主要原因,下面就来分析一下select函数究竟做了什么。

为了能解释清楚,先来讲一下什么是文件描述符(FD):

文件描述符是内核为了高效管理已经被打开的文件所创建的索引,他是一个从0开始的整数,程序所有执行的I/O操作都是通过文件描述符进行的。其中,在程序刚刚启动时,0,1,2三个文件描述符已经被占用了,0代表标准输入设备stdin(比如键盘),1代表标准输出设备stdout(显示器),2代表标准错误stderr。因此再打开一个文件,它的文件描述符会是3。

注意这里的文件并不是平时看到的txt,在unix中所有东西都是文件。文件就是一串二进制流,不管socket,还是FIFO、管道、终端,对我们来说,一切都是文件,一切都是流。

清楚了什么是FD之后,再来看看select函数:

int select(int maxfd,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

中间的三个参数readset、writeset和exceptset指定我们要让内核监测读、写和异常条件的文件描述字集合。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符。

timeout告知内核等待所指定文件描述字中的任何一个就绪可花多少时间。

select的实现依赖于文件的驱动函数poll,在unix中无论是调用 select、poll 还是epoll,最终都会调用该函数

我们想在用户空间对一个文件使用多路I/O复用,那么我们需要实现该文件驱动的poll函数。对于一般的磁盘文件而言,这些函数都已在文件系统驱动中实现,而对于自己定义的设备,我们需要自己实现poll函数。

文件驱动poll函数

select会循环遍历它所监测的fd_set内的所有文件描述符对应的驱动程序的poll函数。

驱动程序提供的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select当前资源哪些可用。当select循环遍历完所有fd_set内指定的文件描述符对应的poll函数后,如果没有一个资源可用(即没有一个文件可供操作),则select让该进程睡眠,一直等到有资源可用为止,进程被唤醒(或者timeout)继续往下执行。

看到这里相信大家已经明白select是如何做到当没有IO流时被阻塞,简单来说就是:

每一个文件描述符有对应的文件驱动poll函数,select在实际调用过程中会调用(readset、writeset和exceptset)每一个文件描述符的文件驱动函数poll

文件驱动函数poll会将调用select的进程放在设备对应资源的等待队列中。当有描述符可进行非阻塞I/O操作时,内核唤醒该描述符poll等待队列中的阻塞进程;进程唤醒后继续执行I/O复用函数,I/O复用函数将进程从描述符表中所有描述符的poll等待队列中移除;然后重新遍历每一个文件驱动函数poll

参考文章

http://blog.chinaunix.net/uid-20643761-id-1594860.html

https://blog.csdn.net/genzld/article/details/84995021

https://www.cnblogs.com/ck1020/p/7263552.html

https://blog.csdn.net/u014590757/article/details/80106135

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章