Netty 零拷贝(一)Linux 零拷贝
本文探讨 Linux 中主要的几种零拷贝技术以及零拷贝技术适用的场景。
一、几个重要的概念
1.1 用户空间与内核空间
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核 (kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
1.2 IO 两个流程
网络 IO 的本质是 socket 的读取,socket 在 linux 系统被抽象为流,IO 可以理解为对流的操作。刚才说了,对于一次 IO 访问 (以 read 举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个 read 操作发生时,它会经历两个阶段:
- 第一阶段:等待网络上的数据分组到达,然后被复制到内核的某个缓冲区 (Waiting for the data to be ready)。
- 第二阶段:把数据从内核缓冲区复制到应用进程缓冲区 (Copying the data from the kernel to the process)。
本文关注的是第二个过程:如何减少拷贝,即零拷贝。
二、传统的 IO 流程
Linux 的 I/O 操作默认是缓冲 I/O。使用了 read 和 write 两个系统调用,我们并不知道操作系统在其中做了什么。实际上在以上 I/O 操作中,发生了多次的数据拷贝。
当应用程序访问某块数据时,操作系统首先会检查,是不是最近访问过此文件,文件内容是否缓存在内核缓冲区,如果是,操作系统则直接根据 read 系统调用提供的 buf 地址,将内核缓冲区的内容拷贝到 buf 所指定的用户空间缓冲区中去。如果不是,操作系统则首先将磁盘上的数据拷贝的内核缓冲区,这一步目前主要依靠 DMA 来传输,然后再把内核缓冲区上的内容拷贝到用户缓冲区中。
接下来,write 系统调用再把用户缓冲区的内容拷贝到网络堆栈相关的内核缓冲区中,最后 socket 再把内核缓冲区的内容发送到网卡上。
整个过程*产生了多次数据拷贝,即使用了 DMA 来处理了与硬件的通讯,用户空间和系统空间 CPU 仍然需要处理两次数据拷贝,与此同时,在用户态与内核态也发生了 4 次上下文切换,无疑也加重了 CPU 负担。
- 通过 DMA copy 数据从 hard drive 拷贝到 kernel buffer
- 通过 CPU copy 数据从 kernel buffer 拷贝到 user buffer
- 通过 CPU copy 数据从 user buffer 拷贝到 kernel buffer
- 通过 CPU copy 数据从 kernel buffer 拷贝到 socket buffer
- 通过 DMA copy 将 socket buffer 中的数据发送出去
在此过程中,我们没有对文件内容做任何修改,那么在内核空间和用户空间来回拷贝数据无疑就是一种浪费,而零拷贝主要就是为了解决这种低效性。
三、零拷贝(zero-copy)
3.1 零拷贝概念
零拷贝主要的任务就是避免 CPU 将数据从一块存储拷贝到另外一块存储,主要就是利用各种零拷贝技术,避免让 CPU 做大量的数据拷贝任务,减少不必要的拷贝,或者让别的组件来做这一类简单的数据传输任务,让 CPU 解脱出来专注于别的任务。这样就可以让系统资源的利用更加有效。
如何减少数据拷贝的次数呢?一个很明显的着力点就是减少数据在内核空间和用户空间来回拷贝,这也引入了零拷贝的一个类型:减少用户空间到内核空间的拷贝。
2.2 减少用户空间和内核空间的拷贝
应用程序调用 sendfile,磁盘上的数据会通过 DMA 被拷贝的内核缓冲区,接着操作系统会把这段内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。应用程序再调用 write(),操作系统直接将内核缓冲区的内容拷贝到 socket 缓冲区中,这一切都发生在内核态,最后,socket 缓冲区再把数据发到网卡去。
目前为止,我们已经减少了数据拷贝的次数,但是仍然存在一次拷贝,就是页缓存到 socket 缓存的拷贝。那么能不能把这个拷贝也省略呢?
2.3 直接传递文件描述符
在上一种方案中是将页缓存的数据拷贝到 socket 缓存中,实际上,我们仅仅需要把缓冲区描述符传到 socket 缓冲区,再把数据长度传过去,这样 DMA 控制器直接将页缓存中的数据打包发送到网络中就可以了。不过这种收集拷贝功能是需要硬件以及驱动程序支持的。
在服务端响应客户端的场景中,如果使用非直接缓冲区第一步就需要将响应的数据从 JVM 内存拷贝到系统内核中再发送,而使用直接缓冲区就可以省略这个步骤,这就是 零拷贝 。
四、总结
传统的 IO 读和写都需要在操作系统内核和用户空间之间拷贝,Linus 优化了内核空间和用户空间的拷贝过程,内核空间也可以通过传递文件描述符进一步减少内核中的一次拷贝过程。Linux 零拷贝演进过程:
每天用心记录一点点。内容也许不重要,但习惯很重要!