0、引言
在现代网络系统中,随着数据传输量的不断增加,如何高效地处理网络请求成为了一个重要的研究课题。本文将从操作系统底层数据传输的过程出发,探讨零拷贝技术的原理以及它如何优化数据传输效率。这对今后学习多种相关技术将有助于我们理解其根本原理。
1、传统的磁盘I/O原理
在最初的操作系统设计中,系统内部的I/O过程涉及到以下主要步骤:
- 用户进程调用系统函数read()方法,向操作系统请求读取数据,然后进程被挂起进行阻塞等待,此时系统从用户态转入到内核态
- CPU收到请求后,紧接着向磁盘发起I/O请求,接着返回
- 磁盘收到I/O请求后,会将磁盘中的数据放到磁盘内部的缓冲区中,然后向CPU发出一个IO中断信号
- CPU收到中断信号后,开始亲自参与到数据转移的过程中,将磁盘控制器缓冲区的数据拷贝到内存的PageCache中
- 接着CPU又从PageCache中拷贝数据到用户缓冲区中
- 完成拷贝之后,CPU会进行返回,通知进程数据准备完毕,此时系统从内核态转入到用户态
可以看到在整个数据的传输过程中,CPU要进行数据从磁盘到内存的搬运操作,在这个过程中,CPU不能进行其他的事情。这样子做带来的后果就是,当需要搬运的数据很大的时候,就需要大量的占用CPU的时间,此时操作系统的性能就会变得很差。
那么,有没有什么办法能让CPU不去参与搬运数据的操作呢?这样子就能让CPU去处理更多重要的事务了。科学家就基于该角度,提出了DMA技术,也就是直接内存访问(Direct Memory Access)技术。
1.1、什么是pagecache?
在上面的磁盘I/O流程我们可以看到,第一次的数据拷贝是将数据从磁盘控制器拷贝到内核缓冲区中,这个内核缓冲区就是磁盘高速缓存(PageCache)。
为什么要有PageCache?
我们知道,磁盘的读取速度相对于内存的读取速度慢了非常多,所以我们能把“读写磁盘”这个过程替换成“读写内存”就好了,所以我们引用了PageCache这个机制来提高磁盘I/O性能,只要将频繁访问的数据存放在PageCache中,就能显著减少对磁盘的访问次数从而提高性能。
主要的问题是,内存的大小远远小于磁盘,那么应该选择哪些数据拷贝到内存中呢?
我们知道程序运行的时候,是具有“局部性”的,即刚被访问的数据在短时间内再次被访问的概率很高,于是我们就将这部分数据存放在PageCache中,当空间不足就淘汰掉最久未访问的数据。
所以,当系统需要读取数据的时候,应该先从PageCache中找,如果没有命中就从磁盘中读取。
还有一点,读取磁盘数据的时候,需要找到数据所在的位置,对于机械硬盘就需要通过磁头旋转到数据所在的盘去,再开始“顺序”读取数据,这个过程是非常耗时的,为了降低这里的能耗输出,PageCache使用了[预读功能]
比如,假设read方法每次会读取32KB
的字节,虽然read刚开始只会读0~32KB的字节,但是内核会将后面的32~64KB的数据也读取到PageCache,这样子当后面需要读取该数据时,访问的成本就很低了。
于是,PageCache的优点主要有两个:
- 缓存最近被访问的数据
- 预读功能
2、DMA技术参与的磁盘I/O
DMA(Direct Memory Access)是一种用于数据传输的技术,它允许某些硬件子系统直接与内存进行通信,而无需经过CPU,这种技术带来的显著优点就是提高了数据传输的效率,减轻了CPU的负担。
有DMA技术参与的数据传输流程大致步骤如下:
- 用户进程调用系统函数read()方法,向操作系统请求读取数据,然后进程被挂起进行阻塞等待,此时系统从用户态转入到内核态
- CPU收到请求后,进一步将IO请求发送给DMA,此时CPU就可以返回去做其他的事情
- DMA进一步将IO请求发送给磁盘
- 磁盘收到请求后,将数据准备好放在磁盘控制器缓冲区中,接着通知DMA控制器
- DMA控制器将数据从磁盘缓冲区拷贝到自己的内核缓冲区中,此时不占用CPU
- 当DMA读取完数据,就将中断信号发送给CPU
- CPU知道数据准备完毕后,就将数据从内核缓冲区拷贝到用户缓冲区,完成read()调用的返回
可以看到,并不是说整个过程都不需要CPU的参与了,只是CPU不必再参与到“将数据从磁盘控制器缓冲区拷贝到内核空间”的工作了,这部分工作全程都由DMA完成。但是CPU在这个过程中也是必不可少的,传输什么数据、从哪里运输到哪里都需要CPU来告诉DMA控制器。
这部分带来的性能优化很大的原因是因为,搬运数据的时耗主要是由磁盘的读取速度来决定的,我们知道磁盘的读取速度相对于内存、CPU的运行速率相差是非常巨大的,将CPU从这部分时间抽出来去做其他的任务就能提高了整体的系统性能。
3、文件传输
3.1、传统的文件传输性能之差
在介绍完DMA技术之后,我们继续往上,来学习当需要进行一次网络I/O的时候,系统内部的文件传输原理。
当服务端需要提供文件传输的功能的时候,我们能想到的一个简单的方式是:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。
传统I/O的工作方式是,数据读取和写入是从用户空间到内核空间的来回复制,而内核空间的数据是通过操作系统的I/O接口从磁盘读取或者写入。
一般我们会用到read、write两个系统调用,该调用发生的事情可以用下图来描述。
在一套调用中,系统总共发生了4次用户态和内核态之间的切换。上下文切换的成本并不小,一次切换要几十纳秒到几十微秒,虽然时间看上去很短,但是在高并发的场景下这种积累会被放大,影响到系统的整体性能。
其次,还发生了4次数据拷贝,两次是DMA的拷贝,两次是CPU的拷贝。
从结果上来,我们为了完成一次数据从磁盘文件到网卡上的传输,一共经历了4次数据的拷贝,太多了!这些不必要的开销必然导致性能的下降。
所以想要提升文件传输的性能,就需要减少“用户态和内核态的切换”和“内存拷贝”的次数。
3.2、如何优化文件传输的性能
首先先从减少“用户态与内核态的切换”的次数来入手。
读取磁盘的数据的时候,之所以要进行上下文的切换,是因为用户空间没有权限去操作磁盘或者网卡,内核的权限最高,这些操作应该都需要交由操作系统内核完成才能保证系统的安全性。所以当需要进行一些内核参与才能完成的任务的时候,就需要去进行系统函数的调用。
所以,想要减少上下文切换的次数,就要减少系统调用的次数。
接着,来看如何减少“数据拷贝”的次数
在刚刚的文件传输中我们知道,我们只需要达成数据从磁盘到达网卡的这一个“结果”,对于中间的数据从内核搬运到用户缓冲区,又从用户缓冲区搬运到内核缓冲区,这一部分我们是不需要的,因为我们不需要对数据的中间加工。
于是,我们得出结论,用户的缓冲区在这里是没有必要存在的。
3.3、零拷贝技术
什么是零拷贝技术?
零拷贝技术是一种用于提高数据传输效率的技术,它通过减少不必要的数据拷贝,降低CPU的负担,从而提高系统性能。⚠️注意,零拷贝并不是说真的“不需要进行数据拷贝了”,而是消除了冗余的拷贝操作。
零拷贝技术的实现方式通常有两种:
mmap+write
sendfile
下面我们来探讨他们的实现原理。
3.3.1、mmap+wrtie
使用mmap()
+write
的方式为我们减少了一次数据拷贝的过程,可以将mmap()
代替read()
的调用,其实现原理是将内核缓冲区里的数据“映射”到用户空间,这样就减少了系统内核与用户空间之间多余的数据拷贝操作。
具体的过程如下:
- 系统进程调用了
mmap()
后,DMA会把数据拷贝到内核的缓冲区中,接着进程和操作系统“共享”了这个缓冲区 - 进程再调用
write()
,操作系统直接将内核缓冲区的数据拷贝到socket缓冲区中,这一切都发生在内核态,CPU来参与数据搬运 - 最后,再把内核的socket缓冲区里的数据拷贝到网卡的缓冲区中,这个过程由DMA搬运
通过这个操作,我们成功减少了1次的数据拷贝过程,但是任然需要4次的上下文切换,因为系统调用还是2次。
这里的“零拷贝”指的是数据从用户态和内核态之间的零拷贝,还是文字游戏呀。
3.3.2、sendfile
在linux内核版本2.1中,提供了一个专门发送文件的系统调用函数sendfile()
,形式如下
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
其前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。
首先,它替代了前面的read()
和write()
这两个系统调用,这样就可以减少了一次系统调用,从而减少了2次上下文切换的开销。
其次,该系统调用可以直接把内核缓冲区里的数据拷贝到socket缓冲区里,不再拷贝到用户态,这样子就只有3次数据拷贝了。
然而这还并不是零拷贝技术的“完全体”,如果网卡支持SG-DMA(The Scatter-Gather DMA)技术,sendfile()
可以进一步减少通过CPU将数据从内核缓冲区拷贝到socket缓冲区的过程
具体流程如下图。
这就是真正意义上的零拷贝技术,在全过程中没有通过CPU来搬运数据,所有的数据都是通过DMA来进行传输的。
零拷贝技术的文件传输方式相比于传统的文件传输方式,减少了2次上下文切换和2次数据拷贝次数就可以完成数据传输,且数据拷贝的过程都是由DMA来搬运。
所以总体来看,零拷贝技术可以把文件传输的性能提高至少一倍以上。
4、使用零拷贝技术的项目
消息队列Kafka就利用了零拷贝技术,从而大幅度提升了I/O的吞吐率,这就是Kafka在处理海量数据这么快的原因。
追溯Kafka文件传输的代码,就会发现它调用了Java NIO库里的transferTo
方法:
long transferForm(FileChannel fileChnnel, long position, long count) throws IOException{
return fileChannel.transferTo(position, count, sokcetChnnel);
}
如果Linux系统支持sendfile()
系统调用,那么transferTo()
实际上最后就会用到sendfile()
系统调用函数。
下面是一张在同样的硬件条件下,使用了零拷贝技术的文件传输上的性能差异数据图,使用了零拷贝技术可以缩短65%
的传输时间,大幅度提升了机器传输数据的吞吐量。
Nginx也支持零拷贝技术,一般默认是开启零拷贝技术,有利于提高文件传输的效率。
5、大文件传输的方式
在1.1节中,我们聊到了PageCache机制,但是当我们需要去传输大文件(GB级别的文件)的时候,PageCache就起不到作用,因为它无法就存储如此多的数据,那么就会白白浪费掉DMA多做的一次数据拷贝,造成性能的降低。
由于文件太大,会带来两个问题:
- PageCache由于长时间被大文件占据,其他「热点」的小文件就可能无法充分的利用PageCache的优势,如此磁盘读写的性能就会下降了。
- PageCache中的大文件数据,没有享受到缓存带来的优势,但却耗费DMA多拷贝到PageCache一次。
所以,对于大文件的传输,我们不应该使用到零拷贝技术。
回顾最初的read()调用,我们知道当进程需要读取数据时,会阻塞地等待read()调用的返回,针对这一段时间的优化,我们可以采用异步I/O
来解决,其工作方式如下。
它将读操作分为两部分:
- 前半部分,内核向磁盘发起读请求,但是可以不等待数据就为就返回,于是进程可以去处理其他的任务。
- 后半部分,当内核将磁盘中的数据拷贝到进程缓冲区后,进程就接收到内核的通知,再去处理数据。
我们发现,异步IO没有涉及到PageCache,所以使用异步I/O就意味着绕开了PageCache。
绕开PageCache的I/O叫做直接I/O,使用PageCache的I/O叫做缓存I/O。通常,对于磁盘来说,异步I/O只支持直接I/O。
于是,在高并发的场景下,针对大文件的传输的方式,应该使用[异步I/O+直接I/O]来替代零拷贝技术。
直接I/O应用场景常见的两种:
- 应用程序已经实现了磁盘数据的缓存,那么可以不需PageCache再次缓存,减少额外的性能损耗。
- 传输大文件时,由于大文件难以命中PageCache,而且会占满缓存导致“热点”文件无法充分的利用,从而增大了性能的开销,因此应该直接使用直接I/O。
另外,由于直接I/O绕开了PageCache,就无法享受内核的这两点的优化:
- 内核的I/O调度算法会缓存尽可能多的I/O请求在PageCache中,最终「合并」成一个更大的I/O请求再发给磁盘,为了减少磁盘的寻址操作。
- 内核也会「预读」后续的I/O请求放在PageCache中,一样是为了减少对磁盘的操作。
6、总结
早期的I/O操作,内存与磁盘的数据传输的工作都是CPU完成的,而此时CPU不能执行其他的任务,会浪费CPU的资源。
为了优化这一个问题,DMA技术诞生了,每一个I/O设备都有自己的DMA控制器,CPU只需要告诉DMA控制器需要搬运什么数据、从哪里搬运到哪里即可,于是CPU不再参与数据的传输工作。
传统的IO工作方式中,系统从硬盘读取数据,再通过网卡向外发送,需要4次上下文切换,和4次数据拷贝,其中2次数据拷贝发生在内存的缓冲区和对应的硬件设备之间,这个由DMA来完成;另外两次发生在内核态和用户态之间,由CPU来完成数据搬运。
为了提高文件传输的性能,引入了零拷贝技术,通过系统调用(sendfile
方法)合并了磁盘读取与网络发送两个操作,降低了上下文切换次数和数据拷贝次数。
Kafka和Nginx都实现了零拷贝技术,大大提高了文件传输的性能。
零拷贝技术是基于PageCache的,PageCache会缓存最近访问的数据,提升了访问缓存数据的性能,同时为了解决机械硬盘寻址慢的问题,还协助I/O调度算法实现了IO合并与预读,这也是顺序读比随机读性能好的原因。这些优势进一步提升了零拷贝的性能。
需要注意的是,零拷贝技术是不允许进程对文件内容进一步加工的,使用sendfile只能拿到发送数据的长度,而不能获取数据的具体消息。
当传输大文件时,不能使用零拷贝,因为可能由于PageCache被大文件占据,许多「热点」的小文件无法利用到PageCache导致对磁盘的访问增加。对于大文件传输应该使用「异步IO+直接IO」的方式。
摘抄总结至:小林coding
标签:PageCache,技术,学习,内核,磁盘,拷贝,数据,CPU From: https://www.cnblogs.com/MelonTe/p/18516984