网络IO是如何一步一步走向零拷贝的

你们知道当程序需要读取或者写入数据的时候,CPU是如何操作我们的磁盘的吗?首先CPU肯定是要把读写数据的命令告诉给磁盘,这个命令可以通过IO总线传给磁盘,那这里有个细节,其实我们常说的磁盘不仅仅是只包含存储数据的媒介,还有接口,接口相信大家都熟悉,接口的意义不仅仅是为了连接到IO总线上的,其实这个接口里还有个叫做控制器的东西,控制器才是真正控制磁盘读写的东西,当CPU发出读写指令的时候,这个指令其实是告诉磁盘控制器的 。以读为例,当控制器收到读的请求时,它告诉磁盘:“你把xx数据给我吧”,当机械硬盘经过转动、寻道找到目标扇区后,把目标数据给磁盘控制器:“哥,这是你要的数据”,控制器收到数据之后,其实不会立马通知CPU,因为需要读的数据可能涉及到多个扇区,如果每读一个扇区的数据就通知,会导致效率低下 。
CPU:“控制器老弟,你这是搞事啊,我很忙的,每次搞这么点数据就通知我,能不能把我需要的数据都准备好,再通知我” 。
“控制器”:“好的,CPU老哥” 。
于是控制器内部就搞了个缓冲区,把读到的数据先缓存起来,然后通知CPU来取数据,但是问题又发生了...
CPU:“控制器老弟,数据你是准备好了,但是你给我的数据已经是损坏的,玩我呢!”
“控制器”:“CPU老哥,俺错了,下次一定不会” 。
于是控制器为了判断读到的数据是否发生了损坏,会先计算下校验和,如果校验和不通过,那么就不会通知CPU来取坏的数据了 。
当缓冲区快要满了或者需要读的数据已经读完了并且校验数据也是OK的,这时控制器就会发出个中断:“CPU老哥,你要的数据好了,过来取吧”,于是CPU屁颠屁颠的过来拿数据,当然它也是分批拿的,每次从控制器的缓冲区中一个字节一个字节的拿,直至取完 。整个过程看起来还不错,但是有个很严重的效率问题:CPU每次取数据的单位有点小(一个字节),这样势必造成CPU多次往返,那有什么办法解决这个问题呢?我们接着往下看 。
缓冲
在讲缓冲之前,我们先了解一下当我们的程序发出read的时候,数据是怎么返回的,首先和设备打交道的时候,需要发起系统调用,系统调用会导致进入内核态,然后CPU去读数据,读到数据后,在把数据返给用户程序,这时又回到用户态 。

网络IO是如何一步一步走向零拷贝的

文章插图
 
【网络IO是如何一步一步走向零拷贝的】这里我们先着重看下数据从内核态到到用户态的过程,通过上文我们知道CPU是一个字节一个字节的读取数据的,当CPU拿到数据之后,可以有这样几个选择:
每次读到一个字节后立马发出中断,然后由中断程序把每个字节交给用户进程,用户进程收到数据之后,再发起下个字节的读取,就这样不停的循环...,直至把数据读完 。这种模式的问题在于每个字节都要唤起进程,然后用户进程继续阻塞等待下个字节的到来,很傻很低效 。
网络IO是如何一步一步走向零拷贝的

文章插图
 
用户程序可以每次多读点数据,比如每次告诉CPU:“我要读n个字节”,CPU收到指令后去磁盘把数据读到,当然这里肯定不是一个字节一个字节的发起中断,不然和1无区别,由于一开始已经告诉CPU要读n个字节,所以要等读满n个字节后才能发起中断,那如何知道读满n个字节了呢?这就需要缓冲了,可以在用户空间开辟一个n个字节的缓冲区,当缓冲区满了,再发起中断,相比第一种n次中断,这里只需要一次中断,是不是效率提高了许多 。
网络IO是如何一步一步走向零拷贝的

文章插图
 
第二种方法解决了用户程序低效的问题,但是不要忘记了还有CPU,CPU还是一个字节一个字节的把数据搬运到用户的缓冲区中,这样看CPU还是挺辛苦的,不仅要读取数据,还要低效的把数据从内核空间搬运到用户空间,注意这个在内核空间和用户空间之间的切换还是挺耗费时间的,于是为了减少切换开销,内核空间干脆也搞个缓冲区,等缓冲区有足够多的数据之后,一次性的给到用户程序,这样是不是就高效多了 。
网络IO是如何一步一步走向零拷贝的

文章插图
 
可以发现最后一种肯定是效率最高的,这也是现代操作系统普遍使用的方式,然而这种模式也不是百分百的完美,我们来看下相关的时序图 。


推荐阅读