Skip to content

第 5 课:Linux 进程、PID、信号与进程树——为什么 Agent 会“命令结束了却还卡着”

今天解决一个非常典型的 Agent Runtime 问题:

Agent 执行 npm run dev、vite、uvicorn、MCP Server 或后台脚本后,明明命令看起来已经结束,为什么 Codex / OpenCode / Kimi 还一直卡着?

这通常不是模型问题,而是 process tree、pipe、signal、child process 没处理好。

先建立今天的主线:

LLM
 ↓
Shell Tool
 ↓
Agent Runtime
 ↓
启动 shell 进程
 ↓
shell 再启动 child
 ↓
child 还可能启动 grandchild
 ↓
形成 process tree
 ↓
stdout / stderr pipe
 ↓
超时或用户取消
 ↓
Runtime 必须正确终止整个进程树

OpenCode 在 2026 年就出现过非常典型的问题:shell 本身已经退出,但后台子进程继续持有 stdout/stderr 的 pipe,导致 Bash Tool 一直等待 EOF,从而“挂住”。citeturn360578search1turn360578search10 Kimi CLI 也有 detached child process 导致 Shell Tool 无法按预期返回的公开问题。citeturn360578search4


1. PID 是什么

Linux 每个运行中的进程都有一个:

PID
Process ID

例如:

ps

可能看到:

PID    COMMAND
1201   bash
1250   python
1308   node

可以把它理解成:

操作系统给进程分配的编号

例如:

PID 1201 → bash
PID 1250 → python

Agent Runtime 启动:

python app.py

操作系统可能创建:

Agent Runtime
    │
    └── PID 1250 python app.py

Runtime 后面如果想取消任务,至少需要知道:

1250

2. 但只记录 PID 还不够

比如 Agent 执行:

npm run dev

真正可能发生:

Agent Runtime
    │
    └── npm
         │
         └── node
              │
              └── vite

形成:

parent
 ↓
child
 ↓
grandchild

如果 Runtime 只杀掉:

npm

那么:

vite

可能仍然存活。

于是你会遇到:

Agent说已经取消
↓
端口5173仍然占用

甚至下一次 Agent 再运行:

npm run dev

得到:

Address already in use

因此一个成熟 Agent Runtime 需要处理:

process tree

而不是只处理单 PID。


3. PPID

Linux 还有:

PPID
Parent Process ID

例如:

ps -ef

可能看到:

PID   PPID   CMD

1000     1   bash
1200  1000   npm run dev
1205  1200   node vite

于是可以恢复:

bash
 └── npm
      └── node vite

这对排查 Agent 残留进程非常有用。

以后你看到:

端口占用
后台任务未退出
Agent结束后还有python
MCP进程残留

就应该想到:

PID
PPID
process tree

4. ps、pgrep、pstree

Linux/macOS 常见:

ps aux

或者:

ps -ef

找 Node:

ps -ef | grep node

更适合程序使用:

pgrep node

显示进程树:

pstree

可能:

agent───bash───npm───node

这里再次用到上一课的 pipe:

ps -ef | grep node

就是:

ps stdout
   ↓
pipe
   ↓
grep stdin

5. Windows 对应是什么

Windows PowerShell:

Get-Process

找 Node:

Get-Process node

结束:

Stop-Process -Id 1234

Windows 还有:

taskkill /PID 1234 /T /F

其中:

/T

表示终止子进程树。

这正是 Coding Agent Runtime 经常需要的操作。

Codex 在 Windows 上的公开问题甚至暴露过内部 taskkill /T /F /PID ... 的输出意外混进 CLI stdout,污染机器可读协议的问题。换句话说,进程树终止本身就是 Codex Runtime 的真实工程逻辑之一。 citeturn360578search22


6. Linux signal 到底是什么

结束进程不是简单:

kill()

Linux 主要使用:

signal

通知进程发生事件。

最需要掌握三个:

SIGINT
SIGTERM
SIGKILL

先理解意义。

SIGINT:

Interrupt

类似终端:

Ctrl+C

意思近似:

用户希望你停止。

SIGTERM:

Terminate

意思近似:

请正常结束。

程序可以:

保存状态
关闭连接
flush日志
释放资源

再退出。

SIGKILL:

立即强制终止

程序没有机会清理。

所以一个成熟的停止流程通常是:

SIGTERM
 ↓
等待 grace period
 ↓
仍未退出
 ↓
SIGKILL

这套逻辑后面在 Docker、Kubernetes 会原样再次出现。

Docker 官方 docker stop 就是先给容器主进程发送停止信号,默认使用 SIGTERM,等待一段时间后仍未结束再强制终止。citeturn360578search27


7. 为什么不是直接 SIGKILL

例如 Agent 启动一个 Python 服务:

while True:
    process_task()

收到 SIGTERM:

def shutdown():
    save_state()
    close_database()
    flush_logs()

然后正常退出。

如果直接:

SIGKILL

这些清理逻辑都没有机会执行。

后果可能是:

数据库transaction没提交
临时文件未清理
日志没flush
锁文件残留
任务状态不一致

所以:

SIGKILL

应该理解成:

最后的强制手段。


8. 为什么 Agent 的 timeout 实际上很复杂

假设 Agent Tool:

timeout = 30秒

30 秒后 Runtime 需要:

停止任务

最简单的实现:

process.kill()

可能只杀:

shell

但没杀:

child / grandchild

于是:

Tool已经timeout

但实际程序:

仍然运行

这叫:

orphaned process

或者更准确地说,原来的父进程退出后,后代进程仍继续存在。

Codex 2026 年公开的 MCP stdio 进程问题里就讨论了类似现象:如果只终止直接 child,而没有正确处理 process group/session,孙进程可能继续持有 pipe;issue 中提出使用独立 session/process group 并整体 kill/wait 的方向。citeturn360578search15

这已经非常接近 Agent Runtime 源码层的调试了。


9. Process Group

Linux 不只可以:

杀一个 PID

还可以把一组进程归入:

process group

结构近似:

Process Group 5000
│
├── bash
├── npm
├── node
└── vite

然后:

给整个 group 发 signal

而不是:

一个一个猜 PID

这就是为什么 Unix Agent Runtime 经常会使用:

setsid
setpgid
killpg

之类机制。

今天不要求记 API。

只理解:

单PID控制

升级成:

一整个执行任务的进程组控制

即可。

Codex 的一个 2026 年桌面端 issue 中就能看到其 Git worker 路径存在“启动独立 process group/session,并要求 kill process tree”的实际设计。citeturn360578search22


10. 为什么后台程序特别容易把 Agent 卡死

例如:

bash -c "sleep 10 &"

Shell:

bash

可能马上结束。

但:

sleep

仍然运行。

关键问题:

它可能继承:

stdout pipe
stderr pipe

于是结构是:

Agent Runtime
  ↑
  │ 等待pipe EOF
  │
 pipe
  ↑
 sleep

虽然:

bash已经exit

但是:

sleep还持有pipe

所以 Runtime:

收不到EOF

继续等。

OpenCode 2026 年的公开问题正是这个模式:后台 child 继承 stdout/stderr 后,foreground shell 已退出,但 Tool 仍不会结束;在 Windows 上运行 vite、uvicorn、npm run dev 等长寿命进程也出现过同类行为。citeturn360578search10turn360578search16


11. 所以 Agent 不能随便用 &

Bash:

npm run dev &

看起来像:

放到后台

但 Agent Runtime 不一定支持这种用法。

因为:

shell background

和:

Agent-managed background task

不是一回事。

好的 Agent 系统应该提供:

BackgroundTask Tool

或者:

TaskHandle

例如:

start_background()
        ↓
task_id = 123
        ↓
Agent继续做其他工作
        ↓
poll(task_id)
        ↓
read logs
        ↓
stop(task_id)

而不是简单:

command &

OpenCode 社区也有明确需求:希望运行 npm test、docker build、pytest 等长任务时能以 Agent 可管理的后台任务方式执行,而不是整个 Agent 被阻塞。citeturn360578search23


12. 企业 Agent Runtime 应该怎么设计

我们后面自己的 Shell Executor 会逐步从:

subprocess.run(command)

升级成:

ShellExecutor
 │
 ├── command
 ├── cwd
 ├── env
 ├── timeout
 ├── stdin policy
 ├── stdout capture
 ├── stderr capture
 │
 └── ProcessManager
       │
       ├── PID
       ├── process group
       ├── descendants
       ├── SIGTERM
       ├── grace period
       └── SIGKILL

再往上才是:

Agent Tool
 ↓
Approval
 ↓
Sandbox
 ↓
ShellExecutor

这就是企业 Agent Runtime 的基本骨架。


13. PID 1 为什么对 Docker 极其重要

现在提前连接 Docker。

Linux 系统启动后的第一个用户空间进程:

PID 1

非常特殊。

普通 Linux:

PID 1
 ↓
systemd/init
 ↓
其他进程

而一个 Container 中:

PID 1

通常就是你的应用:

PID 1
python app.py

或者:

PID 1
node server.js

Docker 官方文档明确提醒:容器内 PID 1 在 Linux 中有特殊的信号处理行为,如果应用没有显式处理信号,SIGINT/SIGTERM 的行为可能和普通进程不同。citeturn360578search8turn360578search21

所以以后会看到:

docker stop agent

但容器:

迟迟不退出

可能就是:

PID 1没有正确处理SIGTERM

14. PID 1 还有第二个工作:reap child

Unix 子进程退出后,父进程应该:

wait()

回收它的退出状态。

如果没人回收,可能出现:

zombie process

通常显示:

<defunct>

所以容器里一个设计不好的 PID 1:

不会处理signal
+
不会正确reap child

对于 Agent workload 很危险。

因为 Agent 特别爱:

大量启动shell
大量启动编译器
大量启动测试
大量启动MCP
大量启动child process

所以未来我们的 Agent Container 可能采用:

tini

或者正确实现 init/reaping 行为。

今天先记住:

Container PID 1

不是普通进程这么简单。


15. Kimi 的真实 Windows 案例

Kimi CLI 2026 年有一个 PowerShell subprocess bug:

执行任何Shell命令
↓
等待约60秒
↓
The handle is invalid

说明同一个 Agent Tool,在:

Linux subprocess

和:

Windows PowerShell subprocess

底层行为并不一样。citeturn360578search2

这也是为什么企业 Agent Runtime 通常要有:

Unix executor
Windows executor

而不是假设:

subprocess is subprocess

就可以完全跨平台。


16. Codex 的真实进程树故障

2026 年 Codex CLI 还出现过 app-server worker 在并行 command burst 后“静默退出”的公开问题:worker 退出后没有把明确 error event 传播给上层,broker 仍在等待 completion,最终 wrapper 看起来就是“卡住”。该 issue 甚至通过追踪 descendant/process tree 来验证 worker 是否消失。citeturn360578search0

这里暴露的是另一个重要原则:

child process died

不应该变成:

Agent forever waiting

Runtime 应该转化成:

ProcessExitedUnexpectedly
exit_code
signal
last_stdout
last_stderr

再返回 Agent。


17. 今天第一次认识“优雅关闭”

英文:

graceful shutdown

理想流程:

用户按Stop
 ↓
Agent Runtime
 ↓
停止发新任务
 ↓
SIGTERM
 ↓
应用停止接受请求
 ↓
处理完当前任务
 ↓
flush logs
 ↓
关闭DB
 ↓
退出child
 ↓
进程退出

如果:

10秒仍没退出

才:

SIGKILL

后面 Kubernetes 会把这个概念变成:

terminationGracePeriodSeconds

所以今天实际上已经提前学了 K8s 的核心运行机制。


18. 今日 Linux/macOS 实验

执行:

sleep 60 &

然后:

ps -ef | grep sleep

找到 PID。

再:

kill <PID>

默认通常发送:

SIGTERM

检查:

ps -ef | grep sleep

然后重新:

sleep 60 &

找到 PID:

kill -9 <PID>

这次是:

SIGKILL

你今天只需要感受:

kill

和:

kill -9

不是同一层级。

再试:

bash -c 'sleep 30 & echo shell-done'

观察:

shell-done

会马上出来,但:

ps -ef | grep sleep

还能找到子进程。

这就是今天最关键的实验。


Windows PowerShell 实验

启动:

$p = Start-Process powershell `
  -ArgumentList "-Command Start-Sleep 60" `
  -PassThru

查看:

$p.Id

然后:

Get-Process -Id $p.Id

停止:

Stop-Process -Id $p.Id

再观察:

Get-Process -Id $p.Id

这就是:

Process handle / PID

的最基础体验。


今日企业故障案例

现象:

Agent执行:
npm run dev

点击Cancel

UI显示:
Cancelled

但是下一次:
npm run dev

报:
Port 5173 already in use

不要首先判断:

Vite坏了

正确故障树是:

Cancel
 ↓
Shell父进程是否退出?
 ↓
node child是否还存在?
 ↓
process group是否被kill?
 ↓
stdout/stderr pipe是否还被child持有?
 ↓
端口由哪个PID占用?

Linux:

ps -ef | grep node

或者:

lsof -i :5173

Windows:

Get-NetTCPConnection -LocalPort 5173

再:

Get-Process -Id <PID>

如果发现:

Agent取消了
但是node还活着

那么:

模型没有问题

问题在:

Process Manager

和 Codex / OpenCode / Kimi 的源码思路对比

这一课不需要把三个项目完整源码读完,但已经可以理解它们为什么都需要复杂的 shell/process 管理。OpenCode 当前公开 issue 明确暴露了 background child + pipe 的问题;Kimi CLI 有 detached child/PowerShell subprocess 问题;Codex 则有 process-tree termination、MCP child process 和 worker lifecycle 类问题。citeturn360578search10turn360578search4turn360578search15

Claude Code 核心产品不按开源源码分析,但从 Agent SDK / Bash Tool 的行为模型来看,同样必须解决:

process creation
timeout
output capture
permission
termination

这些 Runtime 问题。

所以最终不同 Coding Agent 的 UI 差异很大,但底层都会碰到:

OS process model

今天对应的岗位能力

今天已经进入 Agent Runtime / DevOps / Container Runtime / SRE 的交叉区域:

PID、PPID、process tree、process group、SIGINT、SIGTERM、SIGKILL、graceful shutdown、background process、orphan process、PID 1。

其中最重要的不是命令,而是形成这个判断:

Agent卡住
≠
LLM还在思考

可能是:
process没退出
child没退出
pipe没关闭
signal没送达
Runtime没传播异常

这会极大提升你对 Codex、Claude Code、OpenCode、Kimi Code 的使用信心,因为以后看到它“卡”,你已经有能力区分 模型层故障 和 运行时层故障。

今日检查题

  1. 为什么只 kill Shell 的 PID,npm run dev 启动的 Node 进程仍可能存在?
  2. SIGTERM 和 SIGKILL 最大区别是什么?
  3. 为什么后台 child 进程会导致 Agent Tool 等不到结束?
  4. 为什么 Docker 里的 PID 1 值得特殊关注?
  5. Agent 点击 Cancel 后端口仍被占用,你首先应该检查模型还是 process tree?

参考答案核心是:进程可以形成父子树;SIGTERM允许程序处理并优雅退出而SIGKILL强制终止;child可能继续持有 stdout/stderr pipe;容器 PID 1 有特殊信号/子进程管理责任;端口仍占用应先排查进程树和 Runtime。

下一课进入 Linux 文件系统、用户、权限、rwx、chmod、chown 与 Agent Sandbox:为什么 Agent 明明“看得到文件”,却可能读不了、写不了,以及 Docker/Kubernetes 的 root、非 root 用户和最小权限到底解决什么问题。