共计 3058 个字符,预计需要花费 8 分钟才能阅读完成。
本文说的零拷贝都是基于网络传输。
什么是零拷贝
零拷贝并不是不须要拷贝,而是缩小不必要的拷贝次数。
传统 IO 流程
通常咱们须要拜访硬盘数据的时候,用户过程须要借助内核来拜访硬盘的数据;用户通过调用零碎办法,如 read()、write()等办法告诉内核,让内核做相应的事件。
read();
传统读取数据的流程:
在没有 DMA 之前的拷贝流程如上图所示:
- 用户调用 read 零碎办法
- CPU 收到 read 申请后,给磁盘发动一个对应的指令
- 硬盘筹备好数据,并将数据放到缓冲区中,给 CPU 发动 IO 中断指令
- CPU 收到中断指令后,暂停正在做得事件,将磁盘中的数据读取到内核缓冲区中
- 紧接着,CPU 将数据拷贝到用户缓冲区中
- 此时,用户便可拜访数据
以上流程中,波及到数据的拷贝都须要 CPU 来实现,CPU 是十分宝贵的资源,CPU 在拷贝数据的时候,无奈做其余的事件,如果传输的数据十分大,那么 CPU 始终在拷贝数据,无奈执行其余工作,代价十分大。
DMA
实质上,DMA 技术就是计算机主板上一块独立的芯片,当计算机须要在内存和 I/O 设施进行数据传输的时候,不再须要 CPU 来执行耗时的 IO 操作,而是通过 DMA 控制器来实现,流程如下。
上图可知,数据拷贝由 DMA 实现,CPU 不须要在执行一些耗时的 IO 操作。
下图能够更形象的表白文件传输的过程:
步骤阐明如下:
- 用户过程调用零碎函数 read()
- 内核接管到对应指令之后去磁盘将文件读取到内核缓冲区中,数据筹备好之后发动一个 IO 中断
- CPU 收到 IO 中断信号之后进行手中的工作,将内核缓冲区中的数据拷贝到用户过程中
- 用户过程收到数据之后调用零碎函数 write(),由 CPU 将数据拷贝到 socket 缓冲区中
- 由 DMA 控制器将 socket 缓冲区中的数据拷贝到网卡中,进行数据传输。
以上传统的 IO 数据拷贝在性能上有很大的晋升空间,
由上图看出,在文件传输的案例中,咱们将数据拷贝到用户数据缓冲区,用户过程没有通过任何数据处理,将文件间接发送进来。因而,这一个步骤是多余的,能够省略。
实现零拷贝
零拷贝的实现次要是针对上下文切换和拷贝的次数进行优化,通过缩小上下文切换和缩小数据拷贝的次数来达到优化的目标。
实现形式一:mmap(..) + write(..)
什么是 mmap
mmap 全称 Memory Mapped Files,是一种内存文件映射的办法,将一个文件或者其余对象映射到过程的地址空间,实现文件磁盘地址和过程虚拟地址空间中的一段虚拟地址的一一映射关系,映射关系生成之后,用户过程能够通过指针操作内存中的文件数据,零碎会主动将操作后的数据写入到磁盘中,而不须要调用 read(),write()等零碎调用来操作数据。
实现过程
应用 mmap()函数替换 read()函数,mmap 将内核缓冲区中的数据映射到用户空间,用户空间与内核之间就不须要进行数据的拷贝,他们能够进行数据共享。
如图能够看出,数据不再拷贝到用户缓冲区
- 用户过程调用零碎函数 mmap()后,DMA 会将数据从磁盘拷贝到内核缓冲区中,用户过程与内核缓冲区共享这块内存数据;
- 用户过程调用 write()函数,CPU 将数据从内核缓冲区拷贝到 socket 缓冲区中;
- 最初,DMA 将 socket 缓冲区中的数据拷贝到网卡中,进行数据发送。
mmap 缩小了一次数据的拷贝,性能有所晋升,但还是存在 4 次用户态和内核态的切换,并不是最现实的零拷贝。
如何缩小上下文切换?
用户过程没有权限间接操作磁盘的数据,内核领有上帝的权限,所以用户过程能够通过调用零碎函数(如 read,wirte)将工作交给内核来实现。
一次零碎调用会产生两次上下文切换,先从用户态切换到内核态执行工作,工作执行实现后,从内核态切换到用户态,用户过程继续执行逻辑。
上下文的切回须要消耗工夫,每次上下文切换消耗几纳秒到几奥妙,看起来工夫很短,但在并发下会成倍放大。
因而,咱们须要缩小上下文切换的次数,要缩小上下文切换的次数,就须要缩小零碎函数调用的次数。
实现形式二:sendfile 函数
Linux 2.1 版本后提供了一个专门发送文件的零碎调用函数 sendfile(),函数如下:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
参数阐明:
out_fd:目的地端的文件描述符
in_fd:源端的文件描述符
offset:源端的偏移量
count:复制的长度
返回理论复制数据的长度
sendfile 函数用于代替 read 和 write 两个函数,这样就能够缩小一次零碎调用,缩小两次上下文切换的开销。
$ ethtool -k eth0 | grep scatter-gather | |
scatter-gather: on |
- DMA 将磁盘中的数据拷贝到内核缓冲区
- 而后将内核缓冲区中的文件描述符和数据长度传送到 socket 中(不须要将数据拷贝到 socket 中)
- 网卡的 SG-DMA 控制器将内核缓冲区中的数据拷贝到网卡中,实现数据传输
以上过程只波及到一次零碎函数调用,2 次上下文切换,2 次 DMA 数据拷贝,不须要 CPU 拷贝数据,实现了真正的零拷贝。
mmap 和 sendfile 比照
- 都是通过调用操作系统提供的 API 函数来实现
- mmap 为文件内存映射,用户过程对映射内存中的数据反对读和写操作,最终后果会反馈在磁盘上
- sendfile 将数据读取到内核缓冲区之后,网卡通过 SG-DMA 控制器将数据复制过去
- mmap 实现零拷贝波及两次零碎函数调用,产生 4 次上下文切换,三次数据拷贝,不属于真正意义上的零拷贝
- sendfile 只有一次零碎函数调用,产生 2 次上下文切换,2 次必要的数据拷贝,实现了真正意义上的零拷贝
- mmap 优化更多的是在写申请上,sendfile 更多是优化读申请
内核缓冲区(PageCache)
PageCache 为磁盘的高速缓冲区,因为在磁盘中找数据是十分耗时的操作,所以将磁盘中的局部数据缓存到 PageCache 中,将读写磁盘的操作转换到内存中,进步读写的效率。
PageCache 内存空间相比于磁盘来说小很多,所以咱们不可能把所有的数据放到磁盘中,那么咱们须要将什么数据读取到内存中,读取多大?
PageCache 应用了预读性能,如果咱们须要读取 32kb 的数据,然而加载到内存中的数据不只是 32kb,它会以页为单位(每页 64kb)来读取数据,所以不仅会读取 0-32kb 的数据,还会读取 32-64kb 的数据,这样 32-64kb 局部的数据读取的代价十分小,如果在内存淘汰前被过程应用到,收益十分大。
所以 PageCache 有两个次要的益处:
- 缓存最近被拜访的数据
- 预读性能
说白了 PageCache 的诞生就是为了进步磁盘的读写性能。
总结
- 零拷贝并不是不须要拷贝,而是缩小不必要的拷贝,更要防止应用 CPU 进行数据拷贝。
- DMA 拷贝技术很好的代替 CPU 拷贝
- sendfile()函数实现了真正意义上的零拷贝,只须要 2 次 DMA 拷贝,1 次零碎函数调用,2 次上下文切换
探讨
- PageCahe 内存无限,如果咱们读取一个大文件,PageCahe 很快就会被占满,如果长时间暂用 PageCahe,那么其余热点数据就无奈应用到 PageCahe 的益处,会导致磁盘的性能升高,这时怎么办?
答:先说答案:异步 IO + 间接 IO 这个状况咱们应该想方法绕过 PageCache,大文件不应该应用 PageCache 间接 IO 会间接绕过 PageCacheIO 的读取是阻塞的,能够思考应用异步 IO 去代替
- RocketMQ 为什么应用 mmap 而不适应 sendfile?
欢送大家探讨。
文 / 木匠
关注得物技术,携手走向技术的云端