Skip to content

C++ 零拷贝使用

实现文件的拷贝

传统IO

c
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <iostream>

void file_copy_read_write(const char *src_file, const char *dest_file)
{
    int src_fd = open(src_file, O_RDONLY);
    if (src_fd == -1)
    {
        perror("open src file");
        return;
    }

    // 获取源文件大小
    struct stat sb;
    if (fstat(src_fd, &sb) == -1)
    {
        perror("fstat");
        close(src_fd);
        return;
    }

    // 打开目标文件(无需预先分配空间)
    int dest_fd = open(dest_file, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (dest_fd == -1)
    {
        perror("open dest file");
        close(src_fd);
        return;
    }

    // 缓冲区(建议大小与文件系统块对齐,通常4KB-64KB)
    const size_t BUF_SIZE = 4096;
    char buf[BUF_SIZE];

    ssize_t total_written = 0;
    while (total_written < sb.st_size)
    {
        // 读取数据到缓冲区
        ssize_t n_read = read(src_fd, buf, BUF_SIZE);
        if (n_read == -1)
        {
            perror("read");
            break;
        }

        // 写入目标文件
        ssize_t n_written = write(dest_fd, buf, n_read);
        if (n_written != n_read)
        {
            perror("write");
            break;
        }
        total_written += n_written;
    }

    // 确保数据落盘
    fsync(dest_fd);

    // 关闭文件
    close(src_fd);
    close(dest_fd);
}

int main()
{
    file_copy_read_write("/zwh/C++/Test/file/init.txt", "/zwh/C++/Test/file/out.txt");
    return 0;
}

mmap

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

    • 映射的起始地址(通常传 NULL,由内核自动选择)

    • 映射的字节大小(通常为文件大小)

    • 内存保护模式(PROT_READ/PROT_WRITE/PROT_EXEC/PROT_NONE

      • PROT_READ:只读映射(适用于只读文件)。
      • PROT_READ | PROT_WRITE:可读可写(适用于读写文件或共享内存)。
      • PROT_READ | PROT_EXEC:可读可执行(适用于加载动态库)。
    • 文件描述符

      • MAP_SHARED: 共享映射,修改会同步到文件,其他进程可见(适用于进程间通信 IPC),多个进程共享同一文件(如数据库、日志文件)

      • MAP_PRIVATE: 私有映射,修改不会写入文件(写时复制,进程独享),进程需要独立修改文件内容的场景

      • MAP_ANONYMOUS:匿名映射,不用关联文件

        • 可代替 malloc 分配大块内存(如 glibcmalloc 对大内存使用 mmap)。
        • 进程间共享内存(需配合 MAP_SHARED
        c
        // 分配 1MB 匿名共享内存
        void *addr = mmap(NULL, 1024*1024, PROT_READ | PROT_WRITE, 
          MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    • 文件偏移量(通常为 0,表示从文件开头映射)

  • ftruncate:为目标文件预先分配磁盘空间

    • mmap 要求目标文件的大小必须足够,否则映射会失败(例如,如果目标文件是空文件,mmap 会返回 MAP_FAILED
    • ftruncate(fd, size) 的作用是:
      • 如果文件 小于 size,则 扩展文件 并填充 \0(空洞文件,实际不占用磁盘块)。
      • 如果文件 大于 size,则 截断文件
c
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <iostream>

void mmap_write_example(const char *src_file, const char *dest_file)
{
    int src_fd = open(src_file, O_RDONLY);
    if (src_fd == -1)
    {
        perror("open src file");
        return;
    }

    // 获取文件大小
    struct stat sb;
    if (fstat(src_fd, &sb) == -1)
    {
        perror("fstat");
        close(src_fd);
        return;
    }

    // 映射文件到内存
    void *src_map = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, src_fd, 0);
    if (src_map == MAP_FAILED)
    {
        perror("mmap");
        close(src_fd);
        return;
    }

    // 打开目标文件
    int dest_fd = open(dest_file, O_RDWR | O_CREAT | O_TRUNC, 0666);
    if (dest_fd == -1)
    {
        perror("open dest file");
        munmap(src_map, sb.st_size);
        close(src_fd);
        return;
    }

    // 调整目标文件大小,为目标文件预先分配磁盘空间大小是sb.st_size
    if (ftruncate(dest_fd, sb.st_size) == -1)
    {
        perror("ftruncate");
        close(dest_fd);
        munmap(src_map, sb.st_size);
        close(src_fd);
        return;
    }

    // 写入目标文件
    ssize_t n = write(dest_fd, src_map, sb.st_size);
    if (n != sb.st_size)
    {
        perror("write");
    }

    // 清理资源
    close(dest_fd);
    munmap(src_map, sb.st_size);
    close(src_fd);
}

int main()
{
    mmap_write_example("/zwh/C++/Test/file/init.txt", "/zwh/C++/Test/file/out.txt");
    return 0;
}

sendfile

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

  • 目标文件描述符
  • 源文件描述符
  • 源文件的起始偏移量为NULL则从文件当前开始
  • 要传输的字节数
  • 返回实际传输的字节数(可能小于count
c
#include <fcntl.h>
#include <unistd.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <iostream>

void file_copy_sendfile(const char *src_file, const char *dest_file) {
    int src_fd = open(src_file, O_RDONLY);
    if (src_fd == -1) {
        perror("open src file");
        return;
    }

    // 获取源文件大小
    struct stat sb;
    if (fstat(src_fd, &sb) == -1) {
        perror("fstat");
        close(src_fd);
        return;
    }

    // 打开目标文件(必须可写)
    int dest_fd = open(dest_file, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (dest_fd == -1) {
        perror("open dest file");
        close(src_fd);
        return;
    }

    // 使用sendfile拷贝数据
    ssize_t total_sent = 0;
    while (total_sent < sb.st_size) {
        ssize_t n_sent = sendfile(dest_fd, src_fd, NULL, sb.st_size - total_sent);
        if (n_sent == -1) {
            perror("sendfile");
            break;
        }
        total_sent += n_sent;
    }

    // 确保数据落盘
    fsync(dest_fd);

    // 关闭文件
    close(src_fd);
    close(dest_fd);
}

int main() {
    file_copy_sendfile("/zwh/C++/Test/file/init.txt", "/zwh/C++/Test/file/out.txt");
    return 0;
}

测试(仅供参考)

本地

1.5GB文件测试,瓶颈来自磁盘,因此时间基本一致,但是零拷贝的CPU时间和占有率大大降低

传统IO

image-20250504231701042

mmap

image-20250504232133184

sendfile

image-20250504232622849

网络

  • Socket,传统IOsendfile发送文件测试
  • 综合多次测试,从时间吞吐量CPU使用率系统调用次数上看,sendfile效果更好

image-20250504224509624