文章目录

  • 文件传输(读取与发送)中的拷贝与上下文切换
  • 零拷贝技术
    • sendfile
    • sendfile + SG-DMA
    • mmap + write
    • splice
    • Direct I/O
  • 经典应用

文件传输(读取与发送)中的拷贝与上下文切换

如果服务端要提供文件传输的功能,最简单的方式是:
1、将磁盘上的文件读取出来
2、通过网络协议将内容发送给客户端
传统IO的工作方式是,数据读取和写入从用户空间到内核空间来回赋值,内核空间数据通过IO接口从磁盘读取/写入。
就如同下面这两个api的使用:

File.read(file, buf, len);
Socket.send(socket, buf, len);

这个场景下会发生4次数据拷贝+4次上下文切换
read系统调用,从用户态到内核态 切换 ,CPU从磁盘 拷贝 数据到内核pagecache。
read返回,从内核态 切换 到用户态,CPU从pagecache 拷贝 数据到用户缓冲区。
send,可以看作write。
write系统调用,从用户态到内核态切换,CPU从用户缓冲区拷贝数据到内核socket缓冲区
然后CPU从内核socket缓冲区拷贝数据到网卡上
最后write返回,从内核态 切换 到用户态。
当然可以使用DMA技术,替代CPU在IO外设与内核缓冲区之间的拷贝。因为DMA仅仅只能用于设备之间交换数据时的数据拷贝,内存之间的数据拷贝用不了DMA。
这样优化下来会发生2次CPU数据拷贝+2次DMA数据拷贝+4次上下文切换,接下来的讲解都是基于这个成本来的。
想要提高性能就需要减少上下文切换和CPU拷贝的次数。

零拷贝技术

零拷贝是一种高效的数据传输机制,在追求低延迟的传输场景中经常使用,具体思想是计算机执行操作时,CPU不需要将数据从某处内存复制到另外一个特定区域。
现存的比较常用的零拷贝方法有下面几个:

  • sendfile
  • mmap + write
  • splice
  • Direct I/O
    不同的技术使用的场景也是不同的,使用时请结合业务逻辑。

sendfile

应用场景:用户从磁盘读取文件数据后不需要经过CPU计算/处理就直接通过网络传输出去
典型应用:MQ
Linux版本:2.1

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
// out_fd:目的端文件描述符
// in_fd:源端文件描述符
// offset:源端偏移量
// count:数据长度
// 返回值:实际复制数据的长度

我们只需要传递文件描述符就可以代替数据的拷贝了,直接替代read+write操作。sendfile一次系统调用就相当于之前的两次系统调用。这是因为page cache和socket buffer均在内核空间,sendfile直接把内核缓冲区数据拷贝到socket缓冲区上了,直接省略掉用户态。
成本:1次系统调用,2次上下文切换,1次CPU数据拷贝,2次DMA数据拷贝

sendfile + SG-DMA

Linux版本:2.4
如果网卡支持SG-DMA(The Scatter-Gather Direct Memory Access)技术,可以直接将内核态缓冲区数据直接SG-DMA到网卡上,省略了内核态缓冲区->socket缓冲区->网卡的步骤。
成本:1次系统调用,2次上下文切换,1次DMA数据拷贝,1次SG-DMA数据拷贝
这就是真正的zero-copy,完全没有通过内存层面去拷贝数据,全程使用DMA传输。
局限性:当然sendfile也是有局限性的,它直接隔离了应用程序对数据操作,如果需要从数据中提取统计信息或者进行加解密,sendfile根本使用不了。

mmap + write

mmap:memory map,一种内存映射文件的方法。即将一个文件或者其他对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址直接对映。这样进程就可以采用指针的方式直接读写操作这一块内存,系统自动回写脏页到对应的文件磁盘上。这样对文件操作就不需要调用read+write了。并且内核空间对这段区域的修改也直接反映在了用户空间,从而实现不同进程间的文件共享。
零拷贝机制在文件传输中的使用手法-编程知识网
mmap技术特点如下:
1、用户空间的mmap file使用虚拟内存,实际上不占有物理内存,只有内核空间的kernel buffer cache才占据实际物理内存
2、mmap需要配合write
3、mmap仅仅避免内核空间到用户空间的CPU数据包被,但是内核空间内部还是需要CPU负责数据拷贝
使用mmap流程如下:
1、用户调用mmap,从用户态切换到内核态,将内核缓冲区映射到用户缓存区
2、DMA控制器将数据从磁盘拷贝到内核缓冲区
3、mmap返回,从内核态切换到用户态
4、用户进程调用write,尝试把文件数据写到内核socket buffer中,从用户态切换到内核态
5、CPU将内核缓冲区数据拷贝到socket buffer
6、DMA控制器将数据从socket buffer拷贝到网卡
7、write返回
成本:2次系统调用、4次上下文切换、1次CPU数据拷贝、2次DMA数据拷贝
应用场景
1、多个线程以只读方式同时访问一个文件,mmap机制下的多线程共享同一个物理内存空间,节约了内存。
例子:多个进程可以依赖于同一个动态链接库,利用mmap可以实现内存仅仅加载一份动态链接库,多个进程共享此库
2、mmap可用于进程间通信,对于同一个文件对应的mmap分配的物理内存天然多线程共享,可以依赖于操作系统的同步原语
3、mmap比sendfile多了一次CPU参与的内存拷贝,但是用户空间与内核空间之间不需要数据拷贝,所以效率也很高

splice

Linux版本:2.6.17

#include <fcntl.h>
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);

splice用于在两个文件描述符之间移动数据, 也是零拷贝。
fd_in参数是待输入描述符。如果它是一个管道文件描述符,则off_in必须设置为NULL;否则off_in表示从输入数据流的何处开始读取,此时若为NULL,则从输入数据流的当前偏移位置读入。

fd_out/off_out与上述相同,不过是用于输出。

len参数指定移动数据的长度。

flags参数则控制数据如何移动:

SPLICE_F_NONBLOCK:splice 操作不会被阻塞。然而,如果文件描述符没有被设置为不可被阻塞方式的 I/O ,那么调用 splice 有可能仍然被阻塞。
SPLICE_F_MORE:告知操作系统内核下一个 splice 系统调用将会有更多的数据传来。
SPLICE_F_MOVE:如果输出是文件,这个值则会使得操作系统内核尝试从输入管道缓冲区直接将数据读入到输出地址空间,这个数据传输过程没有任何数据拷贝操作发生。
2. 使用splice时, fd_in和fd_out中必须至少有一个是管道文件描述符。

调用成功时返回移动的字节数量;它可能返回0,表示没有数据需要移动,这通常发生在从管道中读数据时而该管道没有被写入的时候。

失败时返回-1,并设置errno

splice系统调用直接在内核空间的read buffer 和socket buffer之间建立了管道,避免了用户缓冲区和socket buffer之间的CPU拷贝
成本:1次splice系统调用、1次pipe调用、2次上下文切换、2次DMA数据拷贝
局限性
1、用户程序不能对数据进行操作,与sendfile类似
2、Linux管道缓冲机制,可以用于任意两个文件描述符中传输数据,但是其中一个必须是管道设备

Direct I/O

缓存文件I/O:用户空间要读取一个文件并不是直接与磁盘进行交互看,而是中间夹了一层缓存,即page cache
直接文件I/O:用户空间读取文件直接与磁盘交互,数据直接存储在用户空间中,没有中间page cache曾,绕过了内核。
部分操作系统中,在直接文件I/O模式下,write虽然能够保证文件数据落盘,但是文件元数据不一定落盘,所以还需要执行一次fsync操作。
局限性
1、设备之间数据传输通过DMA,所以用户空间的数据缓冲区内存页必须进行页锁定,这是为了防止其物理页地址被交换到磁盘或者被移动到新的地址导致DMA去拷贝数据时在指定地址找不到内存页从而引发缺页异常,而页锁定的开销也不小,所以应用程序必须分配和注册一个持久的内存池,用户数据缓冲。(应用程序手动做缓存池)
2、如果在应用程序的缓存中没有找到,那么就直接从磁盘加载,十分缓慢
3、应用层引入缓存管理以及底层硬件管理(页锁定),很麻烦

经典应用

在之前的笔记中有谈到kafka高性能的原因之一就是使用了zero-copy:消息队列重要机制讲解以及MQ设计思路(kafka、rabbitmq、rocketmq,这里稍微拓展一下:
生产者发消息给kafka,kafka将消息持久化落盘。
消费者从kafka拉取消息,kafka从磁盘读取一批数据,通过网卡发送。
接收消息持久化的时候使用到了mmap机制,对接收的数据持久化。发送消息的时候使用sendfile从持久化介质中读取数据然后对外发送。
sendfile避免了内核空间到用户空间的CPU数据拷贝,同时sendfile基于page cache实现,如果有多个消费者同时消费一个topic消息,消息会在page cache上缓存,就只需要一次磁盘IO了。
所以我们应该熟悉掌握sendfile 和 mmap