- 游戏开发
- 后端
【免费下载链接】kbengine
A MMOG engine of server.
导读
本文基于 KBEngine 仓库内捆绑的 CPython 官方 howto(instrumentation.rst)整理而成,系统讲解如何为 CPython 编译期开启静态 DTrace/SystemTap 探针,如何编写脚本观测 Python 进程的函数调用、逐行执行、垃圾回收与模块导入行为,并结合仓库内真实源码(Include/pydtrace.d、Python/ceval.c、Modules/gcmodule.c、Python/import.c)剖析每个探针的底层触发位置与实现细节。读完本文,你将掌握在 Linux/macOS 上启用探针、验证探针存在、编写 DTrace 与 SystemTap 调用层级追踪脚本、使用 tapset 高层封装实现全系统视角监控的完整实战能力。
背景:DTrace 与 SystemTap 如何观察 CPython
DTrace(macOS/illumos 等系统自带)与 SystemTap(Linux)是两类进程监控工具,各自提供一种领域特定语言(DSL),允许用户编写脚本完成三件事:
- 过滤:筛选要观察哪些进程;
- 采集:从感兴趣的进程中收集数据;
- 报告:对收集到的数据生成汇总报告。
从 CPython 3.6 开始,CPython 可以被编译进一组内嵌的静态标记(static markers,也称 probes/探针),供 DTrace 或 SystemTap 脚本观测,从而显著降低监控 Python 进程行为的门槛。需要特别留意的是(官方impl-detail说明):这些 DTrace 标记属于 CPython 解释器的实现细节,探针的兼容性在 CPython 各版本之间不做任何保证——更换 CPython 版本后,DTrace 脚本可能毫无预警地失效或工作异常。因此在依赖探针做监控/发布时,务必锁定 CPython 版本。
本仓库捆绑的正是 CPython 3.7.3(见 patchlevel.h),本文所有源码行号均以此版本为准。
第一步:启用静态探针
安装依赖
- macOS:系统自带 DTrace 支持,无需额外安装。
- Linux:要构建带 SystemTap 内嵌标记的 CPython,必须安装 SystemTap 开发工具。RedHat/CentOS 系与 Debian/Ubuntu 系的安装命令分别为:
$ yum install systemtap-sdt-devel$ sudo apt-get install systemtap-sdt-dev配置编译
安装依赖后,以--with-dtrace参数运行 CPython 的 configure 脚本。仓库内的configure.ac完整描述了该选项的解析逻辑(见 configure.ac):
- 选项默认关闭(
with_dtrace=no); - 开启后先通过
AC_PATH_PROG在$PATH中查找dtrace命令,找不到则直接报错dtrace command not found on $PATH; - 找到后定义宏
WITH_DTRACE(值为 1),并设定DTRACE_HEADERS="Include/pydtrace_probes.h"; - 随后探测
dtrace -G(ELF 生成标志)是否可用:若可用,说明探针需要显式编译并链接进二进制(如 Linux),会追加DTRACE_OBJS="Python/pydtrace.o";macOS 的 dtrace 缺少-G标志(探针无需显式链接),该文件即不生成。这种"探测-G标志而非按操作系统硬编码"的做法,增强了健壮性。
configure 运行成功时会有如下输出:
checking for --with-dtrace... yesMakefile 中探针相关产物的构建规则也已就位(见 Makefile.pre.in):由dtrace从探针定义文件生成头文件与目标文件——-h生成 C 头文件、-G生成带探针描述的目标文件。
验证探针是否生效
macOS:后台运行一个 Python 进程,列出其提供的全部探针:
$ python3.6 -q & $ sudo dtrace -l -P python$! # 或: dtrace -l -m python3.6ID PROVIDER MODULE FUNCTION NAME 29564 python18035 python3.6 _PyEval_EvalFrameDefault function-entry 29565 python18035 python3.6 dtrace_function_entry function-entry 29566 python18035 python3.6 _PyEval_EvalFrameDefault function-return 29567 python18035 python3.6 dtrace_function_return function-return 29568 python18035 python3.6 collect gc-done 29569 python18035 python3.6 collect gc-start 29570 python18035 python3.6 _PyEval_EvalFrameDefault line 29571 python18035 python3.6 maybe_dtrace_line lineLinux:检查构建出的二进制是否包含.note.stapsdt节(SystemTap 静态标记所在节):
$ readelf -S ./python | grep .note.stapsdt [30] .note.stapsdt NOTE 0000000000000000 00308d78如果以--enable-shared构建(Python 作为共享库),则需要到共享库中查找:
$ readelf -S libpython3.3dm.so.1.0 | grep .note.stapsdt [29] .note.stapsdt NOTE 0000000000000000 00365b68足够新版本的 readelf 还能打印探针的完整元数据:
$ readelf -n ./pythonDisplaying notes found at file offset 0x002d6c30 with length 0x00000144: Owner Data size Description stapsdt 0x00000031 NT_STAPSDT (SystemTap probe descriptors) Provider: python Name: gc__start Location: 0x00000000004371c3, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bf6 Arguments: -4@%ebx stapsdt 0x00000030 NT_STAPSDT (SystemTap probe descriptors) Provider: python Name: gc__done Location: 0x00000000004374e1, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bf8 Arguments: -8@%rax stapsdt 0x00000045 NT_STAPSDT (SystemTap probe descriptors) Provider: python Name: function__entry Location: 0x000000000053db6c, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6be8 Arguments: 8@%rbp 8@%r12 -4@%eax stapsdt 0x00000046 NT_STAPSDT (SystemTap probe descriptors) Provider: python Name: function__return Location: 0x000000000053dba8, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bea Arguments: 8@%rbp 8@%r12 -4@%eax这些元数据告诉 SystemTap:如何在精心布置的机器指令位置打补丁(patch),从而开启追踪钩子。Arguments一栏即探针参数的寄存器/栈位置编码(如8@%rbp表示 8 字节参数位于 rbp 寄存器)。
探针定义:仓库里的 pydtrace.d 与 pydtrace.h
仓库内随 CPython 3.7.3 一起维护了探针的权威定义文件 Include/pydtrace.d,共声明 11 个探针:
provider python { probe function__entry(const char *, const char *, int); probe function__return(const char *, const char *, int); probe instance__new__start(const char *, const char *); probe instance__new__done(const char *, const char *); probe instance__delete__start(const char *, const char *); probe instance__delete__done(const char *, const char *); probe line(const char *, const char *, int); probe gc__start(int); probe gc__done(long); probe import__find__load__start(const char *); probe import__find__load__done(const char *, int); };对应的 Include/pydtrace.h 是 C 侧的统一入口:
- 开启
WITH_DTRACE时,包含由dtrace自动生成的头文件pydtrace_probes.h,其中为每个探针生成PyDTrace_{PROBE}与PyDTrace_{PROBE}_ENABLED()宏; - 调用探针必须先用
ENABLED()宏做守卫检查,以在未开启探针时把性能开销降到最低——这是官方强调的标准用法; - 未开启
WITH_DTRACE时,所有宏编译为空操作(空内联函数、ENABLED()恒返回 0),因此无探针构建对生产性能几乎零影响,这也是可以放心默认关闭该特性的原因。
静态 DTrace 探针实战:追踪函数调用层级
以下 DTrace 脚本展示如何打印 Python 脚本的调用/返回层级,且只在名为start的函数调用期间追踪——也就是说,import 阶段的函数调用不会被列出:
self int indent; python$target:::function-entry /copyinstr(arg1) == "start"/ { self->trace = 1; } python$target:::function-entry /self->trace/ { printf("%d\t%*s:", timestamp, 15, probename); printf("%*s", self->indent, ""); printf("%s:%s:%d\n", basename(copyinstr(arg0)), copyinstr(arg1), arg2); self->indent++; } python$target:::function-return /self->trace/ { self->indent--; printf("%d\t%*s:", timestamp, 15, probename); printf("%*s", self->indent, ""); printf("%s:%s:%d\n", basename(copyinstr(arg0)), copyinstr(arg1), arg2); } python$target:::function-return /copyinstr(arg1) == "start"/ { self->trace = 0; }运行方式:
$ sudo dtrace -q -s call_stack.d -c "python3.6 script.py"输出示例:
156641360502280 function-entry:call_stack.py:start:23 156641360518804 function-entry: call_stack.py:function_1:1 156641360532797 function-entry: call_stack.py:function_3:9 156641360546807 function-return: call_stack.py:function_3:10 156641360563367 function-return: call_stack.py:function_1:2 156641360578365 function-entry: call_stack.py:function_2:5 156641360591757 function-entry: call_stack.py:function_1:1 156641360605556 function-entry: call_stack.py:function_3:9 156641360617482 function-return: call_stack.py:function_3:10 156641360629814 function-return: call_stack.py:function_1:2 156641360642285 function-return: call_stack.py:function_2:6 156641360656770 function-entry: call_stack.py:function_3:9 156641360669707 function-return: call_stack.py:function_3:10 156641360687853 function-entry: call_stack.py:function_4:13 156641360700719 function-return: call_stack.py:function_4:14 156641360719640 function-entry: call_stack.py:function_5:18 156641360732567 function-return: call_stack.py:function_5:21 156641360747370 function-return:call_stack.py:start:28脚本要点解读:
self->trace与self->indent是 DTrace 的线程局部变量,用于限定追踪范围并维护缩进层级;- 谓词
/copyinstr(arg1) == "start"/在进入/离开start函数时打开/关闭追踪开关,实现"只追踪 start 调用期间的层级"; arg0/arg1/arg2分别对应文件名、函数名、行号;copyinstr()用于把用户空间字符串指针拷入内核脚本空间;%*s配合self->indent实现按深度缩进,直观呈现调用树。
静态 SystemTap 标记实战:直接使用底层标记
SystemTap 集成的最低层用法是直接引用静态标记(marker),此时必须显式声明包含这些标记的二进制文件。例如下面这个展示 Python 脚本调用/返回层级的 SystemTap 脚本:
probe process("python").mark("function__entry") { filename = user_string($arg1); funcname = user_string($arg2); lineno = $arg3; printf("%s => %s in %s:%d\n", thread_indent(1), funcname, filename, lineno); } probe process("python").mark("function__return") { filename = user_string($arg1); funcname = user_string($arg2); lineno = $arg3; printf("%s <= %s in %s:%d\n", thread_indent(-1), funcname, filename, lineno); }运行方式:
$ stap \ show-call-hierarchy.stp \ -c "./python test.py"输出示例:
11408 python(8274): => __contains__ in Lib/_abcoll.py:362 11414 python(8274): => __getitem__ in Lib/os.py:425 11418 python(8274): => encode in Lib/os.py:490 11424 python(8274): <= encode in Lib/os.py:493 11428 python(8274): <= __getitem__ in Lib/os.py:426 11433 python(8274): <= __contains__ in Lib/_abcoll.py:366各列含义:
- 时间(微秒,自脚本启动起);
- 可执行文件名;
- 进程 PID;
- 其余部分展示脚本执行过程中的调用/返回层级(
thread_indent负责缩进)。
共享库构建的注意事项:对于--enable-shared构建的 CPython,标记位于 libpython 共享库内,探针的路径需要相应调整。例如上面脚本中的:
probe process("python").mark("function__entry") {应改为(以 CPython 3.6 debug 构建为例):
probe process("python").library("libpython3.6dm.so.1.0").mark("function__entry") {探针清单与参数说明
官方 howto 对每个可用探针做了规格化说明,结合仓库源码可进一步明确其触发位置。所有参数在 SystemTap 侧通过$arg1/$arg2/$arg3访问,在 DTrace 侧通过arg0/arg1/arg2访问。
function__entry(str filename, str funcname, int lineno)
标记一个纯 Python(字节码)函数的执行开始,仅对纯 Python 字节码函数触发,C 函数不会触发。参数含义:
$arg1:(const char *)文件名,用user_string($arg1)读取;$arg2:(const char *)函数名,用user_string($arg2)读取;$arg3:int行号。
源码佐证:在 ceval.c 的PyEval_EvalFrameEx入口处,先判断PyDTrace_FUNCTION_ENTRY_ENABLED(),再调用dtrace_function_entry(f);后者从帧对象的 code 对象取出co_filename、co_name,并用PyCode_Addr2Line换算行号后触发探针(见 ceval.c)。
function__return(str filename, str funcname, int lineno)
与function__entry互为逆操作,标记纯 Python 函数执行结束(无论是return正常返回还是抛异常离开)。参数与function__entry相同。
源码佐证:在 ceval.c 的exit_eval_frame标签处(帧弹出前)触发dtrace_function_return(f),实现见 ceval.c。
line(str filename, str funcname, int lineno)
标记一条 Python 行即将执行,等价于 Python profiler 的逐行追踪(line-by-line tracing),C 函数内部不会触发。参数同上。
源码佐证:主字节码分发循环中,每执行到下一条指令前会检查PyDTrace_LINE_ENABLED()并调用maybe_dtrace_line()(见 ceval.c)。maybe_dtrace_line通过维护指令窗口(instr_lb/instr_ub/instr_prev)判断当前指令是否落在某行的起点或发生回跳,从而精确地在"将要执行某一行"时触发探针(见 ceval.c)。另外注意:快速分发宏FAST_DISPATCH中把PyDTrace_LINE_ENABLED()作为是否走快路径的判据之一(见 ceval.c),说明开启 line 探针会降低字节码分发速度——这是做逐行追踪时需要接受的性能代价。
gc__start(int generation)
Python 解释器开始一轮垃圾回收时触发。arg0是被扫描的代(generation),与gc.collect()的语义一致。
源码佐证:见 gcmodule.c,在collect()开始时触发。
gc__done(long collected)
Python 解释器结束一轮垃圾回收时触发。arg0是本次回收的对象数量。
源码佐证:见 gcmodule.c,在回收统计汇总处触发。
import__find__load__start(str modulename)(3.7 新增)
在importlib尝试查找并加载模块之前触发。arg0是模块名。
源码佐证:见 import.c,位于调用importlib._find_and_load之前。
import__find__load__done(str modulename, int found)(3.7 新增)
在importlib的 find_and_load 调用完成之后触发。arg0是模块名,arg1表示模块是否成功加载(mod != NULL)。
源码佐证:见 import.c,位于_PyObject_CallMethodIdObjArgs返回之后,arg1直接取mod != NULL的布尔值。
SystemTap Tapsets:更高级的封装用法
Tapset 是 SystemTap 的"库"机制,可以隐藏底层静态标记的细节。下面是一份基于非共享构建 CPython 的 tapset 文件,它把function__entry/function__return两个底层标记包装成更友好的探针点:
/* Provide a higher-level wrapping around the function__entry and function__return markers: */ probe python.function.entry = process("python").mark("function__entry") { filename = user_string($arg1); funcname = user_string($arg2); lineno = $arg3; frameptr = $arg4 } probe python.function.return = process("python").mark("function__return") { filename = user_string($arg1); funcname = user_string($arg2); lineno = $arg3; frameptr = $arg4 }将该文件安装到 SystemTap 的 tapset 目录(例如/usr/share/systemtap/tapset)后,以下两个高层探针点即可用:
- python.function.entry(str filename, str funcname, int lineno, frameptr):纯 Python 函数执行开始;
- python.function.return(str filename, str funcname, int lineno, frameptr):纯 Python 函数执行结束(return 或异常)。
实战示例:基于 tapset 的两类监控场景
示例一:干净地追踪调用层级
利用上面的 tapset,无需直接点名底层静态标记即可更优雅地实现调用层级追踪:
probe python.function.entry { printf("%s => %s in %s:%d\n", thread_indent(1), funcname, filename, lineno); } probe python.function.return { printf("%s <= %s in %s:%d\n", thread_indent(-1), funcname, filename, lineno); }示例二:全系统的 top 式热帧视图
下面的脚本利用同一 tapset,提供类似 top 的视图:每秒输出全系统范围内进入次数最多的前 20 个字节码帧(跨所有 Python 进程):
global fn_calls; probe python.function.entry { fn_calls[pid(), filename, funcname, lineno] += 1; } probe timer.ms(1000) { printf("\033[2J\033[1;1H") /* clear screen */ printf("%6s %80s %6s %30s %6s\n", "PID", "FILENAME", "LINE", "FUNCTION", "CALLS") foreach ([pid, filename, funcname, lineno] in fn_calls- limit 20) { printf("%6d %80s %6d %30s %6d\n", pid, filename, lineno, funcname, fn_calls[pid, filename, funcname, lineno]); } delete fn_calls; }要点解读:
fn_calls[pid(), filename, funcname, lineno] += 1以"进程+文件+函数+行号"为键做计数聚合,天然支持多进程全系统统计;timer.ms(1000)每秒触发一次输出并清屏,形成 top 式刷新效果;foreach (... in fn_calls- limit 20)输出计数最高的前 20 项,delete fn_calls在每轮结束后清零计数,实现"每秒窗口"的热点统计。
关键结论与使用建议
- 探针是编译期特性:必须用
--with-dtrace重新配置并构建 CPython 才会内嵌探针;未开启时探针宏为空操作,运行开销几乎为零,可放心用于生产默认构建。 - 版本兼容性无保证:探针属于解释器实现细节,跨 CPython 版本不保证兼容;监控方案应锁定解释器版本(如本仓库的 CPython 3.7.3)。
- 两类工具的选择:macOS 直接用 DTrace;Linux 用 SystemTap(底层 marker 或 tapset 封装),共享库构建时探针路径必须指向
libpython*.so。 - 性能权衡:
line探针会绕过字节码快速分发路径(见 ceval.c),逐行追踪应仅在诊断期开启;函数级探针则建议用ENABLED()守卫语义理解其"可随时关闭"的设计。 - 落地场景:对 KBEngine 这类以 CPython 驱动游戏逻辑的引擎而言,可用
function__entry/return与line探针分析脚本热点、用gc__start/done监控回收压力、用import__find__load__start/done排查启动期模块加载瓶颈——全部通过外挂式脚本完成,无需改动任何 Python 业务代码。
延伸阅读
- 探针权威定义与 C 侧接口:Include/pydtrace.d、Include/pydtrace.h
- 探针触发点源码:函数进出与行追踪 Python/ceval.c,垃圾回收 Modules/gcmodule.c,模块导入 Python/import.c
- 编译期配置逻辑:configure.ac、Makefile.pre.in
- 本文所依据的原始文档:kbe/src/lib/python/Doc/howto/instrumentation.rst
- 游戏开发
- 后端
【免费下载链接】kbengine
A MMOG engine of server.
相关推荐
CPython 动态追踪实战:使用 DTrace 与 SystemTap 探针监控 Python 进程
CPython 动态追踪实战:使用 DTrace 与 SystemTap 探针监控 Python 进程 本文围绕 CPython 官方文档《Instrument
编程语言语言运行时解释器标准库Erlang/OTP 动态追踪指南:DTrace 探针体系、构建配置与 SystemTap 兼容实践
Erlang/OTP 动态追踪指南:DTrace 探针体系、构建配置与 SystemTap 兼容实践 本指南以 Erlang/OTP 官方 HOWTO 文档 H
编程语言语言运行时编译器标准库并发编程KBEngine 内嵌 CPython 源码解读:堆上对象分配 API(Allocating Objects on the Heap)实战指南
KBEngine 内嵌 CPython 源码解读:堆上对象分配 API(Allocating Objects on the Heap)实战指南 本指南以 KBE
游戏开发后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考