处理几GB甚至几十GB的大文件时,如果还沿用普通的read或readlines逐行读取,不仅内存占用会快速膨胀,频繁的系统调用也会让CPU时间大量消耗在内核态与用户态之间的切换上。Python标准库中的mmap模块把文件内容映射到进程的虚拟地址空间,之后就可以通过切片、索引、正则搜索等方式直接访问文件数据,操作系统会在背后默默完成缺页加载和脏页回写,这大幅降低了超大文件读写的复杂度。

mmap的底层原理:为什么它比read/write快
传统read系统调用处理文件时,数据需要经过两次拷贝:先从磁盘读入内核的页缓存,再从内核页缓存复制到用户空间的缓冲区。如果程序只是扫描文件内容,第二次拷贝完全是浪费。mmap则让用户进程的虚拟地址直接指向内核页缓存对应的物理页,访问映射区域时如果页已经存在,CPU直接读写该内存页,不发生任何系统调用和数据复制;如果页尚未加载,会触发缺页中断,由内核把对应文件块读入页缓存并建立映射。这样程序就像操作一个巨大的字节数组,省去了反复read带来的上下文切换和内存拷贝。
另外,内存映射还能很好地利用操作系统的页缓存淘汰机制。当物理内存紧张时,干净的文件映射页可以直接丢弃,因为数据在磁盘上还有副本;脏页则会在合适的时机异步写入文件,这种按需调度比应用程序手动维护缓冲区要高效得多。对于随机读取频繁的场景,比如在几GB的日志文件中反复跳转查找某个关键字,mmap的优势尤其明显,因为传统read需要先lseek再读取,每次都要陷入内核,而mmap之后直接用指针偏移访问即可。
用Python的mmap模块读写超大文件
先看一个完整的最小示例:打开文件后调用mmap把整个文件映射到内存,然后可以用切片读取、修改、查找,最后关闭映射对象。注意映射长度可以指定,不一定非要整个文件,对于超大文件也可以只映射需要的几个分段,避免占用过多虚拟地址空间。
import mmap
with open("large.log", "r+b") as f:
# 映射整个文件,access=ACCESS_WRITE表示可读写
mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_WRITE)
# 读取前100个字节
head = mm[:100]
print(head)
# 查找关键字的位置
pos = mm.find(b"ERROR")
if pos != -1:
print(f"找到ERROR,位置: {pos}")
# 查看周围内容
print(mm[pos-20:pos+50])
# 修改指定偏移处的若干字节
mm[0:5] = b"HELLO"
# 主动将修改同步回磁盘
mm.flush()
mm.close()
上面代码中open使用了"r+b"模式,因为映射可写时必须保证文件本身可写。mmap的第二个参数0表示映射整个文件,也可以传入一个正整数来只映射前面若干字节。find方法直接在映射的字节串上搜索,底层会调用C库的memchr或类似实现,效率比Python循环高很多。修改内容后,即使不调用flush,操作系统也会在内核线程中定期刷盘,但调用flush可以强制立即回写,适合对一致性要求较高的场景。
如果文件是只读的,可以将access设置为mmap.ACCESS_READ,同时打开文件使用"rb"模式即可。只读映射不允许任何写操作,但可以安全地共享给多个进程。Python的mmap还支持通过move方法在映射内部移动字节块,以及resize调整映射大小,不过调整映射大小需要文件本身支持截断或扩展,并注意原有映射对象会被更新。
mmap的性能优势与典型应用场景
从性能数据看,mmap在顺序读取大文件时不一定比带缓冲的read快很多,因为read本身也可以设置较大的buffer一次读入几十KB。但一旦涉及随机访问,mmap的收益就非常突出。假设要在一个10GB的二进制文件中随机读取100万个4KB的块,传统做法是每次seek再read,会产生100万次系统调用,而mmap只需要一次映射,然后直接通过偏移计算内存地址访问,系统调用数量几乎降为零。页缓存命中率足够高时,访问速度甚至接近直接读取内存。
典型应用场景包括:大日志文件分析,可以配合正则表达式直接在映射对象上执行re.search或re.finditer,避免把整个文件加载成Python字符串;二进制数据文件检索,如读取自定义格式的索引文件,通过struct模块按偏移解析;多个进程间共享只读数据,比如web服务加载一个大型字典或配置,多个worker进程映射同一个文件,操作系统只保留一份物理页,显著减少内存占用;以及实现简单的数据库存储引擎,把数据和索引映射到文件,修改后定期flush。
但mmap也不是银弹。对于只需要顺序从头到尾处理一遍的文件,普通read循环已经足够简单,而且不会占用虚拟地址空间;对于频繁追加写入的文件,mmap映射的长度固定后,文件增长不会自动反映到映射中,需要重新映射。另外,32位进程地址空间只有4GB,映射超大文件可能失败,64位系统一般没有这个限制,但过大映射也会增加页表大小和缺页处理开销。
mmap使用中的常见误区和避坑要点
第一个常见的坑是映射后文件被其他进程截断或替换。当映射区域对应的文件被truncate缩短时,访问超出文件末尾的映射页会触发SIGBUS信号,直接导致Python进程崩溃,而且很难捕获。解决办法是尽量在映射期间独占文件,或者使用文件锁保护,或者在捕获不到信号时定期检查文件大小并重新映射。
第二个坑是修改映射内容后认为数据立刻落盘,实际上脏页可能停留在内存中,如果机器断电或进程被kill -9,修改会丢失。重要数据必须显式调用flush,并且最好再调用os.fsync确保文件系统元数据也落盘。另外,关于映射对象的生命周期:mmap对象关闭后,所有通过它创建的memoryview可能失效,不要继续使用。还有,当mmap对象被垃圾回收时,映射会自动解除,但如果文件描述符已经被关闭,mmap仍然可以访问之前映射的内容,这有时会造成困惑。
还有一个优化上的误区:以为映射整个超大文件就能获得最大性能。实际上映射本身不消耗物理内存,只占用虚拟地址空间,但访问时会按页加载,如果随机访问非常分散,缺页中断频繁,性能可能还不如带预读的read。针对这类情况可以只映射热点区域,比如文件头部的索引部分,其余数据仍然按需读取。总之,mmap是强大而灵活的工具,理解其背后的页缓存机制,才能在合适的场景中发挥它的威力。
Python mmap内存映射文件超大文件读写修改时间:2026-09-28 01:36:47