在 Python 开发过程中,深入了解程序的运行时行为对于优化性能、排查问题至关重要。本文聚焦于 DTrace 和 SystemTap 这两款强大的监控工具,详细介绍它们在 CPython 中的应用,包括启用静态标记、编写 DTrace 和 SystemTap 脚本、利用可用的静态标记和 Tapsets 等内容,帮助开发者精准剖析 CPython 程序,提升开发效率和程序质量。
目录
一、DTrace 和 SystemTap 简介
二、启用静态标记
(一)macOS 系统
(二)Linux 系统
三、静态 DTrace 探针
四、静态 SystemTap 标记
(一)直接使用静态标记
(二)可用的静态标记
(三)SystemTap Tapsets
五、总结
相关学习资源
一、DTrace 和 SystemTap 简介
DTrace 和 SystemTap 都是功能强大的监控工具,为用户提供了检查计算机系统上进程的有效方式。它们通过特定领域的语言,允许用户编写脚本实现进程监视过滤、数据收集以及生成数据报告等功能。从 Python 3.6 开始,CPython 支持构建带有嵌入式 “标记”(探测器)的版本,借助 DTrace 或 SystemTap 脚本,开发者能够更轻松地监视系统上 CPython 进程的运行状态 。不过需要注意的是,DTrace 标记属于 CPython 解释器的实现细节,在不同 CPython 版本之间,探针兼容性无法得到保证,版本变更时 DTrace 脚本可能会失效。
二、启用静态标记
(一)macOS 系统
macOS 系统内置了对 DTrace 的支持。在 macOS 上,用户可以通过在后台运行 Python 进程,然后使用以下命令列出 Python 程序提供的所有探测器:
$ python3.6 -q& $ sudo dtrace -l -P python$! # 或者:dtrace -l -m python3.6
(二)Linux 系统
在 Linux 系统中,若要使用 SystemTap 的嵌入式标记构建 CPython,首先需要安装 SystemTap 开发工具。可以通过以下命令进行安装:
# 使用yum安装 $ yum install systemtap-sdt-devel # 使用apt-get安装 $ sudo apt-get install systemtap-sdt-dev
安装完成后,在构建 CPython 时需要配置--with-dtrace
选项,例如:
checking for --with-dtrace... yes
构建完成后,可以通过查看二进制文件是否包含.note.stapsdt
部分来验证 SystemTap 静态标记是否存在:
$ readelf -S./python|grep.note.stapsdt
如果 Python 被编译为共享库(使用--enable-shared
配置选项),则需要在共享库内部进行查看,如:
$ readelf -S libpython3.3dm.so.1.0|grep.note.stapsdt
部分较新版本的readelf
命令还可以打印元数据,通过这些元数据能够详细了解 SystemTap 的相关信息,包括如何修补机器码指令以启用跟踪钩子 。
三、静态 DTrace 探针
下面通过一个 DTrace 脚本示例,展示如何显示 Python 脚本的调用 / 返回层次结构,并且仅在调用名为start
的函数内进行跟踪(即导入时的函数调用不会被列出)。
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
通过上述脚本和命令,我们可以清晰地看到函数的调用和返回顺序,以及对应的文件名、函数名和行号,方便开发者分析程序的执行流程 。
四、静态 SystemTap 标记
(一)直接使用静态标记
直接使用静态标记时,需要明确指定包含标记的二进制文件。例如,以下 SystemTap 脚本用于显示 Python 脚本的调用 / 返回层次结构:
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 以及脚本执行时的调用 / 返回层次结构。
如果使用的是 CPython 的--enable-shared
编译版,由于标记包含在libpython
共享库内部,probe 的加点路径需要相应调整。例如,上述脚本中的probe process("python").mark("function__entry") {
应改为probe process("python").library("libpython3.6dm.so.1.0").mark("function__entry") {
(假定为 CPython 3.6 的调试编译版) 。
(二)可用的静态标记
CPython 提供了多个可用的静态标记,方便开发者从不同角度监控程序运行:
标记名称 | 触发时机 | 参数说明 | 用途 |
---|---|---|---|
function__entry(str filename, str funcname, int lineno) |
Python 函数执行开始(仅针对纯 Python 字节码函数) |
$arg1 :文件名(使用user_string($arg1) 访问) $arg2 :函数名(使用user_string($arg2) 访问) $arg3 :行号 |
用于跟踪函数的调用,分析函数执行的起始位置和相关信息 |
function__return(str filename, str funcname, int lineno) |
Python 函数执行结束(通过return 或异常,仅针对纯 Python 字节码函数) |
与function__entry 参数相同 |
用于跟踪函数的返回,分析函数执行的结束位置和相关信息 |
line(str filename, str funcname, int lineno) |
Python 行即将被执行(不会在 C 函数中触发) | 与function__entry 参数相同 |
类似于 Python 分析器逐行追踪,可用于细粒度的代码执行分析 |
gc__start(int generation) |
Python 解释器启动垃圾回收循环时 |
arg0 :要扫描的代(与gc.collect() 中的参数含义相同) |
用于监控垃圾回收机制的启动,分析垃圾回收操作的触发时机和相关参数 |
gc__done(long collected) |
Python 解释器完成垃圾回收循环时 |
arg0 :收集到的对象数量 |
用于监控垃圾回收机制的结束,分析垃圾回收的效果和效率 |
import__find__load__start(str modulename) |
importlib 试图查找并加载模块之前 |
arg0 :模块名称 |
用于跟踪模块导入的开始阶段,分析模块导入的触发原因和相关模块信息 |
import__find__load__done(str modulename, int found) |
importlib 的find_and_load 函数被调用后 |
arg0 :模块名称,arg1 :表示模块是否成功加载 |
用于跟踪模块导入的结束阶段,分析模块导入的结果和相关模块信息 |
audit(str event, void *tuple) |
sys.audit() 或PySys_Audit() 被调用时 |
arg0 :事件名称的 C 字符串,arg1 :指向元组对象的PyObject 指针 |
用于监控系统审计相关的操作,分析程序运行过程中的安全相关事件 |
(三)SystemTap Tapsets
SystemTap 的 Tapsets 是一种更高层次的集成方式,它类似于库,能够隐藏静态标记的一些底层细节,使开发者使用起来更加便捷。例如,以下是一个基于 CPython 非共享构建的 tapset 文件示例:
/* 提供对 function__entry 和 function__return 标记的高级封装 */ 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 字节码函数触发。 -
python.function.return(str filename, str funcname, int lineno, frameptr)
:表示一个 Python 函数的执行已经结束(通过return
或异常),仅针对纯 Python 字节码函数触发。
基于上述 tapset,我们可以编写更简洁的 SystemTap 脚本。例如,以下脚本使用该 tapset 实现跟踪 Python 函数调用层次结构:
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); }
还有另一个脚本,使用该 tapset 提供所有运行中的 CPython 代码的类似top
的视图,显示整个系统中每一秒内前 20 个最频繁进入的字节码帧:
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;
}
五、总结
本文详细介绍了如何使用 DTrace 和 SystemTap 对 CPython 进行检测。通过启用静态标记,开发者可以在不同系统上为 CPython 添加监控能力;利用静态 DTrace 探针和 SystemTap 标记,能够实现对 CPython 程序的函数调用、垃圾回收、模块导入等关键行为的跟踪;而 SystemTap Tapsets 则提供了更高级、更便捷的方式来编写监控脚本。掌握这些工具和技术,开发者可以更深入地了解 CPython 程序的运行时行为,为性能优化、问题排查提供有力支持。
TAG: Python;DTrace;SystemTap;CPython;性能剖析;程序监控
相关学习资源
-
Python 官方文档:使用 DTrace 和 SystemTap 检测 CPython,提供了基础的使用方法和概念。
-
DTrace 官方文档:DTrace 的官方文档,深入了解 DTrace 的语法和功能,有助于编写更复杂的 DTrace 脚本 。
-
SystemTap 官方文档:SystemTap 的官方文档,详细介绍了 SystemTap 的使用方法和特性,是学习 SystemTap 的重要资源 。