py-spy为什么能追踪到python进程的堆栈?

py-spy 的实现本质是:作为另一个进程,利用操作系统提供的“跨进程读内存”能力,读取目标 Python 进程里的 CPython 运行时结构体,然后把 Python thread/frame 链表还原成调用栈。它不是通过 Python API 注入,也不是在目标进程里 import 一个 profiler。

具体链路:

  1. 通过 OS syscall 读目标进程地址空间

    • Linux:process_vm_readv
    • macOS:vm_read
    • Windows:ReadProcessMemory

    py-spy 官方 README 明确说它就是直接用这些系统调用读 Python 程序内存。Linux 的 process_vm_readv(pid, local_iov, remote_iov, ...) 会把远端进程某段虚拟地址里的 bytes 拷贝到 py-spy 自己进程里;权限由 ptrace access check 控制,不是任意读。

  2. 找到目标进程里的 CPython Interpreter
    py-spy 会读取目标进程的 executable、memory maps、符号表。正常情况下可以通过 CPython 的全局符号找到:

    • Python 旧版本:interp_head
    • Python 新版本:_PyRuntime

    但 ASLR、stripped binary、Windows PDB 缺失会让符号不好找,所以 py-spy 还会扫描 BSS 段,寻找“看起来像合法 PyInterpreterState 的地址”。

  3. 按 CPython 内部结构体遍历线程和 frame
    找到 interpreter 后,py-spy 读取:

    • PyInterpreterState
    • PyThreadState
    • 当前 Python frame
    • frame 的 previous/back 链
    • code object 里的 function name、filename、line number

    也就是类似:

    PyInterpreterState
      -> threads: PyThreadState linked list
         -> current frame
            -> previous frame
               -> previous frame

    py-spy 不是“理解 Python 字节码执行过程”,而是知道 CPython 在内存里如何保存线程状态和 frame stack,然后逐个地址读出来。

  4. 为什么能兼容不同 Python 版本
    CPython ABI/内部结构会随版本变化。py-spy 用 Rust 的 bindgen 为不同 Python 版本生成对应的结构布局,然后按目标 Python 版本选择正确 layout。官方 README 也提到它支持 CPython 2.3-2.7、3.3-3.14。

  5. 采样而不是持续跟踪
    py-spy 是 sampling profiler:比如每秒 100 次,读一次所有 Python 线程栈,最后聚合成 flamegraph/top/dump。某些常驻的性能洞察工具,就是利用周期性采样 Python host-side 调用栈,用来判断训练进程是不是卡在 dataloader、GC、JIT、Python waiting、framework 调度等位置。

总结:py-spy 通过系统调用来读取目标进程的虚拟地址内容,按照 CPython 内存结构来复原解释器状态、堆栈信息。

posted @ 2026-08-05 16:44  战拖者  阅读(0)  评论(0)    收藏  举报