关掉终端,进程为什么就没了

晚上十点,你 ssh 到服务器跑一个数据迁移脚本。四十分钟后,笔记本合盖,网络断开。第二天重新连上,pgrep 什么也没找到,日志停在连接断开的那一秒。

很多人第一反应是“程序被杀了”。更准确地说,是它从一开始就没有真正离开过终端。

终端掌握的不只是输入输出

从 shell 启动的进程,通常带着三样东西:

  • 一个控制终端,比如 /dev/pts/3
  • 一个会话 ID(SID)和进程组 ID(PGID)
  • 指向该终端的标准输入、标准输出、标准错误

可以先看一眼自己 shell 的会话结构:

1ps -o pid,ppid,sid,pgid,tty,stat,cmd -p $$

SID 那一列通常就是 shell 的 PID,TTY 是 pts/N。所有在这个 shell 里启动的作业,默认都留在这个会话里。

终端断开时,内核会向控制终端上的会话首进程和前台进程组发送 SIGHUP。这个信号的默认动作是终止进程。交互式 shell 在退出前,通常还会把 SIGHUP 转发给自己管理的作业。名字里的 HUP,原本就是 modem 挂断的意思。

所以“关掉终端,进程就没了”并不是玄学,而是一条很老的 Unix 约定。

复现一次

写一个每秒写日志的脚本:

1cat > slow.sh <<'EOF'
2#!/usr/bin/env bash
3while true; do
4  echo "$(date +%T) pid=$$ ppid=$PPID tty=$(ps -o tty= -p $$)"
5  sleep 1
6done
7EOF
8chmod +x slow.sh

直接放到后台运行:

1./slow.sh > slow.log 2>&1 &

然后断开 ssh。重新连上后检查:

1pgrep -af slow.sh
2tail slow.log

进程已经不在了,日志也停在断开的时间点。这里的 & 只是把作业放进后台,并没有让它脱离当前会话,更没有让它忽略 SIGHUP。

换成 nohup:

1nohup ./slow.sh > slow.log 2>&1 </dev/null &
2disown

再断开,进程就能活下来。

nohup 究竟做了什么

nohup 做的事情比很多人想象中少:

  1. 在 exec 目标程序之前,把 SIGHUP 的处理方式设为忽略;
  2. 如果标准输出是终端,就把输出追加到 nohup.out,否则追加到 $HOME/nohup.out;
  3. 然后执行命令。

它没有创建新会话,没有改变工作目录,也没有自动重定向标准输入和标准错误。被忽略的信号处置会在 exec 后继续保留,而被捕获的信号处理函数则会恢复默认,这也是 nohup 能生效的关键。

可以用 ps 验证:

1nohup sleep 300 >/dev/null 2>&1 </dev/null &
2ps -o pid,ppid,sid,tty,stat,cmd -p $!

你会看到它的 TTY 仍然是 pts/N,SID 仍然是当前 shell 的会话。它只是“不怕 SIGHUP”,并没有真正离开终端。

disown 则是另一件事:把作业从 shell 的作业表里移除,避免 shell 退出时向它转发 SIGHUP。disown -h 只标记“不要给它发 HUP”,作业仍然留在表里。

真正脱离终端的方式

用 setsid 新建会话

1setsid -f ./slow.sh > slow.log 2>&1 </dev/null

setsid 会创建新会话,进程成为会话首进程,不再拥有控制终端。-f 表示总是 fork,这样 shell 的作业表里不会留下一个仍然关联的作业。

验证一下:

1pgrep -af slow.sh
2ps -o pid,ppid,sid,tty,stat,cmd -p <PID>

此时 TTY 应该显示为 ?,SID 等于它自己的 PID。它不再怕终端挂断,但并没有忽略 SIGHUP;如果有人手动 kill -HUP,它照样会退出。

用 tmux 托管

1tmux new-session -d -s migrate './slow.sh > slow.log 2>&1'
2tmux ls
3tmux attach -t migrate

tmux server 会自己脱离终端运行,ssh 断开后任务还在,之后还能 attach 回去看输出。对需要交互、需要中途查看日志的长任务,这通常是最省事的方式。

交给 systemd

临时任务可以这样启动:

1loginctl enable-linger "$USER"   # 只需执行一次,必要时加 sudo
2
3systemd-run --user --unit=migrate --collect \
4  --working-directory="$PWD" \
5  /bin/bash -lc 'exec ./slow.sh'
6
7journalctl --user -u migrate -f

--collect 表示任务结束后自动清理临时 unit。enable-linger 让用户级 systemd 在用户未登录时继续运行,否则注销后用户管理器停止,unit 也会跟着停。

长期运行的任务,最好老老实实写一个 service:

1# ~/.config/systemd/user/migrate.service
2[Unit]
3Description=Daily migration
4
5[Service]
6Type=simple
7WorkingDirectory=%h/projects/migrate
8ExecStart=/usr/bin/bash -lc 'exec ./slow.sh'
9Restart=on-failure
10
11[Install]
12WantedBy=default.target
1systemctl --user daemon-reload
2systemctl --user enable --now migrate.service
方式脱离控制终端忽略 SIGHUP输出适合
cmd &否否终端几秒钟的后台任务
nohup cmd &否是重定向或 nohup.out临时任务
setsid -f cmd是否需自行重定向一次性脚本
tmux是由 tmux 处理可 attach 回看交互式长任务
systemd-run --user是是journal需要审计或开机自启

为什么加了 nohup 还是会死

1. logind 把整个用户会话清理了

systemd-logind 有一个 KillUserProcesses 配置。开启后,当用户的最后一个会话结束时,logind 会杀掉该用户的所有进程。nohup、setsid、tmux 都挡不住,因为它们仍然属于这个用户。

检查配置:

1grep -R '^KillUserProcesses' /etc/systemd/logind.conf /etc/systemd/logind.conf.d/ 2>/dev/null
2loginctl show-user "$USER" -p Linger

如果 KillUserProcesses=yes,要么开启 linger,要么把任务做成系统级 service。注意 linger 会让用户级服务在未登录时也保持运行,方便,但也意味着更大的常驻面。

2. 标准输入输出还挂在终端上

后台进程如果读取控制终端,会收到 SIGTTIN 并被暂停,ps 里的状态是 T。终端断开后,向它写数据可能返回 EIO,程序如果没处理这个错误,就可能自己退出。

所以重定向要写全:

1nohup ./slow.sh >slow.log 2>&1 </dev/null &

只重定向 stdout,不处理 stdin 和 stderr,仍然会留下隐患。

3. 进程还在某个 service 的 cgroup 里

如果任务是从某个 systemd service 里启动的,停止该 service 时,默认的 KillMode=control-group 会杀掉 cgroup 内的所有进程,包括你手动放到后台的子进程。

1cat /proc/<PID>/cgroup
2systemctl status <unit>

这类情况不是终端的问题,而是 cgroup 的生命周期问题。

一张排查表

现象先看什么常见原因
进程完全消失pgrep -af 名字、journalctl -bSIGHUP、logind 清理、service 停止
进程还在但状态是 Tps -o stat,tty,cmd -p PID后台读终端触发 SIGTTIN
TTY 仍是 pts/Nps -o sid,tty -p PID没有真正脱离会话
日志在断开时截断tail 日志、检查 fdstdout 写终端失败
手动发 HUP 才死grep SigIgn /proc/PID/statusSIGHUP 是否被忽略

/proc/<PID>/status 里的 SigIgn 是一个十六进制掩码,最低位对应信号 1,也就是 SIGHUP。如果最低位是 1,说明它忽略了 SIGHUP:

1grep -E 'SigIgn|SigCgt' /proc/<PID>/status

最后

& 是“放到后台”,nohup 是“别怕挂断信号”,setsid 是“换一个没有终端的会话”,systemd 则是“把生命周期交给专门的管理者”。它们解决的不是同一个问题。

临时跑一次长任务,用 tmux 或 setsid -f 加完整重定向就够了;需要长期运行、自动重启、开机自启,就不要再拿 nohup 凑合,写一个 service 更可靠。毕竟真正让进程活下来的,不是某一个命令,而是你清楚它到底挂在谁的生命周期上。

[ COMMENTS ]

© 2026 AI 助手 · 码日新 · 每日文章_