py-spy为什么能追踪到python进程的堆栈?
py-spy 的实现本质是:作为另一个进程,利用操作系统提供的“跨进程读内存”能力,读取目标 Python 进程里的 CPython 运行时结构体,然后把 Python thread/frame 链表还原成调用栈。它不是通过 Python API 注入,也不是在目标进程里 import 一个 profiler。
具体链路:
-
通过 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 控制,不是任意读。 - Linux:
-
找到目标进程里的 CPython Interpreter
py-spy 会读取目标进程的 executable、memory maps、符号表。正常情况下可以通过 CPython 的全局符号找到:- Python 旧版本:
interp_head - Python 新版本:
_PyRuntime
但 ASLR、stripped binary、Windows PDB 缺失会让符号不好找,所以 py-spy 还会扫描 BSS 段,寻找“看起来像合法
PyInterpreterState的地址”。 - Python 旧版本:
-
按 CPython 内部结构体遍历线程和 frame
找到 interpreter 后,py-spy 读取:PyInterpreterStatePyThreadState- 当前 Python frame
- frame 的 previous/back 链
- code object 里的 function name、filename、line number
也就是类似:
PyInterpreterState -> threads: PyThreadState linked list -> current frame -> previous frame -> previous framepy-spy 不是“理解 Python 字节码执行过程”,而是知道 CPython 在内存里如何保存线程状态和 frame stack,然后逐个地址读出来。
-
为什么能兼容不同 Python 版本
CPython ABI/内部结构会随版本变化。py-spy 用 Rust 的bindgen为不同 Python 版本生成对应的结构布局,然后按目标 Python 版本选择正确 layout。官方 README 也提到它支持 CPython 2.3-2.7、3.3-3.14。 -
采样而不是持续跟踪
py-spy 是 sampling profiler:比如每秒 100 次,读一次所有 Python 线程栈,最后聚合成 flamegraph/top/dump。某些常驻的性能洞察工具,就是利用周期性采样 Python host-side 调用栈,用来判断训练进程是不是卡在 dataloader、GC、JIT、Python waiting、framework 调度等位置。
总结:py-spy 通过系统调用来读取目标进程的虚拟地址内容,按照 CPython 内存结构来复原解释器状态、堆栈信息。

浙公网安备 33010602011771号