零拷贝之mmap和sendfile

本文深入探讨了零拷贝技术的两种实现方式:mmap+write和sendfile,详细解释了它们如何避免传统IO操作中的多次上下文切换和数据拷贝,显著提高数据传输效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

零拷贝:https://strikefreedom.top/linux-io-and-zero-copy

内存映射文件

  • 从硬盘上将文件读入内存,都要经过文件系统进行数据拷贝,并且数据拷贝操作是由文件系统和硬件驱动实现的,理论上来说,拷贝数据的效率是一 样的。但是通过内存映射方法访问硬盘上的文件,效率要比read和write系统调用高,这是为什么呢?原因是read()是系统调用,其中进行了数据拷贝,它首先将文件内容从硬盘拷贝到内核空间的一个缓冲区,如图中过程1,然后再将这些数据拷贝到用户空间,如图中过程2,在这个过程中,实际上完成了两次数据拷贝 。
    在这里插入图片描述
  • 而mmap()也是系统调用,如前所述,mmap()中没有进行数据拷贝,真正的数据拷贝是在缺页中断处理时进行的,由于mmap()将文件直接映射到用户空间,用户空间的应用程序缓冲区和内核空间的内核缓冲区映射到同一个物理地址,直接将文件从硬盘拷贝到用户空间,只进行了一次数据拷贝 。因此,内存映射的效率要比read/write效率高。反正大家只要记住一个结论:通过操作内存来操作文件,省了从磁盘文件拷贝数据到用户缓冲区的步骤,所以十分高效。通过内存映射的方式,使用户缓冲区和内核读缓冲区的内存地址为同一内存地址,也就是说,不需要CPU再将数据从内核读缓冲区复制到用户缓冲区。
  • 发出mmap系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。
  • mmap系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。接着用户空间和内核空间共享这个缓冲区,而不需要将数据从内核空间拷贝到用户空间。因为用户空间和内核空间共享了这个缓冲区数据,所以用户空间就可以像在操作自己缓冲区中数据一般操作这个由内核空间共享的缓冲区数据。
    void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
  • mmap的作用是映射文件描述符fd指定文件的 [off,off + len]区域至调用进程的[addr, addr + len]的内存区域, 返回值:若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。使用方法如下:
    1.用open系统调用打开文件, 并返回描述符fd.
    2.用mmap建立内存映射, 并返回映射首地址指针start.
    3.对映射(文件)进行各种操作, 显示(printf), 修改(sprintf).
    4.用munmap(void *start, size_t lenght)关闭内存映射.
    5.用close系统调用关闭文件fd

参数flags:影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
MAP_SHARED对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。
MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)。


sendfile

  • 我们可以看出传统的IO读写操作,总共进行了4次上下文切换,4次Copy动作。我们可以看到数据在内核空间和应用空间之间来回复制,其实他们什么都没有做,就是复制而已,这个机制太浪费时间了,而且是浪费的CPU的时间。那我们能不能让数据不要来回复制呢?零拷贝这个技术就是来解决这个问题。关于零拷贝提供了两种解决方式:mmap+write方式、sendfile方式。
  • sendfile函数在两个文件描述符之间传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,被称为零拷贝。函数定义为:
`#include<sys/sendfile.h>ssize_t senfile(int out_fd,int in_fd,off_t* offset,size_t count);`
in_fd参数是待读出内容的文件描述符,
out_fd参数是待写入内容的文件描述符,
offset参数指定从读入文件流的哪个位置开始读,如果为空,则使用读入文件流默认的起始位置。
count参数指定文件描述符in_fd和out_fd之间传输的字节数。
in_fd必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket和管道,
out_fd必须是一个socket.
  • 首先我们来看看传统的read/write方式进行socket的传输。 当需要对一个文件进行传输的时候,具体流程细节如下:
    1:调用read函数,文件数据copy到内核缓冲区
    2:read函数返回,文件数据从内核缓冲区copy到用户缓冲区
    3:write函数调用,将文件数据从用户缓冲区copy到内核与socket相关的缓冲区
    4:数据从socket缓冲区copy到相关协议引擎。
    在这个过程中发生了四次copy操作。
    硬盘->内核->用户->socket缓冲区(内核)->协议引擎

    在这里插入图片描述
  • sendfile原理:
  • 1、系统调用 sendfile() 通过 DMA 把硬盘数据拷贝到 kernel buffer,然后数据被 kernel 直接拷贝到另外一个与 socket 相关的 kernel buffer。这里没有 用户态和核心态 之间的切换,在内核中直接完成了从一个 buffer 到另一个 buffer 的拷贝。
    2、DMA 把数据从 kernel buffer 直接拷贝给协议栈,没有切换,也不需要数据从用户态和核心态,因为数据就在 kernel 里。

    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值