使用 DTrace 和 SystemTap 检测CPython

    David Malcolm

    作者

    Łukasz Langa

    DTrace和SystemTap是监视工具,每个工具都提供了一种检查计算机系统上的进程正在执行的操作的方法。 它们都使用特定于域的语言,允许用户编写以下脚本:

    从Python 3.6开始,CPython可以使用嵌入式“标记”构建,也称为“探测器”,可以通过DTrace或SystemTap脚本观察,从而更容易监视系统上的CPython进程正在做什么。

    CPython implementation detail: DTrace标记是CPython解释器的实现细节。 不保证CPython版本之间的探针兼容性。 更改CPython版本时,DTrace脚本可能会停止工作或无法正常工作而不会发出警告。

    macOS内置了对DTrace的支持。 在Linux上,为了使用SystemTap的嵌入式标记构建CPython,必须安装SystemTap开发工具。

    在Linux机器上,这可以通过:

    或者:

    然后必须将CPython配置为``–with-dtrace``:

    1. checking for --with-dtrace... yes

    在macOS上,您可以通过在后台运行Python进程列出可用的DTrace探测器,并列出Python程序提供的所有探测器:

    1. $ python3.6 -q &
    2. $ sudo dtrace -l -P python$! # or: dtrace -l -m python3.6
    3. ID PROVIDER MODULE FUNCTION NAME
    4. 29564 python18035 python3.6 _PyEval_EvalFrameDefault function-entry
    5. 29565 python18035 python3.6 dtrace_function_entry function-entry
    6. 29566 python18035 python3.6 _PyEval_EvalFrameDefault function-return
    7. 29567 python18035 python3.6 dtrace_function_return function-return
    8. 29568 python18035 python3.6 collect gc-done
    9. 29569 python18035 python3.6 collect gc-start
    10. 29570 python18035 python3.6 _PyEval_EvalFrameDefault line
    11. 29571 python18035 python3.6 maybe_dtrace_line line

    在Linux上,您可以通过查看是否包含“.note.stapsdt”部分来验证构建的二进制文件中是否存在SystemTap静态标记。

    1. $ readelf -S ./python | grep .note.stapsdt
    2. [30] .note.stapsdt NOTE 0000000000000000 00308d78

    如果您已将Python构建为共享库(使用–enable-shared),则需要在共享库中查找。 例如:

    1. $ readelf -S libpython3.3dm.so.1.0 | grep .note.stapsdt
    2. [29] .note.stapsdt NOTE 0000000000000000 00365b68

    足够现代的readelf命令可以打印元数据:

    上述元数据包含SystemTap的信息,描述如何修补策略性放置的机器代码指令以启用SystemTap脚本使用的跟踪钩子。

    静态DTrace探针

    1. self int indent;
    2. python$target:::function-entry
    3. /copyinstr(arg1) == "start"/
    4. {
    5. self->trace = 1;
    6. }
    7. python$target:::function-entry
    8. /self->trace/
    9. {
    10. printf("%d\t%*s:", timestamp, 15, probename);
    11. printf("%*s", self->indent, "");
    12. printf("%s:%s:%d\n", basename(copyinstr(arg0)), copyinstr(arg1), arg2);
    13. self->indent++;
    14. }
    15. python$target:::function-return
    16. /self->trace/
    17. {
    18. self->indent--;
    19. printf("%d\t%*s:", timestamp, 15, probename);
    20. printf("%*s", self->indent, "");
    21. }
    22. python$target:::function-return
    23. /copyinstr(arg1) == "start"/
    24. {
    25. self->trace = 0;
    26. }

    它可以这样调用:

    1. $ sudo dtrace -q -s call_stack.d -c "python3.6 script.py"

    输出结果会像这样:

    1. 156641360502280 function-entry:call_stack.py:start:23
    2. 156641360518804 function-entry: call_stack.py:function_1:1
    3. 156641360546807 function-return: call_stack.py:function_3:10
    4. 156641360563367 function-return: call_stack.py:function_1:2
    5. 156641360578365 function-entry: call_stack.py:function_2:5
    6. 156641360591757 function-entry: call_stack.py:function_1:1
    7. 156641360605556 function-entry: call_stack.py:function_3:9
    8. 156641360617482 function-return: call_stack.py:function_3:10
    9. 156641360629814 function-return: call_stack.py:function_1:2
    10. 156641360642285 function-return: call_stack.py:function_2:6
    11. 156641360656770 function-entry: call_stack.py:function_3:9
    12. 156641360669707 function-return: call_stack.py:function_3:10
    13. 156641360687853 function-entry: call_stack.py:function_4:13
    14. 156641360700719 function-return: call_stack.py:function_4:14
    15. 156641360719640 function-entry: call_stack.py:function_5:18
    16. 156641360732567 function-return: call_stack.py:function_5:21
    17. 156641360747370 function-return:call_stack.py:start:28

    使用 SystemTap 集成的底层方法是直接使用静态标记。 这需要你显式地说明包含它们的二进制文件。

    例如,这个SystemTap脚本可以用来显示Python脚本的调用/返回层次结构:

    1. probe process("python").mark("function__entry") {
    2. filename = user_string($arg1);
    3. funcname = user_string($arg2);
    4. lineno = $arg3;
    5. printf("%s => %s in %s:%d\\n",
    6. thread_indent(1), funcname, filename, lineno);
    7. }
    8. probe process("python").mark("function__return") {
    9. filename = user_string($arg1);
    10. funcname = user_string($arg2);
    11. lineno = $arg3;
    12. printf("%s <= %s in %s:%d\\n",
    13. thread_indent(-1), funcname, filename, lineno);
    14. }

    它可以这样调用:

    1. $ stap \
    2. show-call-hierarchy.stp \
    3. -c "./python test.py"

    输出结果会像这样:

    其中的列是:

    其余部分则表示脚本执行时的调用/返回层次结构。

    对于 –enable-shared 构建的CPython来说,这些标记是包含在libpython共享库中的,探针的点状路径需要反映这一点。比如上面例子中的这一行:

    1. probe process("python").mark("function__entry") {

    应改为:

      (假设是 CPython 3.6 的调试构建)

      可用的静态标记

      function__entry(str filename, str funcname, int lineno)

      这个标记表示一个Python函数的执行已经开始。它只对纯 Python (字节码)函数触发。

      文件名、函数名和行号作为定位参数提供给跟踪脚本,必须使用 $arg1, $arg2, $arg3 访问:

      function__return(str filename, str funcname, int lineno)

      这个标记与 function__entry() 相反,表示Python函数的执行已经结束 (通过 return 或者异常)。 它只对纯Python (字节码) 函数触发。

      参数和 相同

      line(str filename, str funcname, int lineno)

      这个标记表示一个 Python 行即将被执行。它相当于用 Python 分析器逐行追踪。它不会在C函数中触发。

      参数和 function__entry() 相同

      (int generation)

      当Python解释器启动一个垃圾回收循环时被触发。 arg0 是要扫描的生成器,如 。

      gc__done(long collected)

      当Python解释器完成一个垃圾收集循环时被触发。arg0 是收集到的对象的数量。

      使用SystemTap集成的更高层次的方法是使用 “tapset” 。SystemTap 的等效库,它隐藏了静态标记的一些底层细节。

      这里是一个基于 CPython 的非共享构建的 tapset 文件。

      1. /*
      2. Provide a higher-level wrapping around the function__entry and
      3. function__return markers:
      4. */
      5. probe python.function.entry = process("python").mark("function__entry")
      6. {
      7. filename = user_string($arg1);
      8. funcname = user_string($arg2);
      9. lineno = $arg3;
      10. frameptr = $arg4
      11. }
      12. probe python.function.return = process("python").mark("function__return")
      13. {
      14. filename = user_string($arg1);
      15. funcname = user_string($arg2);
      16. lineno = $arg3;
      17. frameptr = $arg4
      18. }

      如果这个文件安装在 SystemTap 的 tapset 目录下(例如``/usr/share/systemtap/tapset`` ),那么这些额外的探测点就会变得可用。

      python.function.entry(str filename, str funcname, int lineno, frameptr)

      This probe point indicates that execution of a Python function has begun. It is only triggered for pure-python (bytecode) functions.

      python.function.return(str filename, str funcname, int lineno, frameptr)

      This probe point is the converse of python.function.return(), and indicates that execution of a Python function has ended (either via return, or via an exception). It is only triggered for pure-python (bytecode) functions.

      例子

      1. probe python.function.entry
      2. {
      3. printf("%s => %s in %s:%d\n",
      4. thread_indent(1), funcname, filename, lineno);
      5. }
      6. probe python.function.return
      7. {
      8. printf("%s <= %s in %s:%d\n",
      9. thread_indent(-1), funcname, filename, lineno);
      10. }

      下面的脚本使用上面的tapset提供了所有运行中的CPython代码的顶部视图,显示了整个系统中每一秒钟最频繁输入的前20个字节码帧。

      1. global fn_calls;
      2. probe python.function.entry
      3. {
      4. fn_calls[pid(), filename, funcname, lineno] += 1;
      5. }
      6. probe timer.ms(1000) {
      7. printf("\033[2J\033[1;1H") /* clear screen */
      8. printf("%6s %80s %6s %30s %6s\n",
      9. "PID", "FILENAME", "LINE", "FUNCTION", "CALLS")
      10. foreach ([pid, filename, funcname, lineno] in fn_calls- limit 20) {
      11. printf("%6d %80s %6d %30s %6d\n",
      12. pid, filename, lineno, funcname,
      13. fn_calls[pid, filename, funcname, lineno]);
      14. }
      15. }