Linux 零拷贝方案

一 什么是零拷贝(zero copy)? 零拷贝有什么优点?

1.1 什么是零拷贝?

零拷贝: 指的是在I/O过程中,用户空间和内核空间不需要进行CPU数据拷贝。零拷贝并不是指I/O过程中一次拷贝都没有发生。

传统的I/O读的时候,需要将内核缓冲区的数据拷贝到用户空间的缓冲区;传统I/O写的时候,需要将用户空间缓冲区数据拷贝到内核缓冲区。但是零拷贝就是避免了这种拷贝操作,提升了I/O性能。比如我们读取磁盘数据,然后写入网卡为例,按照传统I/O的工作方式如图所示:
在这里插入图片描述

从上图我们可以看到,总共发生了4次内核态集和用户态的切换、发生了4次数据拷贝。

1.2 零拷贝有什么优点

减少了CPU拷贝、降低了用户态和内核态切换次数,从而提升了I/O的性能。

二 零拷贝方案

零拷贝有多种实现方案,比如减少用户空间和内核空间的拷贝次数,也可以用户空间直接绕过内核空间直接进行I/O操作等,这里主要讲的是减少用户空间和内核空间的拷贝次数这种方案,也是目前用的比较多的方案。

2.1 mmap+write

我们知道,传统的I/O读写,将数据从磁盘写入网卡需要4次用户态和内核态的切换以及4次拷贝,性能不高。而文件内存映射因为直接可以在用户空间操作内核缓冲区或者说页高速缓存,只要指定了目的地,就可以在内核空间中直接拷贝数据,这样就可以避免在用户空间和内核空间多次进行数据拷贝。

2.1.1 mmap函数

void *mmap(void *addr, size_t length, int prot , int flags ,int fd, off_t offset);

addr: 开始映射的地址,属于进程的逻辑地址
length: 从开始映射地址,映射多长,一般是一个页大小,4KB
prot:期望的内存保护标志,不能与文件打开的标志冲突,比如文件只可读,这里的就不能可写
#1 PROT_EXEC 页内容可以被执行
#2 PROT_READ 页内容可以被读取
#3 PROT_WRITE 页可以被写入
#4 PROT_NONE 页不可访问
flags: 指定映射对象的类型,映射选项和映射页是否可以共享
MAX_FIXED: 如果参数start所指的地址无法成功建立映射时,则放弃映射
MAP_SHARED: 与其他映射这个文件的进程共享映射内存,可能存在并发修改
MAP_PRIVATE: 对映射区域的写入操作会产生一个映射文件的复制,类似于写时复制,对此区域作的任何修改都不会写回原来的文件内容。
fd: 文件描述符
offset: 文件映射的偏移量,已经映射了多少

2.1.2 mmap+write工作流程

第一:调用mmap函数将文件和进程虚拟地址空间映射
第二:将磁盘数据读取到页高速缓存
第三:调用write函数将页高速缓存数据直接写入套接字缓冲区
第四:将套接字缓冲区的数据写入网卡

2.1.3 mmap+write 数据传输流程

在这里插入图片描述

#1 用户进程调用mmap函数,向内核发起调用,CPU从用户态切换到内核态
#2 建立文件物理地址和虚拟内存映射区域的映射,或者说是内核缓冲区(页高速缓存)和虚拟内存映射区域的映射
#3 CPU向磁盘DMA控制器发送读取指定位置和大小的指令,DMA控制器将数据从磁盘拷贝到内核缓冲区
#4 mmap系统调用结束返回,CPU从内核态切换到用户态
#5 用户进程调用write函数,向内核发起调用,CPU从用户态切换到内核态
#6 CPU将页高速缓存中的数据拷贝到套接字缓冲区
#7 CPU向磁盘DMA控制器发送DMA写指令,DMA控制器从套接字缓冲区调用协议栈处理,最后把数据拷贝到网卡
#8 write系统调用结束返回,CPU从内核态切换到用户态

总结:
总共发生3次数据拷贝,4次上下文切换,所以mmap+write的效率比传统
read/write效率高一些。

2.2 sendfile

基于mmap方式实现的零拷贝已经比以前的传统I/O流程少了一次CPU拷贝,性能有所优化,但是还不是最优。Linux后面又引入了一个新的系统调用sendfile。
它实现了mmap+write的功能,简化了用户接口,即用户只需要一次调用,就可以实现mmap+write的功能。

2.2.1 sendfile函数

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

out_fd: 写入的文件描述符
in_fd: 写入文件描述符
offset: 从哪个位置开始读取
count: 读取多少数据

2.2.2 sendfile工作原理

第一:调用sendfile函数
第二:从磁盘读取数据,拷贝到内核缓冲区
第三:CPU将内核缓冲区数据拷贝到套接字缓冲区
第四:套接字缓冲区数据拷贝到网卡

2.2.3 sendfile数据传输流程

在这里插入图片描述

#1 用户进程调用sendfile函数,向内核发起调用,CPU从用户态切换内核态
#2 CPU向磁盘DMA控制器发送读取数据的指令,DMA控制器读取磁盘数据,拷贝到页高速缓存
#3 然后CPU将页高速缓存的数据拷贝到套接字缓冲区
#4 CPU向网卡DMA引擎发送读取指令,从套接字缓冲区调用协议栈处理,然后数据拷贝到网卡
#5 sendfile调用结束,CPU从内核态切换到用户态

总结:
总共发生3次数据拷贝,2次上下文切换,所以sendfile的效率比mmap+write方式
还高一些

2.3 sendfile + DMA scatter / gather copy

sendfile设计的时候并不是针对处理大文件的,如果需要处理大文件的话,需要调用另外一个接口sendfile64()。它的核心思想就是基于DMA scatter / gather 来实现的。

2.3.1 什么是block DMA?什么是SG-DMA?

2.3.1.1 什么是block DMA

block DMA就是要求传输数据块的源物理地址和目标物理地址都是连续的,每次只能传输一个数据块,传输完成后,中断机构触发中断。

2.3.1.2 什么是SG-DMA?

第一:SG-DMA是scatter / gather的缩写,scatter可以将一个源位置连续的数据块传输到目的地离散的存储;gather可以将源位置分散的数据块传输到目的地连续的存储。
第二:SG-DMA会预先维护一个物理上不连续的块描述符的链表,描述符中包含有数据的起始地址和长度。传输时只需要遍历链表,按序传输数据,全部完成后发起一次中断即可,效率比Block DMA要高。
在这里插入图片描述

2.3.2 sendfile函数

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

out_fd: 写入的文件描述符
in_fd: 写入文件描述符
offset: 从哪个位置开始读取
count: 读取多少数据

2.3.3 sendfile+DMA scatter/gather工作原理

第一:调用sendfile函数
第二:读取磁盘数据,通过scatter方式写入到页高速缓存,并且维护一个块描述符链表
第三:将描述符元数据信息拷贝到套接字缓冲区;并且只是网卡读取数据,网卡通过gather方式拷贝数据

2.3.4 sendfile + DMA scatter / gather copy 数据传输流程

在这里插入图片描述

#1 用户进程调用sendfile或者sendfile64()函数,向内核发起调用,CPU从用户态切换到内核态
#2 CPU向磁盘控制器发送读取数据指令,DMA控制器读取磁盘数据,以scatter方式将数据离散的存储在也缓冲区(也就是说可以不是连续的),然后维护一个块链表,方便在页高速缓存中查找
#3 CPU将描述符信息(数据大小和内存地址等元数据信息),以CPU方式拷贝到套接字缓冲区
#4 CPU通知网卡DMA控制器套接字缓冲区获取元数据信息,然后根据元数据信息,从页高速缓存以DMA gather聚合方式将离散的数据拷贝数据到网卡
#5 sendfile64系统调用结束,CPU从内核态切换到用户态

总结:
总共发生2次数据拷贝,1次元数据拷贝,2次上下文切换,所以sendfile64的效率比sendfile这种方式效果好些,尤其是处理大文件。真正来说1次元数据拷贝可以忽略不计。所以可以认为只有2次DMA数据拷贝和2次CPU的内核态和用户态的切换。
缺点:第一,需要硬件DMA支持scatter/gather功能;第二:只是支持输出的文件描述符不受限制,但是输入的文件描述符类型依然只能是文件。比如你可以文件到文件,也可以文件到网卡,但是不能网卡到文件。这就是FileChannel中transferFrom只能是读取文件,但是transferTo的目标可以是网卡也可以是文件的的根本原。

2.4 splice

我们知道,sendfile + DMA scatter / gather方式虽然可以只发生2次数据拷贝,2次CPU的用户态和内核态的切换,但是说到底,它是需要DMA硬件的支持,如果DMA不支持scatter / gather,那么还是得使用sendfile方式。至少FileChannel中transferTo是这么实现的。另外一个就是输入文件描述符类型的限制。所以后来出现了splice, 他可以完全通过软件实现。

2.4.1 splice函数

splice(int fd_in, loff_t *off_in, int fd_out,loff_t *off_out,size_t len, unsigned int flags );

fd_in: 输入文件描述符
off_in: 输入文件的offset偏移量,指示内核从哪里读取数据
fd_out: 输出文件描述符
off_out: 输出文件描述符,指示内核数据输出到什么位置
len: 指示了此次调用希望传输的字节数

2.4.2 splice工作原理

第一:调用pipe()函数创建管道
第二:调用splice()函数,读取数据,写入管道
第三:调用splice()函数,从管道读取数据,写入到目标

2.4.3 splice的数据传输流程

在这里插入图片描述

#1 用户进程调用pipe函数,向内核发起调用,CPU从用户态切换到内核态
#2 创建单向管道
#3 pipe函数调用结束返回,CPU从内核态切换到用户态
#4 用户进程调用splice函数,向内核发起调用,CPU从用户态切换到内核态
#5 CPU向磁盘DMA控制器发送读指令,DMA读取数据后,拷贝到页高速缓存;并且将页高速缓存数据写入管道中
#6 splice调用结束,CPU从内核态切换到用户态
#7 用户进程调用splice函数,向内核发起调用,CPU从用户态切换到内核态
#8 CPU从管道中消费数据,然后写入到套接字缓冲区,然后向网卡的DMA控制器发送写指令,DMA控制器将经过协议栈处理的套接字缓冲区的数据拷贝到网卡
#9 splice调用结束,CPU从内核态切换到用户态

三 Linux零拷贝方案比较

在这里插入图片描述

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莫言静好、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值