在一台阿里云 ECS 上掐掉客户端的 ssh 进程,三种起法各自的下场:前台那条停在第 12 行,nohup 加 disown 从 12 行跑到 74 行,放进 tmux 的同样跑到 74 行。
本章只回答一个问题:终端窗口被关掉之后,正在运行的进程会不会因此结束。 四条结论里三条是测量出来的(其中前台那条换了第二台机器才测量到),第四条写得很短,因为没有测量到。
关掉终端窗口时,内核发出的挂断信号
终端窗口关掉时,内核会给终端的前台进程组(在终端前台运行、结束时会一起收到信号的一组进程)发一个挂断信号,很多程序就此结束。程序要不要跟着结束,取决于上级进程(谁把这条命令生出来,术语叫父进程)是谁、进程有没有被从终端的会话里摘出去。
一句话:进程只认上级进程是谁,不认窗口开不开。
关键就在这一句。下面这张图要看的是每一格右边那一句------同一个终端被掐掉,三格的结果并不一样。

图: 三种起法各自的上级进程是谁:屏一前台直跑,进程的父亲是终端会话,窗口关掉时该终端的前台进程组会收到挂断信号,很多程序就此结束,这一格在第二台机器上测量到了------日志停在第 4 行、进程变成僵尸;屏二用 nohup 加 disown 起的进程,父进程是 1 号,原来的 shell 退出之后日志从 13 行涨到 21 行;屏三跑在 tmux 会话里的进程,宿主是 tmux 自身一个不属于任何终端的进程,伪终端被掐断之后会话仍在、日志从 6 行涨到 23 行。图底三条:关掉终端窗口打的是窗口不是进程的父亲;反过来 tmux kill-server 之后所有会话一起没了;断网的情况本章没有测
测量到的第一条:分离出去的进程活着
在一台 Linux 实验机上跑一个每秒钟往日志里写一行的循环,用 nohup 起、再 disown 把循环从当前 shell 的任务表里摘掉,然后让启动循环的 shell 真的退出(这台机器上每条命令都由一个独立 shell 执行、执行完就退出,所以"换一条命令"就等于换了一个 shell)。
结果:日志行数从 13 涨到 21,进程还在,ps -ef 显示循环进程的上级进程编号已经是 1。
bash
13 /data/proof/ticks.log ← 起完之后 6 秒
21 /data/proof/ticks.log ← 换一个 shell、再等 8 秒
root 176 1 ... sh -c for i in $(seq 1 120); do echo "tick $i ..." >> /data/proof/ticks.log; sleep 1; done
↑ ↑
进程号 父进程已经是 1
这一条可以直接搬到你自己的机房里:把一个长活交给 agent 时,别把长活挂在 ssh 进去所用的终端上。
测量到的第二条:tmux 会话也活着,但要先确认掐断的是哪一条连接
同一台机器上建一个分离的 tmux 会话跑同样的循环,然后用一个假终端连进去看一眼,3 秒后掐断假终端(timeout 到点杀掉,退出码 124------这是故意掐断,不是失败)。
结果:会话还在(tmux ls 仍是同一条会话、同一个创建时间),日志从 6 行涨到 23 行;直到执行 tmux kill-server,会话才消失。
所以 tmux 带来的不是"进程变强了",而是多出一层不属于任何终端的宿主 :窗口关掉只是没有人看着 tmux 会话,宿主还在运行。真正需要小心的是反过来的那一步------执行 kill-server 之后,所有会话一起消失。
测量到的第三条:前台直跑那一格,这一轮记录到了现场
前两格在云端那台 Linux 实验机上量。那台机器上没有 sshd、不能被连进来 ,所以"关掉窗口"这一步只能用 script 造的伪终端(由程序模拟出来的终端设备)代替。第三格换了一台能连进来的机器:一台阿里云 ECS(CentOS 8,2 核 2G,sshd 在,tmux 没装),进出的通道是厂商自带的网页终端。做法跟前两格一样------一条每秒钟往日志写一行的循环,这一次前台 运行在 script 造的伪终端里,4 秒之后把伪终端关掉。
bash
$ wc -l /root/amr-proof/fg.log
4 /root/amr-proof/fg.log
$ tail -1 /root/amr-proof/fg.log
fg 4 15:19:55 ← 掐断就在这一秒
三分钟后重连回去查,那行循环进程变成了 [ticker.sh] <defunct>------僵尸,意思是进程已经死了,父进程却卡在原地没有回收已经结束的子进程 ;wc -l 隔 3 秒量两次都还是 4 行。伪终端一旦消失,前台进程就会停下,这一句从前是常识陈述,现在是测量到的。
这一轮还测量出两件附带事实,这两件附带事实决定了后面几格能不能继续写:
- 同一条循环改用
nohup ... &启动,想做对照组,结果没有测成 :连输出文件都没有创建出来。原因是进程在"已经 fork(fork 指创建子进程)、还没有执行到nohup"的那一刻就被挂断信号打掉------伪终端关得太快,nohup还没来得及把"忽略挂断信号"装上。这一条不能读成"nohup 也会死" ;当时nohup能活下来的证据只有第一条结论那台 Linux 实验机上的 13 行涨到 21 行。后面第四条结论补做了这一格 :改成先确认日志已经写出 12 行再掐,nohup正常活过了断连,来源从这一个变成两个。 - 关掉网页终端的标签页,不等于服务器侧的连接断开 :重连之后
who里的pts/0仍然存在,timeout和script两个进程还挂在pts/0上。所以"掉线之后远端的进程会不会死"这一格,从厂商自带的网页终端测不出来,需要用真正的 ssh 客户端掐断连接再量。
测量到的第四条:客户端掉线时,三格各自的结果
第二条附带事实已经说明:厂商自带的网页终端测不了"掉线之后远端的进程会不会死"。这一格补上了------用真正的 ssh 客户端连进去,然后把客户端这一侧的 ssh.exe 进程结束掉 。不碰服务器网络、不碰本机网络配置、不拔网线:ssh.exe 一结束,TCP 连接被重置,服务器上的 sshd 立刻发现会话消失。
三格各跑一遍同样的四步:启动 → 确认日志已经在增长 → 掐断 → 等待 60 秒后重连读回。掐断之前先做一道自检,who 里那条会话必须消失;不消失就说明根本没有断开,那一格作废。三格都通过了这道自检。
| 格子 | 掐之后 60 秒读回 | 断连那 60 秒里写了几行 | 还在跑吗 | 上级进程 | 终端列 |
|---|---|---|---|---|---|
| 前台直跑 | 日志停在第 12 行 | 0 行 | 否 | --- | --- |
nohup + disown |
12 行涨到 74 行 | 64 行 | 是 | 1 | ? |
| tmux 会话 | 12 行涨到 74 行 | 63 行 | 是 | tmux 的服务进程 | pts/2 |
表里 tmux 那一行的 pts/2 是重点:不是被掐断的那条 pts/1,而是 tmux 自己新建的一块伪终端。所以"活下来"不是运气------那行循环从启动那一刻起就没有挂在你的连接上。
第一轮实验作废,原因和原始记录如下。 第一轮用 Git Bash 的 ssh ... & 加 kill -9 $! 去掐断,结果三份日志全都一路增长到实验结束:kill -9 结束的是 Git Bash 里的包装进程,ssh.exe 从未停止,服务器全程没有断过连。那一轮"三格全活"的结果作废。 换成 Windows 自身的进程管理(Start-Process -PassThru 取 PID、Stop-Process 结束)并加上 who 自检之后,才得到上面这张表。写在这里是因为这一条与你的操作无关,是工具的性质:掐断客户端的实验,必须先证明客户端真的已经结束。
仍然没有测量到的那一条:真·断网
上面掐断的是客户端进程 ,TCP 连接会被立即重置,服务器当场发现。拔掉网线、关掉 Wi-Fi、路由器掉电这类"不发重置包"的断法,仍然没有测量过。 那种情况下 sshd 可能要等到自身的超时或者 keepalive 才发现连接消失,前台进程收到挂断信号的时点会更晚------会不会最终也停下来,目前还没有证据。想补这一条,需要在场的两台机器,把其中一边的网络真的掐掉。
另外两件这一轮没有做的:每格只跑了一次 ,没有重复三轮;前台那一格这一次没有留下僵尸进程,而上一轮在同一台机器上测量到过 [ticker.sh] <defunct>,差别可能出在父 shell 有没有来得及回收,没有做对照,所以只报现象、不给解释 。原始日志、进程表和自检输出在 evidence/disconnect-three-arms-2026-10-12.md。
另外一条负面事实
这台 Linux 实验机不能被另一台机器连进来 :sshd 不在盘上(ls 直接报不存在),ss -ltnp 的监听列表里没有 22 端口,也没有 tailscale。所以这台 Linux 实验机能当作"跑命令的实验台",不能当作"你的第二台电脑"。真·断网那一格的实验,在这台机器上永远做不出来。
本章的三块屏
模拟器里有三块屏,分别代表前台直跑、nohup 分离、tmux 会话。剧本在 scenario.json,可核对的运行记录在 reference.jsonl, 哪些话有依据、证据是什么等级,全写在 verify.md 里。国内网络环境下的额外差异见 china.md。
这一章的决策表(由 fusion 的投影器从本章记录生成)
这张表不是手写的:它是 fusion 发布的 review-cli.mjs(v0.1.2)读本节的记录文件 chapters/06-kill-the-terminal/reference.jsonl 投影出来的结果,直接贴在下面。表里每一行是一次拍板: 当时被问到什么、选了什么、之后哪一格的状态变了。同一局的随机种子写在记录文件的头部, 所以这张表可以逐字复现。
复盘:关掉终端窗口,这三块屏谁会死(seed 2519033159,拍板 1 次,22 拍,23714 ms,结束于 未结束)
| # | 第几拍 | 哪一格 | 问到什么 / 派了什么活 | 你选的 | 它引发的跃迁 |
|---|---|---|---|---|---|
| 1 | n=10 | a1 | 你现在要关掉这个终端窗口: | 直接关,先看会发生什么 | blocked→working、working→done、done→idle |
其余 0 条是环境跃迁,不由你的拍板引发。
想自己点这里
这一章的模拟器嵌在我们自己的站点上:xiaomijituan.github.io/agent-machi...
打开就是本章那一局:剧本已经递给它了,点格子、拍板、导出这一局都能做。 掘金会把页面里的交互内容过滤掉,所以这一章在掘金只有图文,能点的那部分在上面那个地址里。 那个站点由本仓库每次合进 main 自动构建,和正文是同一份来源,不是另写的一份演示。
本章剧本全文(复制下来就能在自己的模拟器里玩)
打开你手上的 fusion(离线单文件那份也行),进「剧本库」,把下面这段整段粘进输入框,按「导入」。 导入走的是同一套校验,不合格会当场指出是哪一个字段。
json
{
"schemaVersion": 1,
"id": "amr-ch06-kill-the-terminal",
"version": "0.1",
"name": "关掉终端窗口,这三块屏谁会死",
"hosts": [
{
"id": "lab-linux",
"role": "Linux 实验机",
"place": "同一台机器上三种起法,各自等一句话"
}
],
"tasks": [
{
"key": "foreground-job",
"project": "训练脚本",
"title": "前台直接跑的长活",
"lines": {
"zh": [
"在一块屏的前台跑着一个要两小时的脚本",
"它的父亲就是这个终端会话"
]
},
"question": {
"text": "你现在要关掉这个终端窗口:",
"options": [
{
"id": "just-close",
"label": "直接关,先看会发生什么"
},
{
"id": "ctrl-c-first",
"label": "先手动停掉再关"
},
{
"id": "kill",
"label": "这块屏不管了,关掉"
}
]
}
},
{
"key": "detached-job",
"project": "训练脚本",
"title": "用 nohup 分离出去的长活",
"lines": {
"zh": [
"同一个脚本,用 nohup 起、并且 disown 掉了",
"它的父进程已经变成 1,不再挂在这个终端上"
]
},
"question": {
"text": "关掉这个终端窗口之前,你想知道的是:",
"options": [
{
"id": "check-log",
"label": "关完之后换个 shell 去看日志有没有继续长"
},
{
"id": "think-same",
"label": "觉得反正都一样,不用看"
},
{
"id": "kill",
"label": "这块屏不管了,关掉"
}
]
}
},
{
"key": "tmux-job",
"project": "训练脚本",
"title": "在 tmux 会话里跑的长活",
"lines": {
"zh": [
"脚本跑在一个 tmux 会话里,这个会话是分离创建的",
"你正用一个假装出来的终端看着它"
]
},
"question": {
"text": "你把这个看进去的终端掐断:",
"options": [
{
"id": "reattach",
"label": "过一会儿重新 attach 回去看进度"
},
{
"id": "assume-dead",
"label": "认为会话跟着终端一起没了"
},
{
"id": "kill",
"label": "这块屏不管了,关掉"
}
]
}
}
],
"panes": [
{
"host": "lab-linux",
"task": "foreground-job",
"kind": "claude",
"status": "blocked",
"progress": 30
},
{
"host": "lab-linux",
"task": "detached-job",
"kind": "codex",
"status": "blocked",
"progress": 40
},
{
"host": "lab-linux",
"task": "tmux-job",
"kind": "opencode",
"status": "blocked",
"progress": 50
}
]
}
© 2026 xiaomijituan · 正文 CC BY 4.0,代码与数据 MIT · 原仓库 github.com/xiaomijitua...