Linux的标准IO过程中,数据将缓存在Page Cache中。也就是说,在读取数据过程中,数据将首先被复制到内核缓冲区(Page Cache),之后再由内核缓冲区复制到用户缓冲区中;在写数据过程中,用户缓冲区中的内容也将首先复制到内核缓冲区,进而被写到磁盘或者通过socket发送到网络。
采用Page Cache,在一定程度上实现了应用程序和物理设备的分离,也减少了IO次数,提高了系统的性能。但在某些场景下,也需要其它优化措施。

DMA:外部设备与内存数据交互优化
在将数据读取到内核缓存和从内核缓冲写回的过程,都要CPU全程参与。由于这个过程可花费较长时间,就会导致CPU被IO占用,极大地降低了CPU的利用率。因此,诞生了DMA技术。DMA技术是Direct Memory Access的缩写,其意思是“存储器直接访问”。它是指一种高速的数据传输操作,允许在外部设备和内存之间直接读写数据,既不通过CPU,也不需要CPU干预。
DMA是指外部设备不通过CPU而直接与系统内存交换数据的接口技术。要把外设的数据读入内存或把内存的数据传送到外设,一般都要通过CPU控制完成,如CPU程序查询或中断方式。利用中断进行数据传送,可以大大提高CPU的利用率。但是采用中断传送有它的缺点,对于一个高速I/O设备,以及批量交换数据的情况,只能采用DMA方式,才能解决效率和速度问题。DMA在外设与内存间直接进行数据交换,而不通过CPU,这样数据传送的速度就取决于存储器和外设的工作速度。
Direct IO:绕过内核缓冲区
对于某些特殊的应用程序来说,其具有自己用户空间的缓存机制,并不需要内核中缓存,比如数据库管理系统就是这列应用的代表。这种情况下,就需要采用Direct IO。Direct IO避开了内核缓冲区,在用户地址空间和磁盘之间传输数据,从而降低了系统级别管理对应用程序访问数据的影响,获取到更好的性能。除了应用程序已经实现了磁盘文件的缓存这种场景之外,高并发环境中大文件的传输也需要用到Direct IO。大文件难以命中 PageCache 缓存,又带来额外的内存拷贝,同时还挤占了小文件使用 PageCache 时需要的内存,因此,这时应该使用Direct IO。
Direct IO绕过内存缓冲区,减少了内核缓冲区和用户数据复制次数,降低了文件读写所带来的CPU负载能力和内存带宽的占用率。然而,Direct IO并非没有缺点。首先,不经过内存缓冲区直接进行磁盘读写操作,必然会引起阻塞,因而需要将Direct IO与异步IO一起使用。然后,除了缓存外,内核(IO 调度算法)会试图缓存尽量多的连续 IO 在 PageCache 中,最后合并成一个更大的 IO 再发给磁盘,这样可以减少磁盘的寻址操作,内核也会预读后续的 IO 放在 PageCache 中,减少磁盘操作。Direct IO 绕过了 PageCache,所以无法享受Page Cache所带来的性能提升。
零拷贝技术:绕过用户态,用户缓冲区不参与数据拷贝
考虑服务器文件传输的场景,直接读取文件,然后通过网络发送出去。假如使用上面所说的标准IO,数据就需要被copy到用户缓冲区,再从用户缓冲区copy到内核缓冲区中。然而,如果数据并没有发生改变,经过用户缓冲区就是一种浪费。为了避免内核态与用户态之间数据的多余copy,减少上下文切换,就需要采用零拷贝技术。
应用程序对文件的访问常用的访问方式有两种:一种是利用系统调用read()
和write()
进行寻址访问;另一种是通过系统调用mmap()
创建直接访问的虚拟地址空间映射。上文标准IO所示采用了read/write
方式。如果应用程序调用mmap()
,磁盘上的数据会通过DMA被拷贝的内核缓冲区,接着操作系统会把这段内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。应用程序再调用write()
,操作系统直接将内核缓冲区的内容拷贝到socket缓冲区中,这一切都发生在内核态,最后,socket缓冲区再把数据发到网卡去。使用mmap替代read很明显减少了至少一次拷贝,但是仍旧会有用户空间和内核空间的上下文切换。当拷贝数据量很大时,将大大提升效率。使用mmap
时map一个文件前,需要对文件进行加锁,否则当这个文件被另一个进程截断(truncate)时, write
系统调用会因为访问非法地址而被SIGBUS信号终止。SIGBUS信号默认会杀死应用进程并产生一个coredump,如果服务器这样被中止了,那会产生一笔损失。
Linux内核还提供了sendfile
操作,使用sendfile
不仅减少了数据拷贝的次数,还减少了上下文切换,数据传送始终只发生在内核态中。

如果网卡支持 SG-DMA
(The Scatter-Gather Direct Memory Access)技术,还可以再去除 Socket 缓冲区的拷贝,这样一共只有 2 次内存拷贝。

最后,简单介绍下mmap
和 sendFile
的主要区别:
mmap
适合小数据量读写,sendFile
适合大文件传输。mmap
需要 4 次上下文切换,3 次数据拷贝;sendFile
需要 3 次上下文切换,最少 2 次数据拷贝。sendFile
可以利用 DMA 方式,减少 CPU 拷贝,mmap
则不能(必须从内核拷贝到 Socket 缓冲区)。





