- 游戏开发
- 后端
【免费下载链接】kbengine
A MMOG engine of server.
本文以 KBEngine 仓库内置 Python 3.7.3 标准库文档 mmap.rst 为主体,并结合其底层 C 实现 mmapmodule.c 与官方测试 test_mmap.py 展开讲解。读完本文,你将掌握
mmap对象在 Unix 与 Windows 两种平台下的构造差异、四种访问模式(ACCESS_READ / ACCESS_WRITE / ACCESS_COPY / ACCESS_DEFAULT)的语义,以及全部 14 个实例方法与 1 个只读属性的用法,并能在 KBEngine 的游戏服务器脚本中直接使用内存映射文件进行高效的大文件读写、正则检索与进程间共享数据。
一、mmap 是什么:兼具 bytearray 与文件对象双重语义
mmap模块提供对 Unix 与 Windows 平台上内存映射文件(memory-mapped file)的接口。内存映射文件对象的行为同时类似于bytearray和文件对象(file object),因此它几乎可以在任何期望bytearray的地方使用,例如:
- 使用
re模块在内存映射文件中直接进行正则表达式搜索; - 通过
obj[index] = 97修改单个字节; - 通过切片赋值修改子序列:
obj[i1:i2] = b'...'; - 从当前文件位置读写数据,并用
seek在不同位置间移动。
这种设计使 mmap 对象天然具备"可随机寻址的字节缓冲区 + 传统文件游标读写"的双重能力,在 KBEngine 这类以 Python 承载游戏逻辑的服务端环境中,非常适合用来映射并快速检索诸如配置表、地图数据、热更新资源等大文件。
从源码层面看,这种双重语义由 mmapmodule.c 中的类型定义直接支撑:mmap_object同时挂接了序列协议(mmap_as_sequence)、映射协议(mmap_as_mapping)和缓冲区协议(mmap_as_buffer,见 mmapmodule.c#L942-L951),因此既可以像bytearray一样按下标/切片访问,也可以作为 buffer 协议对象被其他模块消费。
二、创建映射:构造函数在 Unix 与 Windows 上的差异
内存映射文件由mmap构造函数创建,该构造函数在 Unix 与 Windows 上签名不同。无论哪种平台,都必须提供一个以"更新(update)"方式打开的文件的文件描述符:
- 如果要把已存在的 Python 文件对象映射到内存,用其
fileno()方法取得fileno参数所需的值; - 否则可以直接用
os.open打开文件,它会直接返回文件描述符(使用完毕后仍需关闭文件)。
注意(官方原文档提示):如果要为一个可写、带缓冲的文件创建内存映射,应先调用
flush()(即io.IOBase.flush)。这是为了保证缓冲区中的本地修改确实对映射可见。
2.1 Windows 版本
mmap(fileno, length, tagname=None, access=ACCESS_DEFAULT[, offset])- 从文件句柄
fileno指定的文件中映射length字节并创建 mmap 对象; - 如果
length大于文件当前大小,文件会被扩展以容纳length字节; - 如果
length为0,映射的最大长度取文件的当前大小;但如果文件为空,Windows 会抛出异常(Windows 上不能创建空映射); tagname:若指定且非None,则是给映射命名的字符串。Windows 允许对同一文件建立多个不同映射:如果指定了已存在的标签名则打开该标签,否则创建同名新标签;省略或为None则创建无名映射。不使用 tag 参数有助于保持代码在 Unix 与 Windows 之间的可移植性;offset:非负整数偏移,mmap 的引用将相对于文件开头的该偏移量,默认为0,且必须是ALLOCATIONGRANULARITY的倍数。
2.2 Unix 版本
mmap(fileno, length, flags=MAP_SHARED, prot=PROT_WRITE|PROT_READ, access=ACCESS_DEFAULT[, offset])- 从文件描述符
fileno指定的文件中映射length字节并返回 mmap 对象;如果length为0,映射的最大长度取调用 mmap 时文件的当前大小; flags决定映射的性质:MAP_PRIVATE创建私有写时复制(copy-on-write)映射,对 mmap 对象内容的修改仅对本进程私有;MAP_SHARED创建与其他映射同一文件区域的进程共享的映射,默认值为MAP_SHARED;prot给出期望的内存保护级别,最常用的两个值是PROT_READ与PROT_WRITE,分别表示页可读、页可写,默认值为PROT_READ | PROT_WRITE;access可以替代flags与prot作为可选关键字参数。同时指定flags、prot与access是错误用法(详见下文源码佐证);offset:非负整数偏移,默认为0,必须是ALLOCATIONGRANULARITY的倍数,在 Unix 上ALLOCATIONGRANULARITY等于PAGESIZE;- 在 Mac OS X 与 OpenVMS 上,为保证所创建内存映射的有效性,描述符
fileno指定的文件会被内部自动与物理后备存储同步。
2.3 构造参数的源码级对照
在 mmapmodule.c 中,两种平台的构造函数由同一函数名的两套实现分别完成:
- Unix 版
new_mmap_object的关键字列表为fileno, length, flags, prot, access, offset(mmapmodule.c#L1042-L1044),内部最终调用系统mmap(NULL, map_size, prot, flags, fd, offset)(mmapmodule.c#L1166-L1168); - Windows 版
new_mmap_object的关键字列表为fileno, length, tagname, access, offset(mmapmodule.c#L1208-L1210),内部通过CreateFileMapping+MapViewOfFile实现(mmapmodule.c#L1362-L1373); - 关于"同时指定 access 与 flags/prot 是错误"的约束,Unix 实现中有显式校验:当
access非ACCESS_DEFAULT且flags、prot不是默认值时,直接抛出ValueError: mmap can't specify both access and flags, prot.(mmapmodule.c#L1061-L1064); - 空文件映射在两种平台上都会被拒绝:Unix 版在
map_size == 0且文件大小为 0 时抛出ValueError: cannot mmap an empty file(mmapmodule.c#L1109-L1114),Windows 版同样处理(mmapmodule.c#L1307-L1313)。测试 test_mmap.py#L491-L498 的test_empty_file用例正是验证这一点。
三、access 访问模式详解:四种常量的完整语义
对于 Unix 与 Windows 两个版本的构造函数,access都可以作为可选关键字参数指定,它接受以下四个值之一:
| 常量 | 语义 | 对内存的影响 | 对底层文件的影响 |
|---|---|---|---|
ACCESS_READ | 只读 | 可读 | 不可写;赋值操作抛出TypeError |
ACCESS_WRITE | 写直达(write-through) | 可写 | 写入同时影响内存与底层文件 |
ACCESS_COPY | 写时复制(copy-on-write) | 可写 | 只影响内存,不更新底层文件 |
ACCESS_DEFAULT | 推迟到prot决定 | 见下文 | 见下文 |
要点:
- 如果不指定
access,Windows 的 mmap 返回**写直达(write-through)**映射; - 三种访问类型的初始内存值均取自指定的文件;
- 对
ACCESS_READ映射赋值会抛出TypeError异常; - 对
ACCESS_WRITE映射赋值同时影响内存与底层文件; - 对
ACCESS_COPY映射赋值只影响内存,不更新底层文件; ACCESS_DEFAULT常量是3.7 版本新增(.. versionchanged:: 3.7),用于"推迟到 prot"。
在 mmapmodule.c 中,访问模式的枚举定义与映射逻辑清晰可见:
- Unix 版枚举
access_mode依次为ACCESS_DEFAULT, ACCESS_READ, ACCESS_WRITE, ACCESS_COPY(mmapmodule.c#L82-L88); - Unix 版
access到flags/prot的转换:ACCESS_READ→MAP_SHARED | PROT_READ;ACCESS_WRITE→MAP_SHARED | PROT_READ|PROT_WRITE;ACCESS_COPY→MAP_PRIVATE | PROT_READ|PROT_WRITE;ACCESS_DEFAULT则从prot反推访问类型(mmapmodule.c#L1065-L1089); - Windows 版
access到 Win32 标志的转换:ACCESS_READ→PAGE_READONLY + FILE_MAP_READ;ACCESS_DEFAULT/ACCESS_WRITE→PAGE_READWRITE + FILE_MAP_WRITE;ACCESS_COPY→PAGE_WRITECOPY + FILE_MAP_COPY(mmapmodule.c#L1218-L1234)。
四、匿名内存映射:以 -1 作为 fileno
要映射匿名内存,应将-1作为fileno传入,并同时给出长度。这在两种平台上行为一致:
- Unix 实现中,当
fd == -1时优先走MAP_ANONYMOUS(BSD 方式);若不支持则退回打开/dev/zero(SVR4 方式)(mmapmodule.c#L1140-L1157); - 测试 test_mmap.py#L408-L418 的
test_anonymous用例验证了匿名映射的初始内容全为 0 且可写。
五、模块级常量:PAGESIZE 与 ALLOCATIONGRANULARITY
PAGESIZE与ALLOCATIONGRANULARITY是决定offset、flush对齐约束的两个模块级常量:
mmap.PAGESIZE:系统内存页大小;mmap.ALLOCATIONGRANULARITY:Windows 上的内存分配粒度;在 Unix 上它等于PAGESIZE。
它们在模块初始化时被写入模块字典:setint(dict, "PAGESIZE", (long)my_getpagesize())、setint(dict, "ALLOCATIONGRANULARITY", (long)my_getallocationgranularity())(mmapmodule.c#L1454-L1456)。Unix 版my_getpagesize基于sysconf(_SC_PAGESIZE),Windows 版则取自GetSystemInfo的dwPageSize/dwAllocationGranularity(mmapmodule.c#L32-L51)。
此外模块还注册了PROT_EXEC、PROT_READ、PROT_WRITE、MAP_SHARED、MAP_PRIVATE、MAP_DENYWRITE、MAP_EXECUTABLE、MAP_ANON/MAP_ANONYMOUS等平台相关常量(mmapmodule.c#L1427-L1452),并把模块级mmap.error指向OSError(mmapmodule.c#L1425),测试test_error亦断言mmap.error is OSError(test_mmap.py#L573-L574)。
六、实例 API 全解:方法与属性
以下方法均在 mmapmodule.c#L695-L721 的mmap_object_methods与mmap_object_getset中注册,与文档一一对应。
6.1 close() 与 closed 属性
close():关闭 mmap。关闭后继续调用对象其他方法会抛出ValueError异常;它不会关闭打开的文件。源码中关闭后把内部资源置为无效标记(Unix 置data = NULL、fd = -1,Windows 关闭句柄),之后CHECK_VALID宏统一抛出ValueError: mmap closed or invalid(mmapmodule.c#L174-L205);closed(属性,3.2 新增):True表示文件已关闭。注意:重复调用 close() 是安全的,测试test_double_close专门验证(test_mmap.py#L307-L318)。
6.2 find(sub[, start[, end]]) 与 rfind(sub[, start[, end]])
find(sub[, start[, end]]):返回对象中sub子序列出现的最低索引,要求sub完整包含在[start, end]区间内;start、end可选参数按切片语义解释;失败返回 -1;rfind(sub[, start[, end]]):返回sub出现的最高索引,参数语义同上,失败返回 -1;- 3.5 版本起两者都接受可写 bytes-like 对象。
源码mmap_gfind内部对负的 start/end 按 Python 切片语义折算,并对越界值做夹取(mmapmodule.c#L263-L326)。测试test_tougher_find(含内嵌\0的二进制数据)、test_find_end、test_rfind覆盖了各种边界(test_mmap.py#L251-L304)。
6.3 flush([offset[, size]])
- 把对文件内存副本的修改刷回磁盘;不调用它则无法保证对象被销毁前修改已写回;
- 若指定
offset与size,仅将给定字节范围刷回;否则刷回整个映射范围;offset必须是PAGESIZE或ALLOCATIONGRANULARITY的倍数; - Windows 版:返回非零值表示成功,零表示失败;
- Unix 版:返回零表示成功,调用失败时抛出异常。
源码 Unix 分支基于msync(..., MS_SYNC)实现,Windows 分支基于FlushViewOfFile;另外当访问模式为ACCESS_READ或ACCESS_COPY时直接返回 0(mmapmodule.c#L559-L588)。
6.4 move(dest, src, count)
把从偏移src开始的count字节复制到目标索引dest。若 mmap 以ACCESS_READ创建,调用move会抛出TypeError。源码通过memmove实现并做越界检查(mmapmodule.c#L629-L653);测试test_move用穷举的 dest/src/count 组合验证了与列表切片等价的语义(test_mmap.py#L359-L406)。
6.5 read([n]) 与 read_byte()
read([n]):返回一个bytes,包含从当前位置起的至多n字节;省略参数、传None或负数时,返回从当前位置到映射末尾的全部字节;文件位置更新到已返回字节之后。3.3 版本起参数可省略或为None。源码对超界请求静默夹取(mmapmodule.c#L243-L261);read_byte():返回当前位置的单个字节(整数),位置前进 1;越界抛出ValueError(mmapmodule.c#L207-L217)。
6.6 readline()
返回从当前位置开始、到下一个换行符为止的单行内容。源码用memchr定位\n,无换行则取到映射末尾(mmapmodule.c#L219-L241)。
6.7 resize(newsize)
调整映射及底层文件(如果有)的大小。若 mmap 以ACCESS_READ或ACCESS_COPY创建,resize会抛出TypeError。注意平台差异:Unix 依赖mremap,无HAVE_MREMAP时抛出SystemError: mmap: resizing not available--no mremap()(mmapmodule.c#L516-L519);Windows 通过SetFilePointer+SetEndOfFile+ 重新CreateFileMapping实现(mmapmodule.c#L469-L514)。测试中多处用try/except SystemError兼容不支持 resize 的平台(如 test_mmap.py#L96-L118)。
6.8 seek(pos[, whence])
设置文件当前位置。whence可选,默认os.SEEK_SET(0,绝对定位);其他取值:os.SEEK_CUR(1,相对当前位置)、os.SEEK_END(2,相对文件末尾)。源码对未知 whence 抛出ValueError: unknown seek type,越界抛出ValueError: seek out of range(mmapmodule.c#L590-L627);测试验证了负位置与越界 seek 均抛ValueError(test_mmap.py#L87-L94)。
6.9 size() 与 tell()
size():返回文件的长度,它可能大于内存映射区域的尺寸。Unix 实现通过fstat读取st_size,Windows 通过GetFileSize读取(mmapmodule.c#L403-L442);tell():返回文件指针的当前位置(mmapmodule.c#L551-L556)。
6.10 write(bytes) 与 write_byte(byte)
write(bytes):把bytes写入文件指针当前位置的内存,并返回实际写入的字节数(不会小于len(bytes),因为写入失败会抛出ValueError);文件位置更新到写入字节之后;对ACCESS_READ映射写入抛出TypeError。3.5 版本起接受可写 bytes-like 对象,3.6 版本起返回写入字节数。源码用memcpy实现,越界抛ValueError: data out of range,并返回data.len(mmapmodule.c#L353-L378);测试test_write_returning_the_number_of_bytes_written专门验证返回值(test_mmap.py#L716-L721);write_byte(byte):把整数byte写入文件指针当前位置,位置前进 1;对ACCESS_READ映射写入抛出TypeError,越界抛ValueError(mmapmodule.c#L380-L401)。
七、实战示例(原文档完整保留并展开)
7.1 基本用法:映射整个文件并读写
import mmap # 写一个简单的示例文件 with open("hello.txt", "wb") as f: f.write(b"Hello Python!\n") with open("hello.txt", "r+b") as f: # 内存映射该文件,size 0 表示整个文件 mm = mmap.mmap(f.fileno(), 0) # 通过标准文件方法读取内容 print(mm.readline()) # 打印 b"Hello Python!\n" # 通过切片记法读取内容 print(mm[:5]) # 打印 b"Hello" # 用切片记法更新内容;注意新内容必须与原内容等长 mm[6:] = b" world!\n" # 再用标准文件方法读一遍 mm.seek(0) print(mm.readline()) # 打印 b"Hello world!\n" # 关闭映射 mm.close()要点:文件必须以"r+b"(可读可写、二进制)方式打开;length=0表示映射整个文件;切片赋值要求等长替换。测试 test_mmap.py#L28-L120 的test_basic是这套流程的完整验证,包括单字节赋值m[0] = b'3'[0]、切片赋值m[PAGESIZE+3:PAGESIZE+3+3] = b'bar'、re.search直接在 mmap 对象上做正则匹配等。
7.2 上下文管理器用法(3.2 新增)
import mmap with mmap.mmap(-1, 13) as mm: mm.write(b"Hello world!")这里-1作为 fileno 表示匿名映射,with语句在退出时自动调用close()。底层由__enter__/__exit__方法实现(mmapmodule.c#L665-L680),测试test_context_manager与test_context_manager_exception验证了正常关闭与异常透传行为(test_mmap.py#L693-L705)。
7.3 匿名映射 + fork:父子进程数据交换
import mmap import os mm = mmap.mmap(-1, 13) mm.write(b"Hello world!") pid = os.fork() if pid == 0: # 子进程 mm.seek(0) print(mm.readline()) mm.close()匿名映射在 fork 后由父子进程共享同一段物理内存,是 Python 侧轻量级 IPC 的经典手段。在 KBEngine 中,服务器进程体系(loginapp、baseappmgr、cellappmgr、baseapp、cellapp、dbmgr、interfaces 等)虽以 C++ 承载核心逻辑,但游戏业务脚本运行在 Python 侧,mmap同样可用于脚本之间共享只读资源或临时数据。
7.4 指定 offset 映射文件窗口
当length传0并配合offset时,可以映射文件从 offset 到末尾的"窗口":
import mmap with open("hello.txt", "rb") as f: # 映射从 offset 开始、到文件末尾的区域 with mmap.mmap(f.fileno(), 0, offset=65536, access=mmap.ACCESS_READ) as mf: print(mf[:16])测试test_length_0_offset与test_length_0_large_offset专门覆盖了该场景的边界(test_mmap.py#L336-L357);LargeMmapTests还验证了在 2GB/4GB 边界及超过 32 位地址空间的大型文件上的行为(test_mmap.py#L745-L803)。
八、使用注意事项与平台差异总结
- 写缓冲文件前先 flush:对可写、带缓冲的文件做映射前必须
flush(),否则缓冲区的本地修改可能对映射不可见; - offset 对齐:
offset必须是ALLOCATIONGRANULARITY的倍数(Unix 上即PAGESIZE);flush的offset同理; - access 与 flags/prot 互斥:同时指定
access与flags、prot会抛ValueError;不指定access时 Windows 默认写直达; - 空文件不可映射:两种平台均对空文件抛
ValueError; - resize 的平台差异:Unix 需要
mremap支持,否则抛SystemError;ACCESS_READ/ACCESS_COPY映射不可 resize(抛TypeError); - flush 返回值语义相反:Windows 非零表示成功,Unix 返回 0 表示成功、失败抛异常;
- 大文件注意:32 位平台上映射超过
sys.maxsize的文件会失败(OverflowError),应改用较小的length分段映射(见 test_mmap.py#L775-L785); - tagname 仅 Windows 使用:为保持跨平台可移植性,避免使用该参数。
九、在 KBEngine 仓库中的定位
- 本文对应的标准库文档位于 kbe/src/lib/python/Doc/library/mmap.rst,它是仓库内置 Python 3.7.3(见 kbe/src/lib/python/Include/patchlevel.h)的标准库参考手册之一;
- 底层 C 实现位于 kbe/src/lib/python/Modules/mmapmodule.c,随 Python 一同编译进引擎的 Python 运行时;
- 官方回归测试位于 kbe/src/lib/python/Lib/test/test_mmap.py,是验证上述全部语义的权威参考。
KBEngine 的服务端以 Python 承载游戏逻辑脚本,因此mmap模块在游戏工程中可直接使用:例如将体量较大的静态配置(地图网格、寻路数据、技能表)预生成后整体映射进内存做随机检索,或利用ACCESS_COPY写时复制语义在多个脚本进程间共享只读数据而不互相污染。理解本文的访问模式与对齐约束,即可安全高效地在自己的游戏服务器脚本中落地内存映射方案。
- 游戏开发
- 后端
【免费下载链接】kbengine
A MMOG engine of server.
相关推荐
深入解析 mmap-go:cloudpods 中的 Go 内存映射文件读写库
深入解析 mmap go:cloudpods 中的 Go 内存映射文件读写库 导读 mmap go 是一个为 Go 语言设计的可移植内存映射(memory ma
后端云原生运维虚拟化人工智能AI 应用Calibre 电子书转换:5 分钟拿到可读 EPUB,目录自动生成
Calibre 电子书转换:5 分钟拿到可读 EPUB,目录自动生成 你攒了几十本电子书,格式五花八门,老阅读器认不全。Calibre 是免费开源的电子书转换工
桌面应用后端CPython mmap 模块完全指南:跨平台内存映射文件的创建、方法与源码剖析
CPython mmap 模块完全指南:跨平台内存映射文件的创建、方法与源码剖析 mmap 是 CPython 标准库中把文件内容直接映射进进程地址空间的模块,
编程语言语言运行时标准库解释器
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考