首页
/ CPython Monitoring C API:扩展如何手动触发 sys.monitoring 监控事件

CPython Monitoring C API:扩展如何手动触发 sys.monitoring 监控事件

2026-09-04 12:10:22作者:廉皓灿Ida

本文基于 CPython 官方文档 Doc/c-api/monitoring.rst(Python 3.13 引入),系统讲解 Monitoring C API 的完整用法:PyMonitoringState 状态结构、PyMonitoring_Fire*Event 系列事件触发函数、PyMonitoring_EnterScope/PyMonitoring_ExitScope 作用域管理,以及事件 ID 宏的定义。读完本文,你将掌握如何在 C 扩展中"模拟 Python 代码执行"并向 sys.monitoring 的回调分发事件(例如为 WASM/Pyodide 解释器、字节码虚拟机或测试桩暴露监控点),并能结合源码理解事件分发的底层机制与 DISABLE 优化的实现细节。

一、背景:C 扩展为什么要主动触发监控事件

CPython 3.13 引入的 sys.monitoring 模块为调试器、覆盖率工具、分析器提供了统一的事件订阅体系。当 C 扩展自己"模拟"执行 Python 代码时(文档原文措辞为 "as it emulates the execution of Python code"),它需要把执行中的关键节点——函数开始、行执行、调用、异常抛出等——主动报告给监控系统。

为此,CPython 提供了一组 C API(文档入口见 Doc/c-api/monitoring.rst):

  • 事件触发PyMonitoring_Fire*Event 系列函数,允许扩展手动发出 PY_STARTLINERAISE 等监控事件;
  • 状态管理PyMonitoring_EnterScope / PyMonitoring_ExitScope,用于同步"哪些事件当前是激活的"这一状态。

事件订阅本身仍然通过 Python 层的 sys.monitoring 完成(use_tool_idset_eventsregister_callback),C API 只负责"发射"端。事件定义与回调签名详见 Doc/library/sys.monitoring.rst

需要注意的前提限制:

  1. 仅限 3.13 及以上版本,文档明确标注 "Added in version 3.13";
  2. 非受限 API(Limited API):头文件 Include/cpython/monitoring.h 顶部即注明 "There is currently no limited API for monitoring",且整个头文件被 #ifndef Py_LIMITED_API 包裹(第 3–8 行),只能在完整 C API 下使用;
  3. 异常状态约定(文档原文明确要求):除下文标注"使用当前异常"的函数外,不得在异常处于设置状态时调用任何 monitoring 函数

二、PyMonitoringState:事件激活状态的紧凑表示

文档定义的 PyMonitoringState 类型表示"某一事件类型在某一作用域内的状态":内存由用户(扩展)自行分配,内容则由 monitoring API 函数维护。头文件中的实际定义非常小:

// Include/cpython/monitoring.h (L49-L52)
typedef struct _PyMonitoringState {
    uint8_t active;   // 激活位图:哪些工具订阅了该事件
    uint8_t opaque;
} PyMonitoringState;

从源码实现看,active 是一个 8 位"工具位图"(每个 bit 对应一个 tool ID)。PyMonitoring_EnterScope 会用解释器全局的 interp->monitors.tools[event] 位图填充它(见 Python/instrumentation.cstate_array[i].active = m->tools[event])。所有 Fire 函数在内部会检查这个位图:没有工具订阅时直接返回 0,不产生任何 Python 调用开销——这正是"紧凑信息"设计带来的快速路径。

三、事件触发函数:PyMonitoring_Fire*Event 全表

文档规定:这些函数成功返回 0,出错返回 -1 并设置异常("All of the functions below return 0 on success and -1 (with an exception set) on error")。每个函数接受一个 PyMonitoringState*、一个 codelike(必须是 types.CodeType 实例或模拟它的对象)、一个 int32_t offset 指令偏移,以及事件特有的附加参数。

文档中给出的完整函数签名如下(与 Include/cpython/monitoring.h 中的声明一一对应):

函数 事件 附加参数 签名
PyMonitoring_FirePyStartEvent PY_START (state, codelike, offset)
PyMonitoring_FirePyResumeEvent PY_RESUME (state, codelike, offset)
PyMonitoring_FirePyReturnEvent PY_RETURN 返回值 (state, codelike, offset, retval)
PyMonitoring_FirePyYieldEvent PY_YIELD 返回值 (state, codelike, offset, retval)
PyMonitoring_FireCallEvent CALL 被调对象 + 第一参数 (state, codelike, offset, callable, arg0)
PyMonitoring_FireLineEvent LINE 行号 (state, codelike, offset, lineno)
PyMonitoring_FireJumpEvent JUMP 目标偏移 (state, codelike, offset, target_offset)
PyMonitoring_FireBranchLeftEvent BRANCH_LEFT 目标偏移 (state, codelike, offset, target_offset)
PyMonitoring_FireBranchRightEvent BRANCH_RIGHT 目标偏移 (state, codelike, offset, target_offset)
PyMonitoring_FireCReturnEvent C_RETURN 返回值 (state, codelike, offset, retval)
PyMonitoring_FirePyThrowEvent PY_THROW 当前异常 (state, codelike, offset)
PyMonitoring_FireRaiseEvent RAISE 当前异常 (state, codelike, offset)
PyMonitoring_FireCRaiseEvent C_RAISE 当前异常 (state, codelike, offset)
PyMonitoring_FireReraiseEvent RERAISE 当前异常 (state, codelike, offset)
PyMonitoring_FireExceptionHandledEvent EXCEPTION_HANDLED 当前异常 (state, codelike, offset)
PyMonitoring_FirePyUnwindEvent PY_UNWIND 当前异常 (state, codelike, offset)
PyMonitoring_FireStopIterationEvent STOP_ITERATION 迭代值 (state, codelike, offset, value)

回调实际收到的参数与 Python 端签名一致(文档要求参见 sys.monitoring):例如 PY_START/PY_RESUME 回调收到 (code, instruction_offset)CALL 回调收到 (code, instruction_offset, callable, arg0)LINE 回调收到 (code, line_number);异常类事件回调收到 (code, instruction_offset, exception)。完整签名列表见 Doc/library/sys.monitoring.rst

3.1 底层分发机制:vectorcall 直调回调

Python/instrumentation.ccapi_call_instrumentation 可以看到实现细节:

  • offset 为负时直接报 ValueError("offset must be non-negative");
  • LINE 事件不向回调传 offset,而是把 lineno 装箱为 int 作为第二个参数(对应 Python 端 func(code, line_number) 的签名),其余事件将 offset 装箱传入;
  • 随后按 state->active 位图从最高位到最低位逐个工具通过 _PyObject_VectorcallTstate 向量调用各工具注册的回调,回调返回 sys.monitoring.DISABLE 时,直接对该事件 state->active &= ~(1 << tool)——即"按位置禁用",无需 StopTheWorld(区别于解释器内联插桩路径需要停世界来改写字节码)。

3.2 异常类事件:自动读取"当前异常"

PY_THROWRAISECRaiseRERAISEEXCEPTION_HANDLEDPY_UNWIND 六个函数不接收异常参数,而是内部通过 PyErr_GetRaisedException() 读取当前异常。源码中的 exception_event_setup / exception_event_teardownPython/instrumentation.c)保证:

  • 调用前必须已有异常处于设置状态,否则报 ValueError: "Firing event N with no exception set"
  • 事件分发期间先 PyErr_GetRaisedException 取出异常、分发结束后再 PyErr_SetRaisedException 还原,因此调用前后异常状态保持不变(回调若自身抛错则会替换掉原异常);
  • 对这类事件返回 DISABLE不允许的capi_call_instrumentation 中,非插桩事件返回 DISABLE 会报 ValueError("Cannot disable %s events. Callback removed.")并清除对应回调。Lib/test/test_monitoring.pyTestCApiEventGeneration.CANNOT_DISABLE 集合正是对此的测试佐证。

3.3 两条特殊语义

  • STOP_ITERATION:若 value 本身是 StopIteration 实例则直接使用,否则新建 StopIteration(value)(实现见 _PyMonitoring_FireStopIterationEventPython/instrumentation.c 中先 PyErr_SetObject(PyExc_StopIteration, value) 再走异常事件路径)。
  • CALL 与 C_RETURN/C_RAISE 的绑定关系:与 Python 端 set_events 的约束一致("cannot set C_RETURN or C_RAISE events independently"),源码中 C_CALL_EVENTS 宏将 CALL | C_RETURN | C_RAISE 视为一个整体(Python/instrumentation.c)。C_RETURN/C_RAISE 事件只能随 CALL 一起启用。

四、作用域管理:PyMonitoring_EnterScope / PyMonitoring_ExitScope

文档原文:"Monitoring states can be managed with the help of monitoring scopes. A scope would typically correspond to a Python function."(monitoring 状态可以通过 monitoring 作用域管理,一个作用域通常对应一个 Python 函数。)

4.1 参数详解

int PyMonitoring_EnterScope(
    PyMonitoringState *state_array,   // 用户分配、API 填充的状态数组
    uint64_t *version,                // 用户分配并初始化为 0 的版本号
    const uint8_t *event_types,      // 该作用域内可能触发的事件 ID 数组
    Py_ssize_t length);              // event_types(从而也是 state_array)的长度
  • event_types:事件 ID 数组。ID 的取值规则文档明确给出:PY_START 事件的 ID 是 PY_MONITORING_EVENT_PY_START,其数值等于 sys.monitoring.events.PY_START 的二进制对数——因为 Python 端事件常量按位定义(PY_START = 1 << 0CALL = 1 << 4……,见 instrumentation.cadd_power2_constant1 << i 生成逻辑),所以 ID 就是 int(math.log2(事件常量))Lib/test/test_monitoring.py 的测试里正是用 int(math.log2(event)) 计算该值传入 EnterScopeLib/test/test_monitoring.py)。
  • state_array:与 event_types 等长的状态数组,由用户分配PyMonitoring_EnterScope 负责用各事件的激活位图填充它。
  • version:指针指向的值须由用户与 state_array 一起分配并初始化为 0,之后只允许 PyMonitoring_EnterScope 修改。它实现了一个版本快速路径:若解释器全局版本未变化,EnterScope 直接返回 0,不做任何刷新(Python/instrumentation.cif (global_version(interp) == *version) return 0;)。
  • 作用域语义:文档强调这里的 scope 是词法作用域(函数、类或方法)。每次进入词法作用域都应调用一次 EnterScope;作用域可以重入——模拟递归 Python 函数时可复用同一组 state_arrayversion;而当 code-like 的执行被暂停时(如模拟生成器挂起),需要先退出作用域再重新进入

实现上的细节:PyMonitoring_EnterScope 每次刷新只是把 interp->monitors.tools[event](全局工具位图)拷贝进 state_arrayPyMonitoring_ExitScope 当前是一个直接返回 0 的占位实现(Python/instrumentation.c),调用它主要是保持调用约定与未来的对称性。

4.2 事件 ID 宏完整表

event_types 数组使用的宏(与 Include/cpython/monitoring.h 中的数值定义对应):

数值 对应事件
PY_MONITORING_EVENT_PY_START 0 PY_START
PY_MONITORING_EVENT_PY_RESUME 1 PY_RESUME
PY_MONITORING_EVENT_PY_RETURN 2 PY_RETURN
PY_MONITORING_EVENT_PY_YIELD 3 PY_YIELD
PY_MONITORING_EVENT_CALL 4 CALL
PY_MONITORING_EVENT_LINE 5 LINE
PY_MONITORING_EVENT_INSTRUCTION 6 INSTRUCTION
PY_MONITORING_EVENT_JUMP 7 JUMP
PY_MONITORING_EVENT_BRANCH_LEFT 8 BRANCH_LEFT
PY_MONITORING_EVENT_BRANCH_RIGHT 9 BRANCH_RIGHT
PY_MONITORING_EVENT_STOP_ITERATION 10 STOP_ITERATION
PY_MONITORING_EVENT_RAISE 11 RAISE
PY_MONITORING_EVENT_EXCEPTION_HANDLED 12 EXCEPTION_HANDLED
PY_MONITORING_EVENT_PY_UNWIND 13 PY_UNWIND
PY_MONITORING_EVENT_PY_THROW 14 PY_THROW
PY_MONITORING_EVENT_RERAISE 15 RERAISE
PY_MONITORING_EVENT_C_RETURN 16 C_RETURN
PY_MONITORING_EVENT_C_RAISE 17 C_RAISE

头文件中把 0–10 归为"Local events. These require bytecode instrumentation",11–15 为异常类事件("can now be turned on and disabled on a per code object basis"),16–18 为辅助事件。INSTRUCTION 事件在头文件中有 ID(6),但 C API 没有为其提供 Fire 函数——它面向逐指令插桩,属于纯解释器内部路径。另外,文档表中列出的 PY_MONITORING_EVENT_INSTRUCTION 等 17 个宏即上表内容(头文件还存在第 18 号 PY_MONITORING_EVENT_BRANCH 辅助宏,用于兼容旧的 BRANCH 语义,C API 文档未列出)。

4.3 PY_MONITORING_IS_INSTRUMENTED_EVENT 宏

int PY_MONITORING_IS_INSTRUMENTED_EVENT(uint8_t ev)

返回事件 ID ev 对应的是否为 local event(即 Doc/library/sys.monitoring.rst 中标注为 local 的事件,需要字节码插桩支撑的事件)。头文件实现为 (ev) <= PY_MONITORING_EVENT_STOP_ITERATIONInclude/cpython/monitoring.h)。该宏 3.13 加入,文档标注 3.14 起软弃用(soft-deprecated),扩展代码应避免在新代码中依赖它来判断事件类别。

五、内联快速路径:Fire 函数的双层结构

阅读头文件时会注意到一个容易忽视的细节:PyMonitoring_Fire*EventInclude/cpython/monitoring.h 中是 static inline 函数,真正的实现是带下划线前缀的 _PyMonitoring_Fire*Event

// Include/cpython/monitoring.h
#define _PYMONITORING_IF_ACTIVE(STATE, X)  \
    if ((STATE)->active) { \
        return (X); \
    } \
    else { \
        return 0; \
    }

static inline int
PyMonitoring_FirePyStartEvent(PyMonitoringState *state, PyObject *codelike, int32_t offset)
{
    _PYMONITORING_IF_ACTIVE(
        state,
        _PyMonitoring_FirePyStartEvent(state, codelike, offset));
}

也就是说:如果 EnterScope 填充的 state->active 为 0(没有任何工具订阅该事件),Fire 函数在头文件内联层就返回 0,连解释器内部的参数装箱都不会发生。这让"未启用监控"时的热路径开销几乎为零。同时,各 _PyMonitoring_Fire* 实现内部都有 assert(state->active)——直接调用下划线版本时 active 必须非零。

六、可运行的最小示例与仓库参考实现

仓库中自带一份可直接参考的 C API 用法示例:Modules/_testcapi/monitoring.c。它定义了一个 CodeLike 对象,内部持有 PyMonitoringState 数组和一个 version 字段——正是文档推荐的"状态数组 + 版本号"的用户侧存储方式;并提供 monitoring_enter_scope / monitoring_exit_scope 与全部 fire_event_* 包装函数。核心模式如下:

// 每个 codelike 对象保存自己的状态数组与版本号(用户分配)
typedef struct {
    PyObject_HEAD
    PyMonitoringState *monitoring_states;
    uint64_t version;      // 初始化为 0
    int num_events;
} PyCodeLikeObject;

// 进入作用域:声明本作用域可能触发哪些事件
PyMonitoringState *state = &cl->monitoring_states[offset];
int res = PyMonitoring_FirePyStartEvent(state, codelike, offset);

在 Python 端订阅并验证的完整流程(与 Lib/test/test_monitoring.pyTestCApiEventGeneration 一致):

import sys, sys.monitoring, math
import _testcapi

TOOL = 0
sys.monitoring.use_tool_id(TOOL, "demo.tool")
sys.monitoring.register_callback(TOOL, sys.monitoring.events.PY_START,
                                 lambda code, offset: print("PY_START at", offset))
sys.monitoring.set_events(TOOL, sys.monitoring.events.PY_START)

cl = _testcapi.CodeLike(1)                 # 1 个事件的状态槽
# event ID = int(log2(PY_START 常量)) = PY_MONITORING_EVENT_PY_START = 0
with _testcapi.monitoring_enter_scope(cl, int(math.log2(sys.monitoring.events.PY_START))):
    _testcapi.fire_event_py_start(cl, 0)    # 打印 PY_START at 0

测试用例还覆盖了若干边界行为,扩展开发者可直接参考:

  • test_fire_event:逐一验证 16 种 Fire 函数在事件开启时回调恰好触发 1 次、事件关闭时不触发;
  • test_missing_exception:异常类事件在无异常设置时抛 ValueError("Firing event N with no exception set"),与源码 exception_event_setup 的行为完全对应;
  • test_disable_event:回调返回 DISABLE 后同一 Fire 调用不再重复触发;对 PY_THROW/RAISE/RERAISE/EXCEPTION_HANDLED/PY_UNWIND 则按预期抛 ValueError
  • test_enter_scope_two_events:同一作用域注册两个事件(PY_YIELDPY_UNWIND),验证两个状态的激活位互不影响。

七、实践要点清单

  1. 状态数组与版本号必须由扩展持有:随 codelike/解释器实例一起分配,version 初始为 0;不要在两次 EnterScope 之间手动改动它。
  2. 递归可复用、挂起需重入:模拟递归函数时复用同一 state_array/version;模拟生成器暂停-恢复时,需 ExitScope 后重新 EnterScope(恢复时通常会触发 PY_RESUME 事件)。
  3. 异常事件只在异常上下文中调用:调用前异常必须已设置(PyErr_SetRaisedException 之后),函数保证调用后异常原样保留。
  4. 回调可以返回 sys.monitoring.DISABLE:对 local 事件(active 对应位为插桩事件)会静默禁用该工具在该作用域槽上的事件;对异常类事件则报错并清除回调——扩展不应依赖 DISABLE 用于异常路径。
  5. C_RETURN/C_RAISE 必须随 CALL 启用set_events 对三者的绑定约束在 C 端同样存在,单独触发 C 类事件前先确认 CALL 已激活。
  6. 版本前提:本 API 需 CPython ≥ 3.13;PY_MONITORING_IS_INSTRUMENTED_EVENT 在 3.14 起软弃用;整个 API 不在 Limited API 中,发布到 PyPI 的 stable ABI 扩展无法使用。

八、延伸阅读(仓库内路径)

内容 路径
本文对应的 C API 官方文档 Doc/c-api/monitoring.rst
sys.monitoring Python API 与事件签名 Doc/library/sys.monitoring.rst
头文件:事件 ID 宏、PyMonitoringState、内联 Fire 包装 Include/cpython/monitoring.h
核心实现:capi_call_instrumentationEnterScope/ExitScope、异常事件路径 Python/instrumentation.c
参考实现(CodeLike + fire_event_* 包装) Modules/_testcapi/monitoring.c
C API 事件生成测试(含 DISABLE/异常边界用例) Lib/test/test_monitoring.py
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341