nohup 到底挡住了什么
本文最后更新于10 天前,其中的信息可能已经过时,如有错误请发送邮件到fzhuang315@163.com

一台测试机上的同步脚本停了。日志最后一次写入是凌晨两点四十。启动方式是 nohup ./sync.sh &,目录里躺着 nohup.out,文件还在,进程没了。

同事的第一反应是:不是加了 nohup 吗。

问题就在这个「不就行了」上。nohup 挡住的只是所有能让进程死掉的原因里的一个,剩下的它一概不管。搞清楚它管什么、不管什么,比记住命令本身有用。

终端关闭的那一刻,信号走了两级

先建模型。Linux 里跟终端相关的层级有三个:进程、进程组、会话。一个终端对应一个会话,会话里可以挂多个进程组,shell 把每个作业放进一个进程组。

登录 shell  PID 12001  SID 12001  TTY pts/0
├── 作业 A  PGID 12345  ──  sleep 1000   (PID 12345)
└── 作业 B  PGID 12350  ──  vim          (PID 12350)

终端断开时,SIGHUP 不是广播给所有进程的,它走两级传递。

第一级在内核。pty 检测到对端关闭,内核向这个终端的会话首进程发 SIGHUP。通常情况下,那就是你登录的 shell。

第二级在 shell。shell 收到 SIGHUP 之后,会把它转发给自己当前管理的所有作业,后台作业也算在内。

所以「关了终端,后台进程也一起死」,多数情况下不是内核干的,是 shell 替你转发的。这里留一个推论,后面要用:进程如果已经不在任何 shell 的作业表里,就没人给它转发 SIGHUP 了。

nohup 做的事只有一件

nohup 在 exec 之前,把 SIGHUP 的处理方式设成「忽略」,然后启动目标程序。就这一件。

顺带它还顺手收拾了一下文件描述符:stdout 挂在终端上,就追加写到 nohup.out;stderr 挂在终端上,就并到 stdout;stdin 挂在终端上,就把它从终端上摘掉。

那一行熟悉的提示就是这么来的:

$ nohup ./sync.sh &
[1] 12345
nohup: ignoring input and appending output to 'nohup.out'

ignoring input 说的是 stdin,appending output 说的是 stdout。

有个容易被忽略的点:nohup 没有新建会话,也没有让进程脱离控制终端。进程还在原来的会话里,控制终端还是那个 pts,只是它对 SIGHUP 免疫了。

想确认这一点,读 /proc 就可以:

$ cat /proc/12345/status | grep SigIgn
SigIgn: 0000000000000001

SigIgn 是一个 64 位掩码,第 N 号信号对应第 N-1 位。SIGHUP 是 1 号信号,占第 0 位,所以末尾这个 1 的意思就是:SIGHUP 已忽略。

那它挡不住什么

四类情况,nohup 全都无能为力。

SIGTERM 和 SIGKILL。nohup 只忽略 SIGHUP。kill 默认发的是 SIGTERM,照收;kill -9 更是拦不住。脚本被别人的部署流程顺手清掉,nohup 在旁边什么也做不了。

前台进程组收到的 Ctrl+C。终端上的 Ctrl+C 发给的是整个前台进程组,不是某一个进程。nohup ./app 不带 & 时,app 仍在前台进程组里,一个 Ctrl+C 就能把它带走。

机器重启。nohup 和开机自启没有任何关系。

OOM killer。内存不够时内核挑进程杀,用的是 SIGKILL。

顺便把信号本身的规则列清楚,判断起来会快很多:

信号编号常见来源能否被忽略
SIGHUP1终端断开能,nohup 忽略的就是它
SIGINT2Ctrl+C能
SIGQUIT3Ctrl+\能
SIGTERM15kill 默认发这个能
SIGKILL9kill -9不能
SIGSTOP19kill -STOP不能

前四个都可以被进程忽略或捕获。最后两个由内核直接执行,进程没有发言权。

四种比 nohup 更彻底的做法

nohup 是在信号层做文章:让进程不怕 SIGHUP。另一条路是在「谁给它发信号」这一层做文章,让 SIGHUP 根本发不到它。

方式实际动作适合什么情况
nohup忽略 SIGHUP临时跑一次性的活,人还在机器旁边
disown从 shell 作业表摘掉,shell 退出时不再转发进程已经跑起来了,不想重来
setsid新建会话,脱离控制终端不想让进程和终端有任何关系
tmux / screen用一个新的伪终端接住它需要随时回去看、回去敲命令
systemd交给 init 托管,可重启、有日志、能开机自启生产环境

setsid 值得单独说。它把进程放进一个全新的会话,成为该会话的首进程,并且没有控制终端。终端断开时,内核只会给「那个终端对应的会话首进程」发 SIGHUP,而 setsid 出来的进程早就不在那个会话里了,自然收不到。

$ setsid ./sync.sh

disown 适合补救已经在跑的进程:

$ jobs
[1]+  Running    ./sync.sh &
$ disown -h %1

-h 的意思是只标记这一点:shell 退出时不要给它发 SIGHUP。作业仍留在作业表里,fg、bg 照常能用。不加 -h 则是彻底从表里移除,之后引用不到它了。

生产环境的标准答案是 systemd:

[Unit]
Description=Data sync worker
After=network-online.target
​
[Service]
Type=simple
User=ops
WorkingDirectory=/opt/sync
ExecStart=/opt/sync/sync.sh
Restart=on-failure
RestartSec=5
​
[Install]
WantedBy=multi-user.target

Restart=on-failure 会把挂掉的进程拉起来,journald 收走 stdout 和 stderr,开机自启由 WantedBy 负责。这三件事恰好是 nohup 一件都不管的。

没人管的 nohup.out

nohup.out 是磁盘写满的常见来源。nohup 只在 stdout 指向终端时才用它,如果当前目录不可写,会退到 $HOME/nohup.out。实际路径就写在提示行里,很多人没看。

长期跑的服务,显式重定向更稳:

nohup ./sync.sh >> /var/log/sync/sync.log 2>&1 &

用 >> 而不是 >,避免下次启动把上一轮日志冲掉。文件增长交给 logrotate:

/var/log/sync/sync.log {
    daily
    rotate 14
    compress
    missingok
    notifempty
    copytruncate
}

copytruncate 在这里是必要的。进程一直持有这个文件的描述符,不做截断的话,rotate 之后它还会往老 inode 上写。

自查清单

交给 systemd 之前,可以用这几条确认一个 nohup 起来的进程是不是真的稳了。

检查项命令期望结果
有没有控制终端ps -o pid,tty,cmd -p <PID>TT 列是 ? 最稳
SIGHUP 是否被忽略grep SigIgn /proc/<PID>/status第 0 位为 1
关终端后还在不在另开一个会话执行 ps -p <PID>进程还在
重启后还在不在重启机器再看只有 systemd 或 cron @reboot 能保证

最后

回到开头那个脚本。它加了 nohup,也确实没有因为终端断开而死。它死于 SIGTERM,来自一次没人记得的批量清理。

nohup 的承诺只有一条:终端挂了,进程不挂。别的它没承诺过。这个名字起得挺实在。

文末附加内容

评论

  1. 博主
    Windows Edge
    3 天前
    2026-9-26 20:19:36

    你好,世界

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
下一篇