导言

在计算机系统的文件处理领域,数据传输效率是衡量程序性能的重要指标。传统文件复制操作依赖于read和write系统调用,这种方式在用户空间与内核空间之间存在多次数据拷贝过程,导致不可忽视的性能损耗。而基于内存映射(mmap)技术的文件复制方案,通过将文件内容直接映射至进程地址空间,有效减少数据拷贝次数,从而显著提升数据处理效率。本文将对一段基于内存映射的 C 语言文件复制代码进行深入分析,详细阐述其实现机制与设计原理。

一、头文件与宏定义

1
2
3
4
5
6
7
8
9
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#define ARGS_CHECK(argc, num) if (argc != num) { fprintf(stderr, "Usage: %s <source_file> <destination_file>\n", argv[0]); exit(EXIT_FAILURE); }
#define ERROR_CHECK(expr, val, msg) if ((expr) == (val)) { perror(msg); exit(EXIT_FAILURE); }

1.1 头文件引入

  • <sys/mman.h>:该头文件包含内存映射相关函数(如mmap、munmap、msync)的函数声明,是实现内存映射文件操作的核心头文件,为后续内存映射相关操作提供函数接口支持。

  • <sys/types.h>:此头文件定义了基本的系统数据类型,如pid_t、size_t等。这些数据类型的标准化定义,确保了代码在不同系统环境下数据类型的一致性与兼容性,为程序的正确执行奠定基础。

  • <fcntl.h>:包含了文件控制相关的宏定义,例如O_RDONLY、O_RDWR等。这些宏用于精确指定文件的打开模式,使开发者能够根据实际需求灵活控制文件的读写权限与打开方式。

  • <unistd.h>:提供了众多 UNIX 系统服务的函数原型,涵盖文件读写(read、write)、文件关闭(close)等基础文件操作功能。这些函数是进行文件 I/O 操作的基础,为程序实现文件处理功能提供必要支持。

  • <sys/stat.h>:主要用于获取文件的状态信息,包括文件大小、权限等关键属性。通过stat系列函数(stat、fstat),程序能够读取文件的详细元数据,这些信息在后续文件处理流程中具有重要作用。

  • <stdio.h>:作为标准输入输出头文件,提供了printf、fprintf等输入输出函数。这些函数用于程序运行过程中的信息输出与错误提示,方便开发者调试程序以及向用户反馈程序运行状态。

  • <stdlib.h>:包含内存分配(malloc、free)、程序终止(exit)等函数,以及一些通用工具函数。这些函数为程序的内存管理和流程控制提供了基本功能,是 C 语言程序开发不可或缺的部分。

1.2 宏定义

  • ARGS_CHECK:该宏用于实现命令行参数数量的有效性检查。其逻辑为:当命令行参数个数argc与指定的参数个数num不相等时,通过fprintf函数向标准错误输出流打印程序的正确使用说明,并调用exit(EXIT_FAILURE)终止程序执行。在本程序场景中,要求用户必须传入源文件和目标文件两个参数(加上程序名自身,共计 3 个参数),该宏能够有效避免因参数缺失导致的程序运行错误。

  • ERROR_CHECK:此宏旨在简化系统调用的错误检查流程。当表达式expr的计算结果与指定的错误值val相等时,调用perror函数输出与错误码对应的系统错误信息,并终止程序。通过将重复的错误检查逻辑封装为宏定义,显著提高了代码的简洁性与可读性,降低了开发者的编码负担。

二、参数检查与文件打开

1
2
3
4
5
6
int main(int argc, char *argv[]) {
// 参数检查
ARGS_CHECK(argc, 3);
// 打开源文件
int src = open(argv[1], O_RDONLY);
ERROR_CHECK(src, -1, "open_read");

2.1 参数检查

在 C 语言程序中,main函数的argc参数记录了命令行参数的总数,argv则是一个字符指针数组,存储着每个命令行参数的字符串内容。ARGS_CHECK(argc, 3)语句用于严格校验用户输入的命令行参数数量,确保用户在运行程序时准确传入源文件和目标文件两个参数(包含程序名在内共 3 个参数)。一旦参数数量不符合要求,程序将立即终止,并向用户提示正确的使用方式,从而有效规避后续因参数缺失引发的操作失败风险。

2.2 打开源文件

利用open函数以只读模式(O_RDONLY)打开源文件,函数执行成功后返回一个文件描述符src,后续对源文件的所有操作均通过该文件描述符进行。ERROR_CHECK(src, -1, "open_read")用于检查open函数的返回值,若返回值为-1,表明源文件打开操作失败。此时,perror函数将根据实际错误码输出相应的错误信息(例如 “open_read: No such file or directory”),并终止程序执行,防止程序继续进行无效操作。

三、获取文件信息与创建目标文件

1
2
3
4
5
6
7
8
// 获取文件状态
struct stat st;
ERROR_CHECK(fstat(src, &st), -1, "fstat");
off_t file_size = st.st_size;
// 创建目标文件并设置大小
int dest = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0775);
ERROR_CHECK(dest, -1, "open_write");
ERROR_CHECK(ftruncate(dest, file_size), -1, "ftruncate_dest");

3.1 获取文件状态

通过调用fstat函数获取源文件的详细状态信息,并将结果存储于struct stat类型的变量st中。fstat函数以源文件的文件描述符src作为输入参数,若函数执行成功,将把文件的各类属性(如文件大小st.st_size、文件类型、权限等)填充至st结构体。ERROR_CHECK(fstat(src, &st), -1, "fstat")用于检测fstat操作是否成功,若失败则输出错误信息并终止程序。获取到的文件大小st.st_size在后续流程中至关重要,将用于确定目标文件的大小以及内存映射区域的大小。

3.2 创建目标文件并设置大小

  • 使用open函数创建目标文件,指定打开模式为O_RDWR | O_CREAT | O_TRUNC:

    • O_RDWR:以读写模式打开文件,允许程序对目标文件进行读取和写入操作。

    • O_CREAT:若目标文件不存在,则自动创建新文件。

    • O_TRUNC:若目标文件已存在,将其长度截断为 0,即清空原有文件内容。

    • 文件权限设置为0775,表示文件所有者和所属组具备读、写、执行权限,其他用户具有读、写权限。ERROR_CHECK(dest, -1, "open_write")用于检查目标文件的打开操作是否成功。

  • 调用ftruncate函数将目标文件的大小调整为与源文件一致(即file_size)。这一步骤对于确保目标文件拥有足够空间存储源文件内容至关重要。ERROR_CHECK(ftruncate(dest, file_size), -1, "ftruncate_dest")用于检查ftruncate操作是否成功执行。

四、内存映射与数据复制

1
2
3
4
5
6
7
8
// 创建内存映射
char *src_map = mmap(NULL, file_size, PROT_READ, MAP_SHARED, src, 0);
ERROR_CHECK(src_map, MAP_FAILED, "mmap_src");

char *dest_map = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, dest, 0);
ERROR_CHECK(dest_map, MAP_FAILED, "mmap_dest");
// 复制数据
memcpy(dest_map, src_map, file_size);

4.1 创建内存映射

  • 对源文件进行内存映射:mmap(NULL, file_size, PROT_READ, MAP_SHARED, src, 0)将源文件映射至进程地址空间。

    • NULL:指示系统自动选择映射区域的起始地址。

    • file_size:指定映射区域的大小,即源文件的字节数。

    • PROT_READ:设置映射区域的保护权限为只读,确保源文件内容在映射期间不可被修改。

    • MAP_SHARED:该标志表示映射区域对其他映射同一文件的进程可见,并且对映射区域的修改会同步反映到磁盘文件中。

    • src:源文件的文件描述符。

    • 0:表示从源文件的起始位置开始映射。若映射成功,mmap函数返回映射区域的起始地址,存储于src_map指针中;若映射失败,则返回MAP_FAILED,此时ERROR_CHECK(src_map, MAP_FAILED, "mmap_src")将触发错误处理机制。

  • 对目标文件进行内存映射:mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, dest, 0),与源文件映射过程类似,但保护权限设置为可读可写(PROT_READ | PROT_WRITE),以便后续将源文件内容写入目标文件。映射成功后,返回的地址存储在dest_map指针中,同样通过ERROR_CHECK宏检查映射操作是否成功。

4.2 复制数据

借助memcpy函数,将源文件映射区域(src_map)的内容直接复制到目标文件映射区域(dest_map),复制的数据长度为file_size字节。由于内存映射技术将文件内容映射至内存,memcpy函数能够在内存中直接进行数据操作,避免了传统 I/O 操作中用户空间与内核空间之间的数据拷贝过程,从而大幅提升数据传输效率。

五、同步与资源释放

1
2
3
4
5
6
7
8
9
    // 同步内存更改到磁盘
ERROR_CHECK(msync(dest_map, file_size, MS_SYNC), -1, "msync");
// 解除映射并关闭文件
ERROR_CHECK(munmap(src_map, file_size), -1, "munmap_src");
ERROR_CHECK(munmap(dest_map, file_size), -1, "munmap_dest");
ERROR_CHECK(close(src), -1, "close_src");
ERROR_CHECK(close(dest), -1, "close_dest");
return 0;
}

5.1 同步内存更改到磁盘

调用msync函数,将目标文件映射区域(dest_map)中已修改的数据同步至磁盘上的目标文件。MS_SYNC标志指定函数等待所有写入操作完成,以确保数据被持久化存储到磁盘。ERROR_CHECK(msync(dest_map, file_size, MS_SYNC), -1, "msync")用于检查同步操作是否成功,若失败则输出错误信息并终止程序,防止数据丢失。

5.2 解除映射并关闭文件

  • 使用munmap函数分别解除对源文件和目标文件的内存映射。munmap(src_map, file_size)和munmap(dest_map, file_size)操作将释放之前通过mmap函数分配的内存映射区域,ERROR_CHECK宏确保解除映射操作成功执行,避免内存泄漏问题。

  • 调用close函数关闭源文件和目标文件对应的文件描述符src和dest,释放系统资源。ERROR_CHECK(close(src), -1, "close_src")和ERROR_CHECK(close(dest), -1, "close_dest")用于检查文件关闭操作是否成功,确保程序正确释放所有占用的系统资源。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // 包含memcpy函数的声明

#define ARGS_CHECK(argc, num) if (argc != num) { fprintf(stderr, "Usage: %s <source_file> <destination_file>\n", argv[0]); exit(EXIT_FAILURE); }
#define ERROR_CHECK(expr, val, msg) if ((expr) == (val)) { perror(msg); exit(EXIT_FAILURE); }

int main(int argc, char *argv[]) {
// 检查输入参数数量
ARGS_CHECK(argc, 3);

// 打开源文件,只读模式
int src = open(argv[1], O_RDONLY);
ERROR_CHECK(src, -1, "open_read");

// 获取源文件大小
off_t file_size;
struct stat st;
ERROR_CHECK(fstat(src, &st), -1, "fstat");
file_size = st.st_size;

// 创建目标文件,权限为0775
int dest = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0775);
ERROR_CHECK(dest, -1, "open_write");

// 设置目标文件大小与源文件相同
int ret_d = ftruncate(dest, file_size);
ERROR_CHECK(ret_d, -1, "ftruncate_dest");

// 使用mmap映射两个文件
char *src_r = (char *) mmap(NULL, file_size, PROT_READ, MAP_SHARED, src, 0);
ERROR_CHECK(src_r, MAP_FAILED, "mmap_s");

char *dest_w = (char *) mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, dest, 0);
ERROR_CHECK(dest_w, MAP_FAILED, "mmap_d");

// 复制文件内容
memcpy(dest_w, src_r, file_size);

// 解除内存映射
munmap(src_r, file_size);
munmap(dest_w, file_size);

// 关闭文件描述符
close(src);
close(dest);

return 0;
}