当程序需要读取或写入数据时,CPU是如何操作磁盘的呢?首先,CPU会向磁盘发送读写数据的命令,这些命令通过IO总线传输给磁盘。然而,磁盘不仅仅是存储数据的介质,还有一个重要的组成部分——接口。这个接口中包含一个称为控制器的关键部件,它实际上是负责控制磁盘读写的核心。当CPU发出读写指令时,实际上是在指示磁盘控制器执行相应操作。以读取数据为例,当控制器接收到读取请求时,会命令磁盘提供指定数据。机械硬盘在旋转并定位到目标扇区后,将数据传递给控制器:“这就是你要的数据”。然而,控制器并不会立即通知CPU,因为读取的数据可能涉及多个扇区,如果每读取一个扇区就通知CPU,效率会很低。
CPU:“控制器,你这样做不太好吧,我很忙,不想频繁地被打扰。能不能等所有数据都准备好后再通知我?”
控制器:“好的,我会考虑的。”
因此,控制器内部设置了缓冲区,先将读取的数据缓存起来,然后通知CPU来获取数据。然而,问题又出现了……
CPU:“控制器,虽然你准备好了数据,但是给我的数据已经损坏了!”
控制器:“抱歉,我犯了个错,下次不会再发生了。”
为了检查读取的数据是否损坏,控制器会先计算校验和。如果校验和不通过,控制器就不会通知CPU获取损坏的数据。
当缓冲区快要满了或者需要读取的数据已经全部准备好并且通过了校验,控制器会发出中断:“CPU,你需要的数据准备好了,来取吧”。于是CPU迅速过去取数据,尽管它是逐字节地从控制器的缓冲区中取,直到取完。尽管整个过程看起来不错,但存在一个严重的效率问题:CPU每次只能取一个字节的数据,这导致CPU需要多次往返。那么,如何解决这个问题呢?让我们继续往下看。
缓冲
在讲缓冲之前,我们先了解一下当我们的程序发出read的时候,数据是怎么返回的,首先和设备打交道的时候,需要发起系统调用,系统调用会导致进入内核态,然后CPU去读数据,读到数据后,在把数据返给用户程序,这时又回到用户态。
这里我们先着重看下数据从内核态到到用户态的过程,通过上文我们知道CPU是一个字节一个字节的读取数据的,当CPU拿到数据之后,可以有这样几个选择:
-
每次读到一个字节后立马发出中断,然后由中断程序把每个字节交给用户进程,用户进程收到数据之后,再发起下个字节的读取,就这样不停的循环…,直至把数据读完。这种模式的问题在于每个字节都要唤起进程,然后用户进程继续阻塞等待下个字节的到来,很傻很低效。
-
用户程序可以每次多读点数据,比如每次告诉CPU:“我要读n个字节”,CPU收到指令后去磁盘把数据读到,当然这里肯定不是一个字节一个字节的发起中断,不然和1无区别,由于一开始已经告诉CPU要读n个字节,所以要等读满n个字节后才能发起中断,那如何知道读满n个字节了呢?这就需要缓冲了,可以在用户空间开辟一个n个字节的缓冲区,当缓冲区满了,再发起中断,相比第一种n次中断,这里只需要一次中断,是不是效率提高了许多。
-
第二种方法解决了用户程序低效的问题,但是不要忘记了还有CPU,CPU还是一个字节一个字节的把数据搬运到用户的缓冲区中,这样看CPU还是挺辛苦的,不仅要读取数据,还要低效的把数据从内核空间搬运到用户空间,注意这个在内核空间和用户空间之间的切换还是挺耗费时间的,于是为了减少切换开销,内核空间干脆也搞个缓冲区,等缓冲区有足够多的数据之后,一次性的给到用户程序,这样是不是就高效多了。
可以发现最后一种肯定是效率最高的,这也是现代操作系统普遍使用的方式,然而这种模式也不是百分百的完美,我们来看下相关的时序图。
时序图中我们先重点看下CPU这块,可以发现当控制器的缓冲区满了之后需要CPU把数据copy到内核缓冲区,然后CPU再把内核缓冲区的数据copy到用户缓冲区,CPU不仅要负责数据的读写还要负责数据的搬运。
进阶-DMA
“我堂堂CPU,竟然要为了缓慢的磁盘而卑躬屈膝,能不能给我安排个下手呀,和低等磁盘打交道的任务就交给下手去做吧,还有其他很多进程在等着我调度呢”。于是设计者们就意识到这个问题,为了让CPU全身心的投入到调度、计算等工作中,后来就搞了个DMA(Direct Memory Access),中文名叫直接存取器存取,中文名挺抽象的,别急,我们接着往下看。
首先这个DMA它内部也有些寄存器,这些寄存器可以存什么呢?答案是内存地址,严格来说是内核缓冲区的地址。有了DMA后,read操作不再由CPU告诉磁盘,而是由CPU告诉DMA:“DMA同学现在某个程序员要读xx数据,你把xx数据放到内存地址是0x1234的内存里去吧”,DMA收到老大CPU的通知后:“收到了老大,这种小事交给小弟吧,你去忙吧”,到这里CPU就去忙别的事了,然后DMA就去通知我们的磁盘控制器了:“你先把xx数据的这一部分直接读到0x1234内存里去吧,读完告诉我一下,我这边还有xx数据的另一部分”,磁盘控制器:“好的,老大哥”,就这样每次控制器读完一部分数据之后就会通知DMA,然后DMA让它再读下一个数据,直至把需要读的数据读完,在读完了数据之后,肯定不能完事呀,这时得告诉老大哥CPU,于是DMA发出一个中断:“CPU大哥,数据已读取完毕,请享用~”,CPU收到通知后,发现数据已经在内核缓冲区了,不需要亲自干一个字节一个字节搬运的鸟事了,而且这期间CPU指挥了三次交通(调度)、扶了四个老奶奶过了马路(计算)。
-
CPU告诉DMA -
DMA告诉磁盘 -
磁盘读完之后告诉DMA -
DMA如果还需要读的话,会重复2,3步骤 -
DMA干完活之后通知CPU
DMA的出现无疑是帮助了CPU很多,特别是和IO设备打交道这块。
正常来说我们的程序在发起读数据后,需要等待数据的返回,因此需要CPU把内核缓冲区的数据再次COPY到用户缓冲区中,同时整个过程用户进程是阻塞的(因为要等数据),这一切看起来很合理,然而其实有这样一种场景:我们需要把读出的数据通过网络发出去,比如kafka,我们知道kafka是非常经典的消息引擎,当消费者需要消费消息的时候,kafka中的broker会把数据读出来,然后发给我们的消费者。
图中有两次看起来非常沙雕的操作,分别是第2步和第3步,关键这两步都需要CPU亲自参与搬运,并且涉及到内核态->用户态->内核态的上下文切换,这个上下文切换会导致什么呢?答案就是CPU需要进行现场保护(活干到一半就被打断了,等忙完了回来还要接着干),这个保护需要花费一定的开销,比如把当前的运行状态给保存下来,程序执行到哪了,寄存器该保存什么值…。
那有什么办法能省掉这次的开销呢?
升华
mmap + write
其实明眼人都看出来了,没必要把一份数据copy来copy去的,直接用内核态的缓冲区不就行了,这就是mmap(内存映射),我们还是先来看个例子,通过例子你就明白mmap的好处了:
现在有两个进程A和B,他们都需要读同一份数据,因此每个进程都要开辟一块用户态的缓冲区,即使数据是一样的,并且CPU还要发生两次copy,而且这只是两个进程,如果有更多的进程势必造成更多的内存空间浪费,于是就出现了mmap,有了mmap之后,不需要cpu copy数据了,并且进程A和进程B共享用户空间的一块内存,然后这块内存和内核空间的内存打通,注意这里并不是copy而是开启了一个映射,相当于开了一个VIP通道,有了VIP通道之后,同一份数据对于不同的进程不需要维护不同的内存空间了,因为大家共享一个公共的内存空间。
mmap只是打通了用户空间和内核空间之间的通路,可以说路是通了,接下来还要发数据呀,因此这时一般调用write把数据发出去,有了mmap+write ,我们再来看看这时数据是如何发出的:
-
首先肯定是程序发出mmap系统调用请求,然后DMA把数据copy到内核缓冲区去 -
DMA copy完之后,把内核缓冲区映射到用户缓冲区,注意映射和copy不一样,比copy的开销小 -
然后用户程序再次发起write请求 -
这时系统会把内核缓冲区的数据直接发到socket缓冲区 -
DMA copy socket缓冲区数据到网卡
通过 mmap + write 的方式可以发现少了一次CPU copy,但是系统调用并没有减少,有没有什么办法让系统调用再少些?
sendfile
没有什么能阻止进步的脚步,于是出现了sendfile,有了sendfile函数之后,首先它不需要进行两次系统调用,只需要一次系统调用,当我们sendfile之后等于告诉系统:“帮我把xx数据直接发出去吧,别再copy或者映射进来了,俺不需要,直接发出去就好”。
-
当我们发起sendfile之后,首先会切到内核态 -
然后DMA把数据copy到内核缓冲区 -
DMA把socket描述符等传到socket缓冲区 -
同时DMA把数据直接从内核缓冲区copy到网卡
可以发现这种方式是目前最优的方式了,通过sendfile+DMA技术可以实现真正的零拷贝,整个过程都不要cpu搬运数据,也没有上下文切换,kafka就是利用这种方式来提供吞吐的。
以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !