大文件复制:mmap()+write()效率、系统优化及与copy_file_range对比
我在开发中需要用C代码复制约10GB的大文件,由此产生以下疑问:
- 将已有文件映射到内存后调用
write()写入目标文件的mmap()+write()组合效率如何? - Windows、macOS、Linux、*BSD这类系统是否会对此场景进行特殊优化以提升文件复制性能?
- 该方式的速度能否媲美
copy_file_range(2)?Linux/*BSD的源码中是否有针对此场景的处理逻辑?
补充:我了解Linux下write(2)单次调用写入量不超过INT_MAX,因此项目中使用full_write()封装函数循环调用write(2)直至数据全部写入或调用失败。
以下是我项目中的示例代码(假设运行类Unix系统且所有系统调用均成功,input和output为常规文件):
int main() { struct stat sb; int fdsrc, fddst; void *p; fdsrc = open("input", O_RDONLY); fddst = open("output", O_WRONLY); fstat(fdsrc, &sb); p = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fdsrc, 0); write(fddst, p, sb.st_size); return 0; }
一、mmap()+write()的效率表现
mmap()+write()的效率在大文件复制场景中远不如专门的文件复制系统调用,核心原因如下:
- 内存映射会将文件页加载到用户态地址空间,但调用
write()时,内核必须先把这些用户态页的数据拷贝到内核态缓冲区,再写入磁盘——这多了一次用户态到内核态的冗余拷贝。 - 对于10GB级别的大文件,一次性内存映射会触发大量页错误(page fault),导致内核频繁调度;若系统内存不足,还会引发页交换(swap),进一步拖慢复制速度。
二、各系统的优化情况
Linux
Linux内核不会对mmap()+write()的文件复制场景做特殊优化。这种方式本质是用户态发起的数据拷贝,内核无法识别这是“文件到文件”的复制操作,只能按常规write()流程处理。
*BSD
与Linux一致,*BSD内核也不会针对该场景做特殊优化。mmap()映射的页在write()时依然要经过用户态到内核态的拷贝,没有捷径可走。
macOS
macOS的XNU内核同样不会对该组合做特殊优化。不过macOS提供了fcopyfile()系统调用,专门用于高效文件复制,性能远高于mmap()+write()。
Windows
Windows下类似的内存映射是CreateFileMapping(),搭配WriteFile()写入时也无特殊优化。Windows的高效文件复制应使用CopyFile()或CopyFile2()这类原生API,它们会利用内核级直接拷贝逻辑。
三、与copy_file_range(2)的对比
copy_file_range(2)(Linux)、sendfile(2)(Linux/BSD)、fcopyfile()(macOS)这类专门的文件复制系统调用,效率远高于mmap()+write():
- 这些调用是内核级直接文件拷贝,完全避免了用户态与内核态之间的数据拷贝——内核直接从源文件的页缓存读取数据,写入目标文件的页缓存,无需经过用户态内存。
- 对于大文件,这类调用能充分利用内核的预读、写回优化,以及磁盘的连续IO特性,性能差距可达数倍。
Linux/*BSD源码中的处理逻辑
- Linux:
copy_file_range()通过copy_file_range_do_loop函数直接操作文件的页缓存,实现内核内的数据拷贝,完全绕开用户态。而write()处理mmap映射的内存时,会触发copy_from_user()操作,把数据从用户态拷贝到内核缓冲区,这一步是copy_file_range()没有的额外开销。 - BSD:
sendfile()或部分版本支持的copy_file_range()同样是内核内直接拷贝,write()处理mmap内存时依然需要用户态到内核态的拷贝,无特殊优化逻辑。
四、实践建议
- 优先使用各平台的原生高效文件复制API:Linux用
copy_file_range(2),macOS用fcopyfile(),Windows用CopyFile2(),BSD用sendfile(2)或copy_file_range()。 - 如果必须使用
mmap(),建议分块映射(比如每次映射256MB),避免一次性映射大文件导致的内存压力和页错误风暴,但即便如此,性能依然不如专门的复制API。 - 确保
write()的循环封装(比如你的full_write())能正确处理部分写入的情况,避免数据丢失。
内容的提问来源于stack exchange,提问作者complex

