CPython C API 剖析与追踪机制:PyEval_SetProfile、PyEval_SetTrace 与 PyRefTracer 完全指南
Python 解释器在 C 层提供了一组低层的剖析(profiling)与执行追踪(tracing)设施,它是 cProfile、sys.settrace、代码覆盖率工具以及内存追踪工具的共同基础。本文以 CPython 官方 C API 文档 profiling.rst 为主体,结合 Python/、Objects/、Include/cpython/ 下的真实源码实现,系统讲解 Py_tracefunc 回调模型、八种事件常量及其参数语义、PyEval_SetProfile/PyEval_SetTrace 四个注册函数,以及 Python 3.13 引入的引用追踪接口 PyRefTracer。读完本文,你将能够理解 C 级回调为何比 Python 级回调更轻量、各事件在字节码执行流中的精确触发时机,以及如何在扩展模块中安全地注册这两类追踪器。
为什么需要 C 级剖析与追踪接口
解释器为剖析、调试和覆盖率分析工具提供低层支持。其核心价值在于:让剖析/追踪代码绕开通过 Python 层可调用对象(callable object)进行调用的开销,改为直接调用一个 C 函数。注册回调是"按线程"(per-thread)的,且上报给 C 级追踪函数的事件集合,与以往上报给 Python 级 trace 函数的事件保持兼容。
这一设计动机可以直接从源码中得到印证。C 级回调类型定义在 pystate.h:
/* Py_tracefunc return -1 when raising an exception, or 0 for success. */
typedef int (*Py_tracefunc)(PyObject *, PyFrameObject *, int, PyObject *);
#define PyTrace_CALL 0
#define PyTrace_EXCEPTION 1
#define PyTrace_LINE 2
#define PyTrace_RETURN 3
#define PyTrace_C_CALL 4
#define PyTrace_C_EXCEPTION 5
#define PyTrace_C_RETURN 6
#define PyTrace_OPCODE 7
注释明确给出了返回值约定:回调返回 0 表示成功,返回 -1 表示回调中引发了异常。对比 Python 层的 sys.setprofile/sys.settrace,C 层注册后每次事件都直接调用 C 函数指针,跳过了 PyObject 属性查找与 Python 调用协议,这是高性能剖析器的关键优化点。
Py_tracefunc 回调类型与事件语义
回调签名
通过 PyEval_SetProfile 或 PyEval_SetTrace 注册的追踪函数类型为 Py_tracefunc,签名为:
int (*Py_tracefunc)(PyObject *obj, PyFrameObject *frame, int what, PyObject *arg);
obj:注册时传入的上下文对象,在每次回调中作为第一个参数原样带回;frame:事件所关联的帧对象(PyFrameObject *);what:事件类型常量之一,见下表;arg:其含义依赖 what 的取值。
各事件下 arg 的语义如下(完整继承自官方文档):
| what 取值 | arg 的含义 |
|---|---|
PyTrace_CALL |
恒为 Py_None |
PyTrace_EXCEPTION |
异常信息,与 sys.exc_info() 返回的内容一致 |
PyTrace_LINE |
恒为 Py_None |
PyTrace_RETURN |
即将返回给调用者的值;若因异常退出则为 NULL |
PyTrace_C_CALL |
正在被调用的 C 函数对象 |
PyTrace_C_EXCEPTION |
正在被调用的 C 函数对象 |
PyTrace_C_RETURN |
正在被调用的 C 函数对象 |
PyTrace_OPCODE |
恒为 Py_None |
各事件常量的精确语义
PyTrace_CALL —— 报告一次对函数或方法的新调用,或一次进入生成器(generator)的新入口。一个容易忽略的细节:生成器函数迭代器的创建动作不会被报告,因为此时没有控制流转入对应帧的 Python 字节码。
PyTrace_EXCEPTION —— 当异常被抛出时报告。回调的触发时机是:任何一条字节码处理完之后,该异常在"正在执行的帧"内变为已设置状态。其效果是:当异常传播导致 Python 栈逐层展开时,回调会在异常传播过程中于返回到每一个帧时各被调用一次。注意:只有追踪函数(trace function)会收到这类事件,剖析函数(profile function)不会,因为 profiler 用不到它。
PyTrace_LINE —— 报告行号事件,仅面向追踪函数(非剖析函数)。可以通过将某一帧的 f_trace_lines 属性置 0 来在该帧内禁用行事件。
PyTrace_RETURN —— 一次调用即将返回时报告(arg 携带返回值)。
PyTrace_C_CALL / PyTrace_C_EXCEPTION / PyTrace_C_RETURN —— 分别报告 C 函数即将被调用、C 函数抛出了异常、C 函数已经返回。这三类事件只面向剖析函数。
PyTrace_OPCODE —— 报告即将执行的一条新字节码指令,仅面向追踪函数,且默认不会发出:必须在帧上显式将 f_trace_opcodes 置 1 才会开启。
PyEval_SetProfile:注册剖析函数
void PyEval_SetProfile(Py_tracefunc func, PyObject *obj);
将当前线程的剖析函数设为 func。obj 参数是任意 Python 对象(可为 NULL),会在每次回调中作为第一个参数传入;如果剖析函数需要维护状态,为每个线程传入不同的 obj 值,就是一个方便且线程安全的状态存放位置。剖析函数会收到除 PyTrace_LINE、PyTrace_OPCODE 与 PyTrace_EXCEPTION 之外的所有受监控事件。对应 Python 层函数为 sys.setprofile。调用者必须处于 attached thread state 状态。
声明位于 ceval.h:
PyAPI_FUNC(void) PyEval_SetProfile(Py_tracefunc, PyObject *);
PyAPI_FUNC(void) PyEval_SetProfileAllThreads(Py_tracefunc, PyObject *);
PyAPI_FUNC(void) PyEval_SetTrace(Py_tracefunc, PyObject *);
PyAPI_FUNC(void) PyEval_SetTraceAllThreads(Py_tracefunc, PyObject *);
PyEval_SetProfileAllThreads(Python 3.12 新增)
void PyEval_SetProfileAllThreads(Py_tracefunc func, PyObject *obj);
与 PyEval_SetProfile 相同,但将剖析函数设置到当前解释器中所有正在运行的线程,而不仅是当前线程。与前者一样,该函数在设置过程中忽略任何被引发的异常。
从源码结构看,这一"全线程"能力是有实打实代价的。其内部实现 _PyEval_SetProfileAllThreads 位于 legacy_tracing.c:它先调用 _PyEval_StopTheWorld(interp) 暂停整个解释器,加 HEAD_LOCK(&_PyRuntime) 遍历所有线程状态,用 swap_profile_func_arg 逐个替换每个线程的 c_profilefunc,统计 interp->sys_profiling_threads 计数后再统一开启监控事件、恢复世界。也就是说,全线程注册需要一次 stop-the-world 暂停。
公开包装与底层实现
ceval.c 中,公开的 PyEval_SetProfile 只是取当前线程状态后转调内部函数,审计钩子异常仅记录 unraisable 错误(这正是文档所说"忽略异常"的实现):
void
PyEval_SetProfile(Py_tracefunc func, PyObject *arg)
{
PyThreadState *tstate = _PyThreadState_GET();
if (_PyEval_SetProfile(tstate, func, arg) < 0) {
PyErr_FormatUnraisable("Exception ignored in PyEval_SetProfile");
}
}
真正的逻辑在 legacy_tracing.c 中的 _PyEval_SetProfile:先触发 sys.setprofile 审计事件(_PySys_Audit),再用一次性标志注册 PEP 669 监控回调(_PyOnceFlag_CallOnce(..., setup_profile_callbacks, ...)),然后 stop-the-world 内交换 c_profilefunc/c_profileobj 并按 sys_profiling_threads 计数设置全局监控事件掩码(set_monitoring_profile_events)。
可以推断,从源码结构看,当前版本的 C 级 legacy tracing 并非直接挂在解释器主循环里,而是构建在 PEP 669 字节码插桩(instrumentation)之上的一层兼容层:setup_profile_callbacks 将 PEP 669 事件映射回传统 PyTrace_* 事件——PY_MONITORING_EVENT_PY_START/PY_RESUME 映射为 PyTrace_CALL,PY_RETURN/PY_YIELD 映射为 PyTrace_RETURN,PY_UNWIND 也映射为 PyTrace_RETURN(arg 为 NULL),CALL/C_RETURN/C_RAISE 分别映射为 PyTrace_C_CALL/PyTrace_C_RETURN/PyTrace_C_EXCEPTION,映射表见 legacy_tracing.c。这一实现细节解释了为什么剖析函数收不到 PyTrace_EXCEPTION:profile 工具注册的回调集合(PY_MONITORING_SYS_PROFILE_ID)根本没有挂 RAISE 事件。
PyEval_SetTrace:注册追踪函数
void PyEval_SetTrace(Py_tracefunc func, PyObject *obj);
将追踪函数设为 func。与 PyEval_SetProfile 的关键区别是:追踪函数会收到行号事件(PyTrace_LINE)与逐字节码事件(PyTrace_OPCODE),但不会收到任何与 C 函数调用相关的事件——即 what 参数永远不会是 PyTrace_C_CALL、PyTrace_C_EXCEPTION 或 PyTrace_C_RETURN。对应 Python 层函数为 sys.settrace。
Python 3.12 新增的 PyEval_SetTraceAllThreads 与 PyEval_SetProfileAllThreads 行为对称:把追踪函数装到当前解释器的所有运行线程上,且忽略设置过程中引发的异常。
行事件与指令事件在源码中的路径也清晰可查:setup_trace_callbacks 将 PY_MONITORING_EVENT_LINE、PY_MONITORING_EVENT_JUMP 映射为 PyTrace_LINE,将 PY_MONITORING_EVENT_INSTRUCTION 映射为 PyTrace_OPCODE,见 legacy_tracing.c。两个值得注意的实现细节:
- 行事件受
f_trace_lines门控。trace_line(legacy_tracing.c)开头就检查frame->f_trace_lines,为 0 直接返回,与文档"可通过将f_trace_lines置 0 禁用该帧的行事件"一一对应。 - 指令事件按需开关。
call_trace_func与sys_trace_instruction_func会读取frame->f_trace_opcodes:置位时通过_PyEval_SetOpcodeTrace为该帧的 code 对象在PY_MONITORING_SYS_TRACE_ID工具下动态打开PY_MONITORING_EVENT_INSTRUCTION本地事件,关闭时再摘除。该函数在 stop-the-world 暂停中修改监控事件位图(legacy_tracing.c),这正对应文档所述"PyTrace_OPCODE默认不发出,必须显式请求"。
引用追踪接口:PyRefTracer(Python 3.13 新增)
与 Py_tracefunc 面向"执行流事件"不同,参考追踪(Reference tracing)面向对象生命周期:在 Python 对象刚被创建或即将被销毁时通知回调。该接口在 Python 3.13 引入。
回调类型与事件常量
int (*PyRefTracer)(PyObject *obj, int event, void *data);
第一个参数是刚刚创建的对象(event 为 PyRefTracer_CREATE 时)或即将销毁的对象(PyRefTracer_DESTROY 时);data 是注册时提供的不透明指针(opaque pointer)。当新的追踪函数注册并替换现有追踪器时,旧回调会收到一次特殊调用:obj 为 NULL、event 为 PyRefTracer_TRACKER_REMOVED,时机在新函数正式注册之前——这给旧追踪器一个收尾的机会。
头文件中的枚举定义与文档完全一致,见 object.h:
typedef enum {
PyRefTracer_CREATE = 0,
PyRefTracer_DESTROY = 1,
PyRefTracer_TRACKER_REMOVED = 2,
} PyRefTracerEvent;
typedef int (*PyRefTracer)(PyObject *, PyRefTracerEvent event, void *);
PyAPI_FUNC(int) PyRefTracer_SetTracer(PyRefTracer tracer, void *data);
PyAPI_FUNC(PyRefTracer) PyRefTracer_GetTracer(void**);
其中 TRACKER_REMOVED 事件于 Python 3.14 新增。
注册与查询
int PyRefTracer_SetTracer(PyRefTracer tracer, void *data);
注册引用追踪函数。成功返回 0;出错时设置异常并返回 -1。调用时必须有 attached thread state。若已存在旧追踪函数,旧函数会在新函数注册前收到 PyRefTracer_TRACKER_REMOVED 事件。
PyRefTracer PyRefTracer_GetTracer(void **data);
查询当前注册的引用追踪函数及其不透明 data 指针。若没有注册过,返回 NULL 并将 *data 置 NULL。同样要求 attached thread state。
三条必须遵守的约束
文档对追踪函数本体给出了三条硬性限制,实现者也必须逐条对照:
- 追踪函数内部不得创建 Python 对象,否则会产生再入(re-entrant)问题——因为销毁回调正发生在引用计数归零、对象析构的路径上;
- 不得清除当前异常,也不得设置异常;
- 每次调用追踪函数时都会处于一个活动(active)的 thread state 中。
前两条约束在仓库内使用方的实现中有直观体现。标准库 tracemalloc 模块就是 PyRefTracer 的真实消费者:它在 tracemalloc.c 中调用 PyRefTracer_SetTracer(_PyTraceMalloc_TraceRef, NULL) 注册自身,其回调 _PyTraceMalloc_TraceRef 第一步就检查 if (event != PyRefTracer_CREATE) return 0;,随后立刻用 get_reentrant() 判断是否再入——如果当前正处在追踪器自己发起的 Python 对象创建中(如构造 traceback 对象),直接跳过,以此严守"不得创建对象导致再入"的约束。
实现细节:事件从哪里触发
从源码结构看,销毁事件的注入点就在引用计数递减的快路径中。内部头文件 pycore_object.h 里,_Py_DEC_REF 相关路径在对象引用归零时会调用 _PyReftracerTrack(op, PyRefTracer_DESTROY);创建事件则在各对象的初始化路径上对称触发。
注册函数本身的实现见 object.c:PyRefTracer_SetTracer 先做 _Py_AssertHoldsTstate() 断言,然后 _PyEval_StopTheWorldAll(&_PyRuntime) 全运行时暂停;若已有旧追踪器,先回调旧追踪器并传 PyRefTracer_TRACKER_REMOVED(注意此处若旧回调遗留了异常会直接走错误分支返回 -1),最后写入 _PyRuntime.ref_tracer 全局槽位并恢复世界。可以看到该槽位是每运行时全局单例而非每线程,这与执行流追踪的"per-thread"模型形成鲜明对比——同一时刻整个运行时只有一个引用追踪器。
测试侧的用法可参考 _testcapimodule.c,其中演示了 PyRefTracer_SetTracer(_simpletracer, the_data) 的注册、用 PyRefTracer_GetTracer 保存/恢复旧追踪器,以及 PyRefTracer_SetTracer(NULL, NULL) 的注销流程。
与 Python 层接口的对应关系
sys.setprofile / sys.settrace 正是通过同一套内部入口实现的。sysmodule.c 中,sys.setprofile 在解除旧剖析器后调用 _PyEval_SetProfile(tstate, profile_trampoline, function)——即把 Python 可调用对象包进一个 C 层 trampoline,再转调用户函数。这说明 Python 层工具(如 cProfile、trace 模块)走的也是本文所述的 C 级事件通道;C 扩展直接注册 Py_tracefunc 时,则省去了 trampoline 这一层 Python 调用开销。
小结与使用检查清单
- 事件矩阵:剖析函数收
CALL/RETURN/C_CALL/C_EXCEPTION/C_RETURN,不收LINE/OPCODE/EXCEPTION;追踪函数收CALL/RETURN/LINE/OPCODE/EXCEPTION,不收 C 类事件。选择接口时先按所需事件定,再谈性能。 - 返回值约定:
Py_tracefunc返回0成功、-1表示回调引发了异常;PyRefTracer_SetTracer则返回0成功、-1失败并置异常。 - 状态存放:剖析/追踪回调是 per-thread 的,把状态挂在 per-thread 的
obj参数上最稳妥。 - 线程范围:3.12 起如需覆盖全部线程,用
PyEval_SetProfileAllThreads/PyEval_SetTraceAllThreads(会忽略各线程上设置过程的异常,且内部需要 stop-the-world)。 - 引用追踪:3.13 起可用
PyRefTracer观察对象创建/销毁(3.14 起替换时旧追踪器会收到TRACKER_REMOVED);回调内严禁创建对象、严禁触碰异常状态;该接口为每运行时单例。 - 适用前提:以上全部要求调用方持有 attached thread state;
PyEval_*系列属于 Python C API(stable ABI),PyRefTracer系列同样在 stable ABI 中可见(Include/cpython/object.h)。
核心实现文件索引:回调类型与常量定义在 pystate.h,函数声明在 ceval.h 与 object.h,公开入口在 ceval.c,legacy 追踪的插桩映射与线程状态切换在 legacy_tracing.c,sys 模块桥接在 sysmodule.c,引用追踪器槽位与注册逻辑在 object.c,真实使用范例在 tracemalloc.c 与 _testcapimodule.c。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python08
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00