参考:零拷贝技术视频
参考:零拷贝技术文章
一、传统IO执行流程
1.用户应用进程调用read函数,向操作系统发起IO调用,上下文从用户态转为内核态(切换1)
2.DMA控制器发起I/O请求,把数据从磁盘中,拷贝到内核缓冲区,DMA发出数据读完信号。
3.CPU把内核缓冲区数据,拷贝到用户应用缓冲区
4.read()调用结束数据返回,内核态切换回用户态
5.用户应用进程通过write函数,发起IO调用,上下文从用户态转为内核态
6.CPU把用户缓冲区数据,拷贝到socket缓冲区
7.DMA发出IO请求,把数据从socket缓冲区,拷贝到网卡设备,DMA发出数据写完信号
8.write()调用结束数据返回,内核态切换为用户态
从流程图可以看出,传统IO的读写流程,包括了4次上下文切换(4次用户态和内核态的切换),4次数据拷贝(两次CPU拷贝以及两次的DMA拷贝)
二、mmap+write实现零拷贝
原理:将内核读缓冲区(PageCache)和用户空间的缓冲区域进行映射,是所有的IO都在内核完成
1.用户进程通过mmap方法向操作系统内核发起IO调用,上下文从用户态切换为内核态。
2.CPU利用DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
3.mmap调用返回,上下文从内核态切换回用户态
4.用户进程通过write方法向操作系统内核发起IO调用,上下文从用户态切换为内核态
5.CPU将内核缓冲区的数据拷贝到的socket缓冲区。
6.CPU利用DMA控制器,把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write调用返回。
可以发现,mmap+write实现的零拷贝,I/O发生了4次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中,包括了2次DMA拷贝和1次CPU拷贝。
三、sendfile实现的零拷贝
sendfile是Linux2.1内核版本后引入的一个系统调用函数
sendfile表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作,因此可以使用它来实现零拷贝。
1.用户进程发起sendfile系统调用,上下文(切换1)从用户态转向内核态
2.DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
3.CPU将读缓冲区中数据拷贝到socket缓冲区
4.DMA控制器,异步把数据从socket缓冲区拷贝到网卡,
5.上下文(切换2)从内核态切换回用户态,sendfile调用返回。
可以发现,sendfile实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中,包括了2次DMA拷贝和1次CPU拷贝
四、代码演示
mmap+write演示代码:
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <iostream>
#include <sys/stat.h>
#include <cstring>
int main()
{
// 打开源文件
int src_fd = open("source.txt", O_RDONLY);
if (src_fd == -1)
{
std::cerr << "Error opening source file." << std::endl;
return 1;
}
// 获取源文件大小
struct stat sb;
if (fstat(src_fd, &sb) == -1)
{
std::cerr << "Error getting file size." << std::endl;
return 1;
}
size_t file_size = sb.st_size;
// 内存映射源文件
void *mapped_file = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, src_fd, 0);
if (mapped_file == MAP_FAILED)
{
std::cerr << "Error mapping file to memory." << std::endl;
return 1;
}
// 打开目标文件
int dest_fd = open("destination.txt", O_WRONLY | O_CREAT, 0644);
if (dest_fd == -1)
{
std::cerr << "Error opening destination file." << std::endl;
munmap(mapped_file, file_size); // 解除映射
return 1;
}
// 将映射的文件内容写入目标文件
ssize_t bytes_written = write(dest_fd, mapped_file, file_size);
if (bytes_written != file_size)
{
std::cerr << "Error writing to destination file." << std::endl;
munmap(mapped_file, file_size); // 解除映射
return 1;
}
// 清理
munmap(mapped_file, file_size); // 解除映射
close(src_fd);
close(dest_fd);
std::cout << "File copied successfully." << std::endl;
return 0;
}
sendfile演示代码:
#include <fcntl.h>
#include <sys/sendfile.h>
#include <unistd.h>
#include <iostream>
#include <sys/stat.h>
int main()
{
// 打开源文件
int src_fd = open("source.txt", O_RDONLY);
if (src_fd == -1)
{
std::cerr << "Error opening source file." << std::endl;
return 1;
}
// 打开目标文件
int dest_fd = open("destination.txt", O_WRONLY | O_CREAT, 0644);
if (dest_fd == -1)
{
std::cerr << "Error opening destination file." << std::endl;
close(src_fd);
return 1;
}
off_t offset = 0; // 从源文件的开始位置读取
size_t count = 0; // 不限制每次读取的字节数量,sendfile() 将自动决定
ssize_t transferred;
// 使用 sendfile() 进行文件传输
while ((transferred = sendfile(dest_fd, src_fd, &offset, count)) > 0)
{
std::cout << "Transferred: " << transferred << " bytes" << std::endl;
}
if (transferred == -1)
{
std::cerr << "Error during sendfile operation." << std::endl;
close(src_fd);
close(dest_fd);
return 1;
}
// 清理
close(src_fd);
close(dest_fd);
std::cout << "File copied successfully." << std::endl;
return 0;
}
二、splice+DMA copy实现零拷贝
1.用户进程发起splice系统调用,上下文(切换1)从用户态转向内核态
2.DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
3.将数据从PageCache写到环形管道中,再将数据从环形管道读到Socket缓冲区中
4.DMA控制器,异步把数据从socket缓冲区拷贝到网卡,
5.上下文(切换2)从内核态切换回用户态,splice调用返回。
可以发现,sendfile实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及2次DMA数据拷贝,无CPU拷贝