首页
/ CPython C API 剖析与追踪机制:PyEval_SetProfile、PyEval_SetTrace 与 PyRefTracer 完全指南

CPython C API 剖析与追踪机制:PyEval_SetProfile、PyEval_SetTrace 与 PyRefTracer 完全指南

2026-09-06 21:52:08作者:盛欣凯Ernestine

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_SetProfilePyEval_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_opcodes1 才会开启。

PyEval_SetProfile:注册剖析函数

void PyEval_SetProfile(Py_tracefunc func, PyObject *obj);

将当前线程的剖析函数设为 funcobj 参数是任意 Python 对象(可为 NULL),会在每次回调中作为第一个参数传入;如果剖析函数需要维护状态,为每个线程传入不同的 obj 值,就是一个方便且线程安全的状态存放位置。剖析函数会收到除 PyTrace_LINEPyTrace_OPCODEPyTrace_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_CALLPY_RETURN/PY_YIELD 映射为 PyTrace_RETURNPY_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_CALLPyTrace_C_EXCEPTIONPyTrace_C_RETURN。对应 Python 层函数为 sys.settrace

Python 3.12 新增的 PyEval_SetTraceAllThreadsPyEval_SetProfileAllThreads 行为对称:把追踪函数装到当前解释器的所有运行线程上,且忽略设置过程中引发的异常。

行事件与指令事件在源码中的路径也清晰可查:setup_trace_callbacksPY_MONITORING_EVENT_LINEPY_MONITORING_EVENT_JUMP 映射为 PyTrace_LINE,将 PY_MONITORING_EVENT_INSTRUCTION 映射为 PyTrace_OPCODE,见 legacy_tracing.c。两个值得注意的实现细节:

  1. 行事件受 f_trace_lines 门控trace_linelegacy_tracing.c)开头就检查 frame->f_trace_lines,为 0 直接返回,与文档"可通过将 f_trace_lines 置 0 禁用该帧的行事件"一一对应。
  2. 指令事件按需开关call_trace_funcsys_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);

第一个参数是刚刚创建的对象(eventPyRefTracer_CREATE 时)或即将销毁的对象(PyRefTracer_DESTROY 时);data 是注册时提供的不透明指针(opaque pointer)。当新的追踪函数注册并替换现有追踪器时,旧回调会收到一次特殊调用:objNULLeventPyRefTracer_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 并将 *dataNULL。同样要求 attached thread state。

三条必须遵守的约束

文档对追踪函数本体给出了三条硬性限制,实现者也必须逐条对照:

  1. 追踪函数内部不得创建 Python 对象,否则会产生再入(re-entrant)问题——因为销毁回调正发生在引用计数归零、对象析构的路径上;
  2. 不得清除当前异常,也不得设置异常
  3. 每次调用追踪函数时都会处于一个活动(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.cPyRefTracer_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.hobject.h,公开入口在 ceval.c,legacy 追踪的插桩映射与线程状态切换在 legacy_tracing.csys 模块桥接在 sysmodule.c,引用追踪器槽位与注册逻辑在 object.c,真实使用范例在 tracemalloc.c_testcapimodule.c

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391