CPython 低内存崩溃修复:代码编译、解释器通道与_winapi.CreateProcess中的MemoryError规范化
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
本文基于 CPython 主线仓库中的 Misc/NEWS.d/next/Core_and_Builtins/2026-06-09-10-28-30.gh-issue-151126.DKa6Sl.rst 修复记录,剖析当设备内存耗尽时,代码编译、_interpchannels模块与_winapi.CreateProcess三处可能引发崩溃的路径,以及它们如何改为抛出规范的MemoryError。读完本文,你将理解 CPython 在内存分配失败场景下的错误处理约定、各修复点的底层代码位置,以及如何在低内存环境下验证该行为。
修复背景:内存耗尽时的崩溃风险
在嵌入式设备、容器或长期运行的守护进程中,内存可能在任何时刻被耗尽。CPython 内部大量使用malloc/calloc及自有内存分配器,当分配失败时,C 层代码必须检查返回值并转入 Python 错误处理流程。然而,历史上部分路径在分配失败时未做检查,或直接返回NULL而未设置异常,导致解释器在上层拿到NULL时触发段错误(segfault)而非可捕获的MemoryError。
本次修复(issue gh-issue-151126)针对三类场景:
- 代码编译阶段(
Python/compile.c)的编译器对象分配; - 解释器通道模块
_interpchannels(Modules/_interpchannelsmodule.c)中的锁与等待项分配; - Windows 专属的
_winapi.CreateProcess(Modules/_winapi.c)内部缓冲分配。
修复的统一手段是:在分配失败处调用PyErr_NoMemory()(其内部通过_PyErr_NoMemory(tstate)设置MemoryError),确保NULL返回值始终伴随异常,从而让上层以异常而非崩溃的方式失败。
代码编译:编译器对象与作用域单元的分配检查
代码编译由 Python/compile.c 实现。该文件在两处分配失败路径上补充了PyErr_NoMemory():
new_compiler()(约 Python/compile.c#L171-L185):使用PyMem_Calloc(1, sizeof(compiler))分配编译器上下文,若返回NULL立即调用PyErr_NoMemory()并返回NULL。此前若未设置异常,调用方会把NULL当作一般错误处理,在无异常状态下继续解引用,进而可能崩溃。_PyCompile_EnterScope()(约 Python/compile.c#L599-L609):进入新的作用域单元(struct compiler_unit)时同样使用PyMem_Calloc分配,失败时调用PyErr_NoMemory()并返回ERROR标志。该函数为每个函数、类或模块作用域创建编译单元,是编译多层嵌套代码时的热路径,内存耗尽时极易在此触发问题。
此外,Python/codegen.c 与 Python/assemble.c 等编译链路的其他文件中也已普遍存在PyErr_NoMemory()检查(例如Python/assemble.c#L421、Python/codegen.c#L141-L162),说明编译器整体遵循“分配失败即设异常”的约定,本次修复补全了剩余缺口。
解释器通道:_interpchannels的锁与等待项分配
_interpchannels是 CPython 子解释器(subinterpreter)之间进行通道(channel)通信的内部模块,实现在 Modules/_interpchannelsmodule.c。该模块中大量使用PyThread_allocate_lock()分配线程锁,并在等待队列中管理_waiting_t结构,这些分配在极端内存压力下同样可能失败。
本次修复在该文件的 9 处分配失败点统一补上了PyErr_NoMemory()(分布在 Modules/_interpchannelsmodule.c#L466、#L604、#L683、#L862、#L921、#L1115、#L1314、#L1700、#L1743 等位置)。以等待项初始化为例(Modules/_interpchannelsmodule.c#L461-L475):
static int _waiting_init(_waiting_t *waiting) { PyThread_type_lock mutex = PyThread_allocate_lock(); if (mutex == NULL) { PyErr_NoMemory(); /* 分配失败:设置 MemoryError 而不是静默返回 NULL */ return -1; } *waiting = (_waiting_t){ .mutex = mutex, .status = WAITING_NO_STATUS, }; return 0; }从源码结构看,_interpchannels中还有更多PyThread_allocate_lock()、动态数组扩容等分配点(文件共 3661 行,PyErr_NoMemory()出现 9 处),本次 NEWS 条目描述的即是这批低内存路径的系统性加固,目标是让通道接收(recv)、发送(send)与等待(waiting)操作在内存不足时以MemoryError优雅失败。
_winapi.CreateProcess:Windows 进程创建前的缓冲分配
_winapi是 CPython 在 Windows 上的进程创建内部模块(Modules/_winapi.c),其CreateProcess包装了 Win32 APICreateProcessW。该函数在真正调用系统 API 之前需要完成多项内存分配:
getenvironment()将环境变量映射转换为宽字符wchar_t*环境块;PyUnicode_AsWideCharString()将命令行字符串转为可写的宽字符副本;- 属性列表(
lpAttributeList)等结构的内存管理。
当这些分配在低内存下失败时,路径直接goto cleanup;若失败发生在未设置异常的分配上,上层就会在无异常状态下继续,最终崩溃。本次修复在这些分配失败点补上了PyErr_NoMemory()(例如 Modules/_winapi.c#L1078、#L1134、#L1195、#L1280、#L1685、#L2027、#L2409),使得CreateProcess在内存耗尽时抛出可捕获的MemoryError,而不是让进程创建代码崩溃。
需要说明的是,CreateProcessW系统调用本身失败(例如可执行文件不存在、权限不足)时,代码走的是另一条路径:调用PyErr_SetFromWindowsErr(GetLastError())抛出对应 Windows 错误(Modules/_winapi.c#L1422-L1425)。本次修复只涉及调用系统 API 之前的内部内存分配失败,两者错误语义互不干扰。
修复的统一模式与验证方式
三处修复遵循同一模式:内存分配 API 返回NULL→ 调用PyErr_NoMemory()→ 返回NULL/错误码。PyErr_NoMemory()是 CPython 中设置MemoryError的标准入口,其底层为_PyErr_NoMemory(tstate)(见 Python/bltinmodule.c#L1588 的引用用法),确保异常与NULL返回值严格配对。这一约定在 CPython 内存错误处理中具有普适性:整个代码库(Python/compile.c、Python/codegen.c、Python/assemble.c、Python/ceval.c、Python/crossinterp.c、Python/codecs.c等)均大量使用PyErr_NoMemory()。
由于修复点位于解释器内部,普通用户难以直接构造内存耗尽场景,但可以通过以下方式验证相关行为:
- 单元测试:运行
_winapi相关测试验证CreateProcess的异常路径,测试位于 Lib/test/test_winapi.py; - 注入式测试:在支持 fault injection(如
LD_PRELOAD拦截malloc返回NULL)的环境中运行 Python,可观察代码编译(compile()内建函数)与子解释器通道操作在低内存下抛出MemoryError而非段错误; - 受限内存运行:使用
ulimit -v(Linux)等工具限制虚拟内存后执行嵌套函数编译或_interpchannels通信,验证异常可被try/except MemoryError捕获。
修复意义小结
本次修复消除了三类低内存崩溃路径,将“设备内存耗尽”从解释器段错误转变为标准 Python 异常:
| 修复点 | 所在文件 | 失败场景 |
|---|---|---|
| 代码编译 | Python/compile.c | 编译器对象、作用域单元分配失败 |
| 解释器通道 | Modules/_interpchannelsmodule.c | 线程锁、等待项等分配失败 |
| 进程创建 | Modules/_winapi.c | CreateProcess内部缓冲分配失败 |
对应用开发者而言,这意味着在内存受限的部署环境中,compile()、子解释器通道通信以及 Windows 进程创建代码可以依赖try/except MemoryError进行优雅降级与资源回收,而不是面对无法捕获的进程崩溃;对 CPython 内核开发者而言,这是一次低内存健壮性(OOM-resilience)的常规加固,与此前代码库中既有的PyErr_NoMemory()约定保持一致。
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考