ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

CPython C API 剖析与追踪机制:PyEval_SetProfile、PyEval_SetTrace 与 PyRefTracer 完全指南 CPython C API 剖析与追踪机制PyEval_SetProfile、PyEval_SetTrace 与 PyRefTracer 完全指南【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpythonPython 解释器在 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.settraceC 层注册后每次事件都直接调用 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_NonePyTrace_EXCEPTION异常信息与sys.exc_info()返回的内容一致PyTrace_LINE恒为Py_NonePyTrace_RETURN即将返回给调用者的值若因异常退出则为NULLPyTrace_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.hPyAPI_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_SetProfileAllThreadsPython 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_RETURNarg 为NULLCALL/C_RETURN/C_RAISE分别映射为PyTrace_C_CALL/PyTrace_C_RETURN/PyTrace_C_EXCEPTION映射表见 legacy_tracing.c。这一实现细节解释了为什么剖析函数收不到PyTrace_EXCEPTIONprofile 工具注册的回调集合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_linelegacy_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默认不发出必须显式请求。引用追踪接口PyRefTracerPython 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.htypedef 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.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 statePyEval_*系列属于 Python C APIstable ABIPyRefTracer系列同样在 stable ABI 中可见Include/cpython/object.h。核心实现文件索引回调类型与常量定义在 pystate.h函数声明在 ceval.h 与 object.h公开入口在 ceval.clegacy 追踪的插桩映射与线程状态切换在 legacy_tracing.csys模块桥接在 sysmodule.c引用追踪器槽位与注册逻辑在 object.c真实使用范例在 tracemalloc.c 与 _testcapimodule.c。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表