普通模式数据交互
普通模式数据交互分为仅CPU和CPU&DMA两种方式。
仅CPU方式
read流程:
- 当程序执行read()时,调用syscall从用户态切换到内核态;
- CPU向磁盘发起IO请求,磁盘收到后开始Ready数据;
- 磁盘将数据放到磁盘缓冲区之后,向CPU发起I/O中断,报告CPU数据已经Ready了(可见其使用了 Page Cache 机制);
- CPU收到磁盘控制器的I/O中断之后,把磁盘缓冲区数据拷贝到内核缓冲区中;
- 然后再把数据从内核缓冲区拷贝到用户缓冲区中
- 完成之后syscall返回,也就是read()的返回,从内核态切换到用户态;
小结:
- 仅CPU方式read流程经历了2次CPU拷贝,2次空间切换。
- 而write流程也是2次CPU拷贝,2次空间切换。
CPU&DMA
我们可以看出仅CPU方式需要跟硬件交互,很耗CPU,这是在浪费CPU资源。
于是就有了DMA(Direct Memory Access, 直接内存访问)来分担CPU的活。
有必要先讲一下DMA是什么,DMA是一种硬件设备绕开CPU独立直接访问内存的机制。所以DMA在一定程度上解放了CPU,与硬件交互的工作让硬件直接自己做了,提高了CPU效率。支持DMA的有网卡、声卡、显卡、磁盘控制器等。
CPU&DMA方式对比仅CPU方式的区别就是,CPU不再和磁盘直接交互,而是DMA和磁盘交互,使得读写运行效率更高。
并且在DMA读写硬件时,CPU可以执行其他任务。
read流程:
- 当程序执行read()时,调用syscall从用户态切换到内核态;
- DMA向磁盘发起IO请求,磁盘收到后开始Ready数据;
- 磁盘将数据放到磁盘缓冲区之后,向DMA发起信号,报告DMA数据已经Ready了;
- DMA收到磁盘控制器的I/O中断之后,把磁盘缓冲区数据拷贝到内核缓冲区中;
- 然后CPU再把数据从内核缓冲区拷贝到用户缓冲区中;
- 完成之后syscall返回,也就是read()的返回,从内核态切换到用户态。
小结:
- CPU&DMA方式read流程经历了1次CPU拷贝,1次DMA拷贝,2次空间切换。
- 而write流程也是1次CPU拷贝,1次DMA拷贝,2次空间切换。
Zero-Copy技术
通过上面两种普通模式数据交互的方式发现,如果我们执行read和write操作,会经历四次数据拷贝和四次空间切换。基于这两点我们可以做如下优化:
-
内核缓冲区->用户缓冲区->套接字缓冲区
这两次数据拷贝是有冗余的,我们可以优化成从内核缓冲区直接拷贝到套接字缓冲区; - 四次空间切换是比较耗时的操作,我们可以减少空间切换次数来增加效率。
当程序中有系统调用语句,程序执行到系统调用时,首先使用类似int 80H的软中断指令,保存现场,去系统调用,在内核态执行,然后恢复现场。每个进程都会有两个栈,一个内核态栈和一个用户态栈。当int中断执行时就会由用户态栈转向内核态栈,系统调用时需要进行栈的切换。而且内核代码对用户不信任,需要进行额外的检查。系统调用的返回过程有很多额外工作,比如检查是否需要调度等。
系统调用一般都需要保存用户程序得上下文(context),在进入内核的时候需要保存用户态的寄存器,在内核态返回用户态的时候会恢复这些寄存器的内容。这是一个开销的地方。 如果需要在不同用户程序间切换的话,还要更新cr3寄存器,这样会更换每个程序的虚拟内存到物理内存映射表的地址,也是一个比较高负担的操作
基于上面两点优化,实现了这些零拷贝技术:mmap+write、sendfile、sendfile+DMA收集、splice等。
mmap+write
mmap即memory map,也就是内存映射。我在mmap的使用一文中详细介绍了mmap的用法、特点、注意事项等信息,感兴趣的同学可以去看看。
mmap+write流程:
- 用户进程调用 mmap(),从用户态切换到内核态,将内核缓冲区映射到用户缓存区;
- DMA 控制器将数据从硬盘拷贝到内核缓冲区;
- mmap() 返回,上下文从内核态切换回用户态;
- 用户进程调用 write(),从用户态切换到内核态;
- CPU 将内核缓冲区中的数据拷贝到的套接字缓冲区;
- DMA 控制器将数据从套接字缓冲区拷贝到网卡完成数据传输;
- write() 返回,从内核态切换回用户态。
小结:
- MMap+write流程经历了1次CPU拷贝,2次DMA拷贝,4次空间切换。比CPU&DMA少了一次CPU拷贝。
sendfile
不管是ready+write还是mmap+write,都是使用两个接口来做数据传输,按照第二点优化思路,我们可以用一个系统调用接口来实现,这样空间切换就会从四次缩为两次。
sendfile是Linux 内核2.1版本中被引入,与mmap+write一样,sendfile是从内核缓冲区拷贝到socket缓冲区,流程如下:
小结:
- sendfile流程经历了1次CPU拷贝,2次DMA拷贝,2次空间切换。比CPU&DMA少了一次CPU拷贝和两次空间切换,由于数据不经过用户缓冲区,因此数据无法被修改。
sendfile+DMA
从 Linux 内核 2.4 版本开始起,sendfile() 系统调用的过程发生了点变化,具体过程如下:
- 通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;
- 将内核空间缓冲区中对应的数据描述信息(文件描述符、地址偏移量等信息)记录到socket缓冲区中。
- DMA控制器根据socket缓冲区中的地址和偏移量将数据从内核缓冲区拷贝到网卡中,从而省去了内核空间中仅剩1次CPU拷贝。
这种方式才是实现了真正的零拷贝,真正的解放了CPU。但是这种方式需要硬件DMA控制器的配合。流程图示如下:
小结:
- sendfile+DMA流程经历了0次CPU拷贝,2次DMA拷贝,2次空间切换。比CPU&DMA少了两次CPU拷贝和两次空间切换,需要DMA支持,数据仍然无法被修改。
splice
- splice系统调用是Linux 在 2.6 版本引入的,其不需要硬件支持,并且不再限定于socket上,实现两个普通文件之间的数据零拷贝。
- splice 系统调用可以在内核缓冲区和socket缓冲区之间建立管道来传输数据,避免了两者之间的 CPU 拷贝操作。
- splice也有一些局限,它的两个文件描述符参数中有一个必须是管道设备。
小结:
- splice流程经历了0次CPU拷贝,2次DMA拷贝,2次空间切换。比CPU&DMA少了两次CPU拷贝和两次空间切换,数据仍然无法被修改,但是不依赖硬件,只要有一个管道设备即可。
大文件传输
内核缓冲区
为什么磁盘数据拷贝到网卡中,需要经过内核缓冲区呢?原因是磁盘的读写速度太慢。
内核缓冲区工作:
- 缓存最近被访问的数据。最近访问过的数据接下来很可能还会被访问,所以利用PageCache 缓存最近被访问的数据,读磁盘数据的时候,优先在 PageCache 找,如果数据存在则可以直接返回;如果没有,则从磁盘中读取,然后缓存在 PageCache 中。当PageCache的空间不足时,淘汰最久未被访问的缓存。
- 预读功能。利用空间局部性原理,假设 read 方法每次只会读 32 KB 的字节,虽然 read 刚开始只会读 0 ~ 32 KB 的字节,但内核会把其后面的 32~64 KB 也读取到 PageCache,这样后面读取 32~64 KB 的成本就很低,如果在 32~64 KB 淘汰出 PageCache 前,进程读取到它了,收益就非常大。
- 内核的 I/O 调度算法会缓存尽可能多的 I/O 请求在 内核缓存区中,最后合并成一个更大的 I/O 请求再发给磁盘,这样做是为了减少磁盘的寻址操作。
大文件的缓存命中率不高,并且可能内核缓存区被大文件占据,而导致其他的热点小文件无法利用内核缓存区。
所以适用内核缓冲器时,不适合大文件传输。也就是说零拷贝技术不适合大文件传输。
异步IO
我们了解了为什么使用内核缓冲区后,就能自然而然的想到,如果可以忍受磁盘读写速度就可以避免使用内核缓冲区。
异步IO就是可以忍受磁盘读写速度,因为线程不用等待异步IO的执行结果。
所以异步IO可以做到直接从磁盘缓冲区拷贝到用户缓冲区,适用于大文件传输。
总结
本文介绍了CPU和CPU&DMA两种数据传输的流程:
- 仅CPU方式的read+write流程经历了4次CPU拷贝,4次空间切换
- CPU&DMA方式的read+write流程经历了2次CPU拷贝,2次DMA拷贝,4次空间切换。
然后介绍了优化的点分别是减少数据拷贝次数和介绍空间切换次数,于是引出了零拷贝技术:mmap+write、sendfile、sendfile+DMA收集、splice等。
- MMap+write流程经历了1次CPU拷贝,2次DMA拷贝,4次空间切换。比CPU&DMA少了一次CPU拷贝。
- sendfile流程经历了1次CPU拷贝,2次DMA拷贝,2次空间切换。比CPU&DMA少了一次CPU拷贝和两次空间切换,由于数据不经过用户缓冲区,因此数据无法被修改。
- sendfile+DMA流程经历了0次CPU拷贝,2次DMA拷贝,2次空间切换。比CPU&DMA少了两次CPU拷贝和两次空间切换,需要DMA支持,数据仍然无法被修改。
- splice流程经历了0次CPU拷贝,2次DMA拷贝,2次空间切换。比CPU&DMA少了两次CPU拷贝和两次空间切换,数据仍然无法被修改,但是不依赖硬件,只要有一个管道设备即可。
最后介绍了内核缓冲区的作用,也引出了异步IO可以处理大文件传输工作。
引用
wiki Page_cache
wiki Zero-copy
wiki Mmap
man2 sendfile
wiki Splice