Skip to content

Commit 64238af

Browse files
committed
update
1 parent 000ef18 commit 64238af

47 files changed

Lines changed: 2764 additions & 263 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

2026-04-19-python-process-injection.md

Lines changed: 44 additions & 87 deletions
Large diffs are not rendered by default.

2026-05-08-python38-gevent-attach-failure.md

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ tags: [python, gevent, debug]
1414

1515
不同工具的控制面和数据面位置不一样。`cProfile` 基本都在目标进程内部;`py-spy` 的控制面在外部,数据采样也尽量从外部完成;memray 会把采集逻辑放进目标进程内部,再把数据写到外部可分析的文件;peeka 的控制面横跨内外两侧,外部 CLI 负责发命令,目标进程里的 agent 负责接命令和执行观测。
1616

17-
这种差异平在多数情况下并不明显。一旦目标进程使用 gevent,问题就会浮现出来。gevent 通过 monkey patch 把 `socket``threading``time``select` 等标准库对象替换成协程版本。对业务代码来说,使用 gevent 的目的就是使用其协程能力;对诊断工具来说,它可能会破坏控制面和数据面的正常工作
17+
这些位置差异平时不影响使用。一旦目标进程使用 gevent,问题就会浮现出来。gevent 通过 monkey patch 把 `socket``threading``time``select` 等标准库对象替换成协程版本。对业务代码来说,使用 gevent 的目的就是使用其协程能力;对诊断工具来说,它会同时影响控制面和数据面
1818

1919
控制面可能在启动 socket、线程或同步信号时卡住。数据面可能还能给出结果,但结果的含义会变化:函数 wrapper 仍然能观察入口调用,递归 trace 和线程 frame 采样却不一定还能代表完整执行过程。
2020

21-
attach agent 在 gevent 目标进程里的启动超时,就是这种影响最先暴露出来的地方
21+
attach agent 在 gevent 目标进程里的启动超时,是这条链路最先出问题的环节
2222

2323
很多 gevent 应用会在启动早期执行:
2424

@@ -37,7 +37,7 @@ Agent initialization timeout
3737

3838
具体工具可能会把这个超时写成等待某个文件、notify socket 或 IPC 消息超时。传递方式不同,但外部控制端等的都是同一件事:agent 发出「我已经启动完成」的就绪信号。
3939

40-
这个启动超时容易被误读成「注入失败」或者「等待时间太短」。实际更常见的情况是:agent 代码已经在目标进程里开始执行,但在发出就绪信号之前阻塞在启动路径上,或者抛出异常中断;外部控制端只能继续等待,最后表现为初始化超时。
40+
这种超时容易被误读成「注入失败」或者「等待时间太短」。更常见的情况是:agent 代码已经在目标进程里开始执行,但在发出就绪信号之前阻塞在启动路径上,或者抛出异常中断;外部控制端只能继续等待,最后表现为初始化超时。
4141

4242
---
4343

@@ -118,29 +118,29 @@ class MiniAgent:
118118

119119
monkey patch 会改变后续 import 的结果:再 `import socket``import threading` 时,拿到的可能已经不是原始实现。
120120

121-
对业务代码来说,这是 gevent 的能力来源。对 attach agent 来说,这会影响自己的启动路径
121+
业务代码看中的是 gevent 的能力来源attach agent 看到的却是同一套 API 的副作用
122122

123123
最明显的风险是:
124124

125125
```python
126126
accept_started.wait(timeout=10)
127127
```
128128

129-
更隐蔽的风险在
129+
风险还藏在
130130

131131
```python
132132
thread.start()
133133
```
134134

135-
CPython 的 `Thread.start()` 不只是创建底层线程。它还会等待线程内部的 `_started` 事件
135+
CPython 的 `Thread.start()` 分两步:先创建底层线程,再等线程内部的 `_started` 事件返回
136136

137137
```python
138138
def start(self):
139139
_start_new_thread(self._bootstrap, ())
140140
self._started.wait()
141141
```
142142

143-
如果 `threading` 已经被 gevent 改造,这个 wait 可能落到 gevent 的 event 语义里。在某些注入时机下,调用链会变成:
143+
如果 `threading` 已经被 gevent 改造,这个 wait 可能进入 gevent 的 event 语义。在某些注入时机下,调用链会变成:
144144

145145
```text
146146
agent.start()
@@ -150,9 +150,9 @@ agent.start()
150150
-> gevent.exceptions.BlockingSwitchOutError
151151
```
152152

153-
把这条链路展开看,可以更清楚地看到哪一层撞上 gevent、外部诊断工具又看到了什么:
153+
把这条链路展开看,可以更清楚地看到问题出在哪一层、外部诊断工具又看到了什么:
154154

155-
![agent.start() 的调用栈撞上 gevent.event.Event.wait() BlockingSwitchOutError,外部诊断工具最终超时](images/gevent/agent-blocking-stack.png)
155+
![调用栈走到 gevent.event.Event.wait() 时抛 BlockingSwitchOutError,外部诊断工具最终超时](images/gevent/agent-blocking-stack.png)
156156

157157
常见错误是 `BlockingSwitchOutError`[^blocking-switch-out]
158158

@@ -161,7 +161,7 @@ gevent.exceptions.BlockingSwitchOutError:
161161
Impossible to call blocking function in the event loop callback
162162
```
163163

164-
这个异常和 GDB、LLDB、PEP 768[^pep-768] 这些注入方式没有直接关系。agent 已经进入目标进程,只是在执行过程中撞到了 gevent 对阻塞切换的限制。
164+
这个异常和 GDB、LLDB、PEP 768[^pep-768] 这些注入方式没有直接关系。agent 已经进入目标进程,只是在执行过程中触发了 gevent 对阻塞切换的限制。
165165

166166
外部最终只等到:
167167

@@ -289,13 +289,13 @@ def probe_agent(sock_path, timeout=1.0):
289289

290290
就绪信号只表示「agent 已完成基本初始化」。写文件、回连 notify socket、发送 IPC 消息都只是传递方式;agent 是否真的能接收连接并返回响应,交给后续 hello 探测确认。
291291

292-
这样一来,就绪同步由内核 socket backlog 和外部 hello 探测承接,发出就绪信号之前不需要 Python 层 event。
292+
就绪同步因此落在内核 socket backlog 和外部 hello 探测上,发出就绪信号之前不需要 Python 层 event。
293293

294294
---
295295

296296
## 3. 数据面:命令能执行,不代表观测语义完全相同
297297

298-
命令通道稳定以后,问题还没有结束。agent 能接收命令,只说明 socket、后台线程和 hello 探测这些控制面路径已经可用;真正执行 `watch``trace``top` 时,代码会进入函数 wrapper、tracing backend、frame sampling 这些数据面路径。它们依赖的观测机制不同,受 gevent 影响的方式也不同,所以还要继续看数据面:命令能返回结果是一回事,结果能解释到什么程度是另一回事
298+
命令通道稳定以后,问题还没有结束。agent 能接收命令,只说明 socket、后台线程和 hello 探测这些控制面路径已经可用;真正执行 `watch``trace``top` 时,代码会进入函数 wrapper、tracing backend、frame sampling 这些数据面路径。它们依赖的观测机制不同,受 gevent 影响的方式也不同。
299299

300300
### 3.1 不同命令在 gevent 下能看到的范围不同
301301

@@ -313,7 +313,7 @@ def probe_agent(sock_path, timeout=1.0):
313313

314314
`trace``top` 更复杂,因为它们试图观察更深的执行过程。
315315

316-
这时输出仍然可以给,但应该把 backend、观测范围和降级原因一起带出来,避免调用方把降级结果当成完整结果
316+
这时输出仍然可以给,但应该把 backend、观测范围和降级原因一起带出来,让调用方知道这是降级结果,不要按完整结果来解读
317317

318318
---
319319

@@ -325,7 +325,7 @@ def probe_agent(sock_path, timeout=1.0):
325325

326326
![gevent 下 trace 事件流被另一个 greenlet 穿插的示意图](images/gevent/gevent-trace-timeline.png)
327327

328-
从单次 `target()` 的角度看,这棵「调用树」会被别的执行路径穿插,已经不能代表它自己的内部结构。此时更稳的选择是降级成 `wrapper_only`
328+
从单次 `target()` 的角度看,这棵「调用树」会被别的执行路径穿插,已经不能代表它自己的内部结构。与其展开错的,不如降级成 `wrapper_only`
329329

330330
```text
331331
trace target()
@@ -364,7 +364,7 @@ trace target()
364364

365365
这套采样模型在普通线程程序里比较直观。但 gevent 使用 greenlet 调度,多个 greenlet 共享同一个 OS thread。
366366

367-
`sys._current_frames()` 返回的是每个 OS thread 当前正在执行的 frame。它天然更容易看到「现在正在跑的 greenlet」,看不到所有挂起的 greenlet。
367+
`sys._current_frames()` 返回的是每个 OS thread 当前正在执行的 frame。它能看到「现在正在跑的 greenlet」,看不到所有挂起的 greenlet。
368368

369369
![普通线程程序与 gevent 程序下 frame sampling 的覆盖范围对比](images/gevent/top-sampling-blindspot.png)
370370

@@ -389,16 +389,16 @@ trace target()
389389

390390
---
391391

392-
## 4. 总结:控制面先保活,数据面说明边界
392+
## 4. 总结
393393

394-
gevent 带来的问题不是单点故障。它先可能让 attach agent 的控制面卡在 socket、线程、就绪信号这些启动路径上;控制面修好以后,数据面的结果也不能直接按普通线程模型解释。
394+
gevent attach agent 的影响分两个阶段:先是控制面卡在 socket、线程、就绪信号这些启动路径上;控制面修好以后,数据面的结果也不能直接按普通线程模型解释。
395395

396-
处理顺序可以简单分成两步
396+
处理顺序分两步
397397

398-
1. 控制面先退到底层原语,用 `_socket`原始 `_thread.start_new_thread` 和 hello 探测保证 agent 能启动、能通信;
399-
2. 数据面再按命令说明观测范围,`watch``monitor``stack` 保持函数边界观测`trace``top` 在 gevent 下要标出降级和盲区。
398+
1. 控制面先退到底层原语,用 `_socket``_thread.start_new_thread` 和 hello 探测保证 agent 能启动、能通信;
399+
2. 数据面再按命令说明观测范围,`watch``monitor``stack` 只做函数边界观测`trace``top` 在 gevent 下要标出降级和盲区。
400400

401-
外部最先看到的可能只是 `Agent initialization timeout`但它只说明控制面没有完成就绪流程。继续往下排查时,要分清 agent 是不是已经进入目标进程、命令通道能不能建立,以及命令返回的数据还能代表什么
401+
外部最先看到的可能只是 `Agent initialization timeout`但这条信息只说明控制面没走完就绪流程。再往下排查,要分清三件事:agent 是不是已经进入目标进程、命令通道能不能建立、命令返回的数据还能代表什么
402402

403403
[^blocking-switch-out]: `BlockingSwitchOutError`:gevent 在「不允许切换 greenlet 的上下文」里检测到要执行阻塞操作时抛出。gevent 的协作式阻塞函数(patch 后的 `Event.wait()``socket.recv()` 等)在阻塞前需要 switch 到 hub 让别的 greenlet 跑;如果当前栈本来就在 hub 的 callback 里,再 switch 会造成 hub 重入,于是直接报错而不是死锁。详见 gevent API 文档:[`BlockingSwitchOutError`](https://www.gevent.org/api/gevent.hub.html#gevent.hub.BlockingSwitchOutError)
404404
[^pep-768]: [PEP 768: Safe external debugger interface for CPython](https://peps.python.org/pep-0768/)

0 commit comments

Comments
 (0)