背景
有这样一个场景,有两台服务器A,B。你在A服务器上写了一个程序,这个程序功能是将服务器A的数据拷贝到服务器B上。这个功能会经历下面几步。
“1.数据从(A服务器)数据库中读取,拷贝到内核空间的缓存中。 2.内核空间的数据拷贝到用户空间的缓存中。 3.用户空间的数据拷贝到Socket buffer(套接字缓存)中。 4.数据从Socket buffer中拷贝到NIC buffer(网卡缓冲区)。 ”
一共经历了4次拷贝。我只要传输数据而已,就需要4次拷贝数据。
第2、3步有个很大的问题:内核空间与用户空间的来回切换。这种切换很耗资源。
用户空间与内核空间
定义
内核空间:操作系统的核心就是内核(kernel),它独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的权限。操作系统是不会让用户直接操作内核的。内核所运行的空间就是内核空间。
用户空间:就是应用程序运行的空间。
用户空间如何访问内核空间
在实际场景中,有很多操作都需要在内核空间进行。比如磁盘数据的读取,网卡数据的读取,内存的分配回收等。我们编写的应用程序是无法直接去操作的。应用程序只能通过内核提供的接口去操作,这就是常说的系统调用。通过系统调用,进程从用户空间进入到了内核空间(用户态 -> 内核态)。
进程进入了内核空间,将数据从里面拷贝到用户空间。这时,进程从内核空间转出到了用户空间(内核态 -> 用户态)。
系统调用为何那么耗时
那么哪些方式可以触发系统调用呢,在Linux系统下有这几种方式:
“
- 使用软件中断(Software interrupt)触发系统调用;
- 使用
SYSCALL
/SYSENTER
等汇编指令触发系统调用; - 使用虚拟动态共享对象(virtual dynamic shared object、vDSO)执行系统调用;
”
下面是基于中断的系统调用,看看这个流程,挺复杂。应用程序通过软件中断陷入内核态并在内核态查询并执行系统调用表注册的函数,整个过程不仅需要存储寄存器中的数据、从用户态切换至内核态,还需要完成验证参数的合法性,这些操作都需要带来额外的开销。
那么汇编指令发出的系统调用性能如何?
有些汇编指令如SYSENTER
和 SYSCALL
是专门为系统调用设计的汇编指令,它们不需要在中断描述表(Interrupt Descriptor Table、IDT)中查找系统调用对应的执行过程,也不需要保存堆栈和返回地址等信息,所以能够减少所需要的额外开销。
相对来说,汇编指令发出的系统调用性能上要优于中断的系统调用。
当然,使用虚拟动态共享对象(vSDO)执行系统调用性能更优越,这里涉及到Linux知识,不做细讲。
但是vSDO性能这么优良,无论什么情况下都使用vSDO可以么。当然不行。
“
- 使用软件中断触发的系统调用需要保存堆栈和返回地址等信息,还要在中断描述表中查找系统调用的响应函数,虽然多数的操作系统不会使用
INT 0x80
触发系统调用,但是在一些特殊场景下,我们仍然需要利用这一古老的技术; - 使用汇编指令
SYSCALL
/SYSENTER
执行系统调用是今天最常见的方法,作为专门为系统调用打造的指令,它们可以省去一些不必要的步骤,降低系统调用的开销; - 使用 vSDO 执行系统调用是操作系统为我们提供的最快路径,该方式可以将系统调用的开销与函数调用拉平,不过因为将内核态的系统调用映射到『用户态』确实存在安全风险,所以操作系统也仅会放开有限的系统调用;s
”
优化一
如上图,通过调用transferTo()
,直接将数据从读缓冲区拷贝到套接字缓冲区,这样就没有内核空间到用户空间的来回拷贝了。
优化二
然而还是有3次拷贝,我们还是要继续优化。
将只包含关于数据的位置和长度的信息的描述符被追加到了socket buffer 缓冲区中。DMA引擎直接把数据从内核缓冲区传输到协议引擎(protocol engine),从而消除了最后一次CPU copy。经过上述过程,数据只经过了2次copy就从磁盘传送出去了。
零拷贝并不是真的就没有拷贝了,它是去掉了用户空间与内核空间的来回拷贝。对于用户空间来讲,我并没有拷贝,所以相对的来说就是零拷贝。
哪些地方用到了零拷贝技术
说了这么多,你可能会觉得我说的太理论,太抽象。这个技术实际哪里有用到呢?
Kafka
Kafka之所以支持高吞吐量的文件传输很大一个原因就是使用了零拷贝。
Netty
Netty中也使用了零拷贝技术,不过这里的零拷贝技术是基于用户层面的。
Java NIO 中的零拷贝 - MappedByteBuffer
MappedByteBuffer
是 NIO 基于内存映射 (mmap) 这种零拷贝方式的提供的一种实现,它继承自 ByteBuffer
。
Spark
Spark虽然是一个高效的积极使用内存的计算框架,但在需要使用磁盘时也会适当地溢写。零拷贝机制在Spark Core中主要就被用来优化Shuffle过程中的溢写逻辑。由于Shuffle过程涉及大量的数据交换,因此效率当然是越高越好。