零拷贝总结

1.缓冲区

一个java进程发起read请求加载数据大致的流程图:
在这里插入图片描述

2.虚拟内存

在这里插入图片描述
所有现代操作系统都使用虚拟内存,使用虚拟地址取代物理地址,好处是:
1)多个虚拟地址可以指向同一个物理内存地址
2)虚拟内存空间可大于实际可用的物理地址;
利用第一条特性可以把内核空间地址和用户空间的虚拟地址映射到同一个物理地址,这样DMA就可以填充对内核和用户空间进程同时可见的缓冲区了。

3.传统的文件数据

在这里插入图片描述
步骤:
1)系统调用read方法,上下文切换到内核态(切换),DMA把磁盘数据复制到内核缓冲区
2)read调用返回,上下文切换到用户进程(切换),CPU把数据复制到用户的缓冲区
3)write系统调用,上下文切换到内核态(切换),CPU把数据复制到内核Socket缓冲区
4)write调用返回,上下文切换到用户进程(切换)
5)DMA把数据复制到网卡缓冲区
总结:4次上下文切换 + 2次CPU复制 + 2次DMA复制。

4.多次拷贝原因分析

1)操作系统为了保护系统不被应用程序破坏,设置了用户态和内核态。用户态想要获取系统资源(例如访问硬盘), 必须通过系统调用进入到内核态, 由内核态获取到系统资源,再切换回用户态返回应用程序。
2)出于异步写入避免每次写都需要IO操作等性能优化, OS在内核态中也增加了一个"内核缓冲区",读取数据时先读取到kernel buffer, 再由kernel buffer复制到应用程序buffer。

5.系统调用mmap()来代替read

在这里插入图片描述
在这里插入图片描述
步骤:
1)系统调用mmap(),上下文切换到内核态(切换),DMA把磁盘数据复制到内核缓冲区。
2)操作系统把这段内核缓冲区与应用程序共享(就不需要往用户空间拷贝了)。
3)mmap()调用返回,上下文切换到用户态(切换)。
4)write系统调用,上下文切换到内核态(切换),CPU把内核缓冲区数据复制到内核缓冲区
5)write调用返回,上下文切换到用户进程(切换)
6)DMA把数据复制到网卡缓冲区(协议栈)
总结:
1)4次上下文切换 + 1次CPU复制 + 2次DMA复制。
2)有陷阱,当程序map了一个文件,但是被另一个进程截断(truncate)时, write系统调用会因为访问非法地址而被SIGBUS信号终止,会使你进程被杀掉。可以使用文件锁来解决。

6.sendfile调用(内核版本2.1)

在这里插入图片描述
总结:
2次上下文切换 + 1次CPU拷贝 + 2次DMA拷贝。

7.sendfile调用(内核版本2.4)

在这里插入图片描述
步骤:
1)应用程序开始读文件的操作
2)应用程序发起系统调用, 从用户态进入到内核态(第一次上下文切换)
3)内核态中把数据从硬盘文件读取到内核中间缓冲区
4)内核态中把数据在内核缓冲区的位置(offset)和数据大小(size)两个信息追加(append)到socket的缓冲区中去
5)网卡的buf上根据socekt缓冲区的offset和size从内核缓冲区中直接拷贝数据
6)从内核态返回到用户态(第二次上下文切换)
相比于sendfile2.1的优化点:
sendfile2.1中会将内核缓冲区的全量数据CPU拷贝到Socket缓冲区。而sendfile2.4中的CPU拷贝只是offset、size等一点点信息。
总结:
2次上下文切换 + 2次DMA拷贝。

8.零拷贝的零体现在哪

零拷贝,是从OS角度来看,内核中没有数据是重复。sendFile 2.4中,只有 kernel buffer 有一份数据,是零拷贝。而mmap和sendFile 2.1 方式,在内核中有 2 份数据,算不上零拷贝。

java零拷贝

1)MappedByteBuffer
2)DirectByteBuffer
3)Channel-to-Channel传输

其他零拷贝

1)Netty零拷贝
2)RocketMQ采用零拷贝mmap+write
3)kafka中存在大量的网络数据持久化和磁盘文件通过网络发送的过程,使用了sendfile零拷贝方式;

10.总结

1)mmap方式:4次上下文切换 + 1次CPU复制 + 2次DMA复制。 2)sendFile 2.1 方式:2次上下文切换 + 1次CPU拷贝 + 2次DMA拷贝。 3)sendFile 2.4方式:2次上下文切换 + 2次DMA拷贝。 4)零拷贝,更少的数据复制,更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。