零拷贝相较于传统的IO流程拥有更高的数据发送效率,无论是RocketMq,Kafka还是Netty等都用到了零拷贝技术,那究竟什么是零拷贝呢,零拷贝又是通过什么方式提升数据发送效率呢?
首先我们要明白,一次数据发送过程就是将磁盘中的目标数据交给网卡传输出去的流程。磁盘以及网卡都属于硬件层。而应用程序是不能直接操作硬件的。如果要操作硬件,需要进行上下文切换从用户态切换到内核态由操作系统来完成硬件交互。关于用户态,内核态,上下文切换这些这里不再赘述。
传统IO
先看一下传统IO的数据发送流程:
- 应用程序调用read函数,IO开始。进行上下文切换,从用户态切换到内核态。
- DMA控制器将磁盘中的数据拷贝到OS缓冲区
- CPU将OS缓冲区的数据拷贝到用户缓冲区。进行上下文切换,从内核态切换到用户态。
- 应用程序调用write函数往Socket中写数据。进行上下文切换,从用户态切换到内核态。CPU将用户缓冲区的数据拷贝到Socket缓冲区
- DMA控制器将Socket缓冲区中的数据拷贝到网卡中完成数据发送
- 进行上下文切换,从内核态切换到用户态。一次数据发送流程结束。
可以看出传统的IO流程包括4次上下文的切换,4次拷贝数据(两次CPU拷贝以及两次DMA拷贝)。
CPU拷贝和DMA拷贝
数据拷贝流程都是CPU来负责进行拷贝的。但是和磁盘,网卡这种硬件交互时,因为硬件的速度限制,如果CPU全程参与拷贝,那么就很浪费CPU的时间片。DMA便是用来优化这个过程的,DMA,英文全称是Direct Memory Access,即直接内存访问。DMA本质上是一块主板上独立的芯片,能在外设设备和内存存储器之间直接进行IO数据传输,CPU只需发起拷贝命令给DMA,DMA就能完成数据的拷贝。其拷贝过程不需要CPU的参与。 可以简单的理解为,DMA是硬件为CPU找的一个助手,对于硬件方面较慢的数据拷贝,CPU只需将指令发给DMA,DMA就能帮忙CPU完成数据拷贝,这样高效的CPU就能空下来处理其它事情。
这是一个硬件层次的优化,只需要知道有这个技术即可。
零拷贝技术
零拷贝并不是说不会发生任何数据拷贝,而是不发生OS缓冲区到用户缓冲区的拷贝。以此减少内核态和用户态之间切换以及CPU拷贝的次数。目前零拷贝技术有三种:
- mmap+write
- sendfile (以及升级版的带有DMA收集功能的sendFile)
mmap和sendFile各有优缺点,并不是说谁一定优于谁。各有各的适用场景。比如RocketMQ就是用的mmap,而RabbitMQ用的则是sendfile。
1 mmap+write
mmap通过内存映射来减少上下文切换与CPU拷贝。
内存映射
内存映射即在进程的虚拟地址空间创建一个映射,分为两种:
文件映射:文件支持的内存映射,把文件的一个区间映射到进程的虚拟地址空间,数据源是存储设备上的文件。
匿名映射:没有文件支持的内存映射,把物理内存映射到进程的虚拟地址空间,没有数据源。
即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程可以采用指针的方式读写操作这一段内存,而无需将数据由内核态拷贝到用户态。且系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。
可以理解为java中的浅拷贝。用户进程的缓冲指向OS的缓冲,这时用户进程对内存映射对象的操作
mmap就是一种内存映射文件的方法。
mmap
mmap函数原型如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr:指定映射的虚拟内存地址
length:映射的长度
prot:映射内存的保护模式
flags:指定映射的类型
fd:进行映射的文件句柄
offset:文件偏移量
Java中的实现是MappedByteBuffer
,通过channel#map
方法得到。
通过内存映射,IO流程就变成了这样
- 应用程序调用mmap函数,IO开始。进行上下文切换,从用户态切换到内核态。
- DMA控制器将磁盘中的数据拷贝到OS缓冲区
- mmap函数建立内存映射完毕。进行上下文切换,从内核态切换到用户态。
- 应用程序调用write函数通过内存映射往Socket中写数据。进行上下文切换,从用户态切换到内核态。
- CPU将OS缓冲区的数据拷贝到Socket缓冲区。
- DMA控制器将Socket缓冲区中的数据拷贝到网卡中完成数据发送
- 进行上下文切换,从内核态切换到用户态。一次数据发送流程结束。
关键就在于3,4,5三步。 mmap无需像传统IO一样操作数据需要通过CPU拷贝将OS缓冲区的数据拷贝到用户缓冲区。因此在第3步和第4步也就无需进行CPU的数据拷贝。发起write指令后,因为数据实际是在OS缓冲区的,所以CPU可以直接将数据从OS缓冲区拷贝到Socket缓冲区。
可以看到mmap+write的方式包括4次上下文的切换,3次拷贝数据(一次CPU拷贝以及两次DMA拷贝)。
sendFile
sendFile搜索Linux2.1版本内核引入的一个系统调用函数,原型如下:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
out_fd:为待写入内容的文件描述符
in_fd:为待读出内容的文件描述符
offset:文件偏移量
count:指定在fdout和fdin之间传输的字节数
Java对sendfile的支持是NIO中的FileChannel.transferTo()
或者transferFrom()
。
sendFile可以在两个文件描述符之间传输数据,整个传输过程在内核态完成,避免了数据在OS缓冲区和用户缓冲区之间的拷贝,而且减少了上下文切换次数;
sendFile流程是这样的:
- 应用程序调用sendFile函数指出源描述符和目标描述符,IO开始。进行上下文切换,从用户态切换到内核态。
- DMA控制器将磁盘中的数据拷贝到OS缓冲区
- CPU将OS缓冲区(源描述符)的数据拷贝到Socket缓冲区(目标描述符)。
- DMA控制器将Socket缓冲区中的数据拷贝到网卡中完成数据发送
- 进行上下文切换,从内核态切换到用户态。一次数据发送流程结束。
可以发现,sendfile实现的零拷贝仅仅发生了2次上下文切换以及3次拷贝(2次DMA拷贝+1次CPU拷贝)
sendfile +DMA scatter/gather实现的零拷贝
linux2.4 版本,sendfile进行了优化升级, 引入SG-DMA技术。其实就是对DMA拷贝加入了scatter/gather操作,可以让DMA在多个缓冲区实现一个简单的IO操作,比如从通道中读取数据到多个缓冲区,或者从多个缓冲区写入数据到通道。这使得数据可以直接从OS缓冲区到网卡。彻底避免了CPU拷贝。
流程如下:
- 应用程序调用sendFile函数指出源描述符和目标描述符,IO开始。进行上下文切换,从用户态切换到内核态。
- DMA控制器将磁盘中的数据拷贝到OS缓冲区
- CPU将OS缓冲区的文件描述信息(包括内存地址和偏移量)发送到socket缓冲区
- DMA通过文件描述信息直接将数据从OS缓冲区拷贝到网卡。
- 进行上下文切换,从内核态切换到用户态。一次数据发送流程结束。
可以发现,sendfile +DMA scatter/gather实现的零拷贝仅仅发生了2次上下文切换以及2次拷贝(2次DMA拷贝),实现了真正意思上不通过CPU搬运数据的零拷贝。
kafka和RocketMQ的零拷贝
从上面的流程来看sendFile是明显比mmap更高效的。但是因为sendFile相当于原汁原味的读写,直接将硬盘上的数据发送给网卡,如果需要对硬盘的数据做一定的修改再发送给网卡的话,就不适合使用sendFile了。
在基于这种区别,数据发送上,Kafka与RocketMQ采用了不同方式的零拷贝。Kafka采用了sendFile,而RocketMQ则采用了mmap.
RocketMQ为了写入的速率,是将所有的队列数据统一写入同一个CommitLog来实现顺序写。这就是导致消费时需要读出CommitLog进行应用层过滤,所以就不能用到sendFile+DMA的零拷贝,而只能使用mmap.
kafka则是同一个队列的数据存再一起,发送时无需过滤,因此可以使用sendFile来获得更高的发送效率。
PS:
【JAVA核心知识】系列导航 [持续更新中…]
关联导航:39:一文看懂RocketMQ
欢迎关注…
参考资料:
内存映射原理及mmap
【Linux内核】内存映射原理
什么是零拷贝
mmap 相比 sendFile 有什么优势?