Linux下零拷贝技术
- 为什么提出零拷贝
- sendfile函数实现的零拷贝
- mmap函数实现的零拷贝和munmap函数
- splice函数实现的零拷贝
为什么提出零拷贝 通常我们会有这样的需求:将本地磁盘上的一个文件通过网络发送给远端的另一个服务。在传统的I/O中,会经过下面几个步骤:
- 发出read()系统调用,这时处理器会从用户空间切换至内核空间;
- 向磁盘请求数据;
- 通过DMA将文件从磁盘上读取到内核空间缓冲区;
- read()系统调用返回,将数据从内核空间缓冲区拷贝至用户空间缓冲区,这时候处理器会从内核空间切换至用户空间;
- 发出write()系统调用,并将数据从用户空间缓冲区拷贝至目标socket在内核空间的缓冲区;这时候处理器会从用户空间切换至内核空间;
- write()调用返回;
- 通过DMA将数据从内核空间缓冲区中拷贝至协议引擎(该操作是独立且异步的)。
零拷贝的实现:
Linux中提供类似的系统调用主要有sendfile()、mmap()和splice()。
sendfile函数实现的零拷贝 sendfile函数在两个文件描述符之间直接传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高。
sendfile函数的定义如下:
#include
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
【Linux|Linux下零拷贝技术】in_fd参数是待读出内容的文件描述符,out_fd 参数是待写人内容的文件描述符。offset参数指定从读入文件流的哪个位置开始读,如果为空,则使用读人文件流默认的起始位置。count参数指定在文件描述符in_fd 和out_fd 之间传输的字节数。sendfile 成功时返回传输的字节数,失败则返回-1并设置errmo。该函数的man手册明确指出,in_fd 必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket和管道;而out_fd则必须是一个socket。由此可见,sendfle 几乎是专门为在网络上传输文件而设计的。
sendfile的实现过程:
- 发出sendfile()系统调用,这是处理器会从用户空间切换至内核空间;
- 通过DMA将目标文件从磁盘上读取到内核空间缓冲区;
- 将数据从内核缓冲区拷贝到目标socket缓冲区;
- sendfile()返回,这是处理器从内核空间切换至用户空间;
- 通过DMA将数据从目标socket缓冲区拷贝至协议引擎。
该实现虽然减少了2次上下文切换,但仍然还有1次CPU拷贝。那这次拷贝是不是也可以省掉呢?答案是肯定的。但是需要底层操作系统的一些支持。那就是带有DMA收集功能的sendfile实现的零拷贝。
有DMA收集功能的sendfile实现的零拷贝
操作系统底层提供了带有scatter/gather的DMA来从内核空间缓冲区中将数据读取到协议引擎中。这就
意味着等待传输的数据不需要在连续存储器中,它可以分散在不同的内存位置。那这样一来,从文件中
读出的数据就不必拷贝至目标socket的缓冲区中,只需要将缓冲区描述符添加到目标socket的缓冲区
中,DMA收集操作会根据缓冲区描述符中的信息将内核空间缓冲区中的数据读取到协议引擎。这种方法不仅减少了上下文切换、还减少了由CPU参与的数据拷贝。
- 发出sendfile()系统调用,处理器从用户空间切换至内核空间;
- 通过DMA将目标数据copy至内核空间缓冲区;
- 将数据在内核空间缓冲区的地址和偏移量拷贝至目标socket的缓冲区;
- sendfile()返回,处理器从内核空间切换至用户空间。
- 带有scatter/gather 功能的DMA将数据直接从内核缓冲区读取到协议引擎,从而消除了CPU拷贝。
但如果先把数据从磁盘读出来后编辑一遍再发送出去,以上说的两种sendfile零拷贝则不能实现,这正是sendfile的缺点所在。
mmap函数实现的零拷贝和munmap函数 针对sendfile的缺点,Linux内核为我们提供了mmap方法。
mmap(内存映射):mmap操作提供了一种机制,让用户程序直接访问设备内存,这种机制,相比较在用户空间和内核空间互相拷贝数据,效率更高。
mmap函数用于申请一段内存空间。我们可以将这段内存作为进程间通信的共享内存,也可以将文件直接映射到其中。munmap函数则释放由mmap创建的这段内存空间。它们的定义如下:
#include
void* mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *start, size_t length);
start参数允许用户使用某个特定的地址作为这段内存的起始地址。如果它被设置成NULL,则系统自动分配一个地址。
length 参数指定内存段的长度。
prot参数用来设置内存段的访问权限。它可以取以下几个值的按位或:
- PROT_READ,内存段可读;
- PROT_WRITE,内存段可写;
- PROT_EXEC,内存段可执行;
- PROT_NONE,内存段不能被访问。
常用值 | 含义 |
---|---|
MAP_SHARED | 在进程间共享这段内存。对该内存段的修改将反映到被映射的文件中。它提供了进程间共享内存的POSIX方法 |
MAP_PRIVATE | 内存段为调用进程所私有。对该内存段的修改不会反映到被映射的文件中 |
MAP_ANONYMOUS | 这段内存不是从文件映射而来的。其内容被初始化为全0。这种情况下,mmap函数的最后两个参数将被忽略 |
MAP_FIXED | 内存段必须位于start参数指定的地址处。start 必须是内存页面大小(4096字节)的整数倍 |
MAP_HUGETLB | 按照“大内存页面"来分配内存空间。“大内存页面”的大小可通过/proc/meminfo文件来查看 |
mmap函数成功时返回指向目标内存区域的指针,失败则返回MAP_FAILED ((void*)-1)并设置errno。
munmap 函数成功时返回0,失败则返回-1并设置errno。
mmap实现零拷贝的过程:
- 发出mmap()系统调用,处理器从用户空间切换至内核空间。
- 通过DMA将目标数据从磁盘拷贝至内核空间缓冲区;
- mmap()调用返回,这时候用户程序和操作系统共享这个缓冲区,不需要再将数据从内核缓冲区
拷贝至用户缓冲区,处理器从内核空间切换至用户空间; - 用户逻辑处理;
- 发出write()系统调用,将数据从内核空间缓冲区拷贝至目标socket缓冲区,这时处理器从用户空间
切换至内核空间; - write()调用返回,处理器从内核空间切换至用户空间;
- 通过DMA将数据拷贝至协议引擎。
splice函数用于在两个文件描述符之间移动数据,也是零拷贝操作。splice 函数的定义如下:
#include
ssize_t splice(int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flage);
fd_in参数是待输人数据的文件描述符。如果fd_in 是一个管道文件描述符,那么off_in参数必须被设置为NULL。如果fd_in 不是一个管道文件描述符(比如socket),那么off_in表示从输人数据流的何处开始读取数据。此时,若off_in被设置为NULL,则表示从输人数据流的当前偏移位置读入;若off_in 不为NULL,则它将指出具体的偏移位置。
fd_out/off_out参数的含义与fd_in/off_in 相同,不过用于输出数据流。
len 参数指定移动数据的长度;
flags参数则控制数据如何移动,它可以被设置为下表中的某些值的按位或。
常用值 | 含义 |
---|---|
SPLICE_F_MOVE | 如果合适的话,按整页内存移动数据。这只是给内核的一个提示。不过,因为它的实现存在BUG,所以自内核2.6.21后,它实际上没有任何效果 |
SPLICE_F_NONBLOCK | 非阻塞的splice操作,但实际效果还会受文件描述符本身的阻塞状态的影响 |
SPLICE_F_MORE | 给内核的一个提示:后续的splice调用将读取更多数据 |
SPLICE_F_GIFT | 对splice没有效果 |
错误 | 含义 |
---|---|
EBADF | 参数所指文件描述符有错 |
EINVAL | 目标文件系统不支持splice,或者目标文件以追加方式打开,或者两个文件描述符都不是管道文件描述符,或者某个offset参数被用于不支持随机访问的设备( 比如字符设备) |
ENOMEM | 内存不够 |
ESPIPE | 参数fd_in (或fd_out)是管道文件描述符,而off _in (或of _out) 不为NULL |
推荐阅读
- 【正点原子Linux连载】第三章深入探究文件I/O-摘自【正点原子】I.MX6U嵌入式Linux C应用编程指南V1.1
- web开发学习|HTML期末大作业放在了阿里云服务器上
- rockchip|Rockchip Linux SDK软件包的解压
- #|i.MX6ULL终结者QT应用开发一键烧写QT程序到开发板
- linux|Linux Shell编程
- Vulnhub|vulnhub之darkhole_2
- go语言的学习|go语言常用命令和包管理
- go语言|Go mod 使用私有git仓库依赖包
- linux|zabbix server 6.0安装(rancher+kubernetes部署)