ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux 下 nohup 记录日志实战:原理、用法与排坑指南

Linux 下 nohup 记录日志实战:原理、用法与排坑指南

前段时间有朋友从某家公司的技术分享会上带回来一个问题:他们在生产环境启动一个数据同步脚本,终端一关,进程就没了,日志也找不到了。后来排查下来,问题就出在启动命令上——没有用 nohup,也没有把输出写到文件里。这话题说起来基础,但很多人真到了要用的时候,会在一堆细节上栽跟头。今天就把“Linux 里用 nohup 记录日志”这件事从头到尾拆一遍,从原理到实操,再到排坑,一次性说透。

1. 先搞清楚 nohup 到底在解决什么问题

1.1 为什么终端一关,进程就跟着退

很多人第一次遇到进程“莫名其妙消失”,第一反应是程序崩溃了,其实大多数时候不是。你从 SSH 登进服务器,敲下./run.sh,这个脚本就成了当前终端会话里的一个前台进程。终端窗口关闭时,系统会向这个会话的进程组发送挂断信号,进程收到的信号如果没做特殊处理,默认动作就是终止退出。

在 Linux 下,终端关闭导致进程退出的链路很清晰:终端会话结束时,内核向会话首进程发送 SIGHUP 信号,前台进程组里的所有进程都会收到这个信号。信号名字里的 HUP 就是 hang up 的缩写,字面意思就是“挂断电话”。可以这么理解:终端是电话线,进程是正在通话的人,电话线断了,通话里的人默认就不再继续说话了。

nohup 这个词也是 no hang up 的缩写,它的作用就是让进程忽略 SIGHUP 信号。信号被忽略后,终端断了,进程照样跑,日志照样写,这就是它解决的核心问题。

1.2 “后台运行”并不等于“断开终端还能跑”

新手常犯的一个混淆,是以为加上&把命令放到后台就万事大吉。&的作用只是把进程放到后台调度,让当前 shell 不用等它结束,可以继续敲别的命令。但如果终端会话关闭,SIGHUP 照样会发给这个后台进程,没有 nohup 保护的话,它一样会退出。

nohup和&是一对常见组合:nohup 负责“忽略挂断信号”,&负责“放到后台执行”。单独用任何一个都不完整,前者不放到后台的话会占住终端,后者不忽略挂断信号的话关终端还是会死。这也是很多脚本写nohup command &的原因,两个行为同时生效,进程才能真正“脱离开终端”跑。

1.3 进程的输出默认去了哪里

进程内部有三个标准数据通道:标准输入、标准输出、标准错误。终端里直接跑一条命令时,标准输出和标准错误都指向屏幕,所以你看到的信息全是“打印在终端上”的。终端关了,屏幕没了,这些输出也就丢了。

nohup 命令在这里做了一件很贴心的事:如果启动命令时没有手动重定向标准输出,nohup 会把标准输出自动写到当前目录下一个叫nohup.out的文件里,同时把标准错误也一并追加进去。启动时屏幕上出现的那行提示nohup: ignoring input and appending output to 'nohup.out',就是在告诉你:输入被忽略,输出进了 nohup.out。

这就是日志记录的最初形态,也是很多人第一次接触 nohup 时看到的“自动日志文件”。但自动归自动,实际生产环境里直接把日志丢给 nohup.out 并不是好做法,原因后面细说。

2. nohup 日志记录的核心用法与重定向细节

2.1 基础命令格式

命令格式其实很简单:

nohup 命令 [参数] [重定向] &

不写重定向时,输出默认进 nohup.out。但这里有第一个坑:nohup.out 的位置取决于你启动命令时所在的目录。你在/home/user/下启动,它就写在/home/user/nohup.out,在/tmp/下启动就写在/tmp/下。如果不注意当前目录,后面找半天日志找不到,这种情况我真见过不少。

所以规范做法是显式指定输出文件,而不是依赖默认的 nohup.out:

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

这条命令做了三件事:忽略挂断信号、把标准输出写到指定日志文件、把标准错误也指向同一个文件,然后放到后台执行。日志路径完全可控,后面查日志、做轮转都方便。

2.2 标准错误和标准输出为什么要同时重定向

很多人不理解2>&1是什么含义,简单拆一下:

  • 1代表标准输出,默认指向屏幕
  • 2代表标准错误,默认也指向屏幕
  • >是重定向符号,> 文件表示把标准输出写入文件
  • 2>&1的意思是:把标准错误重定向到“当前标准输出所指向的地方”

所以> app.log 2>&1合起来的效果就是:标准输出写进 app.log,标准错误也写进 app.log。两条通道合并到同一个日志文件里。

为什么非要写2>&1?因为很多脚本调试时讲不清错误到底去了哪。你不重定向标准错误的话,程序报错信息仍然打到屏幕上,而屏幕随终端关闭后这些内容就彻底丢了。日志文件里只看到正常的输出,查不到异常堆栈,排查问题的时候非常被动。

这里有个细节不少人都理解反了:2>&1的写法是固定的,两个符号的先后顺序不能随便改。写成2>&1 > app.log和写成> app.log 2>&1结果完全不同。前者的顺序是先把标准错误指向“当前的的标准输出”(此时还是屏幕),再把标准输出重定向到文件,最终结果变成:错误打到屏幕,输出写进文件,并没有达到合并日志的目的。正确写法一定是把2>&1放在>之后。

2.3 追加还是覆盖:日志不能“每次启动都从头开始”

重定向符号>是覆盖写入,每次启动都会把旧日志清空重写。这在有些场景下没问题,但做长期任务监控时,历史日志是有价值的,建议用追加写入:

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

>>表示以追加方式写入,旧日志不会丢。这个区别特别适合那种“会多次启动”的任务,比如每天跑一次的定时脚本。覆盖写入会把前一天的关键记录冲掉,查问题时缺了上下文,很麻烦。

另外要区分一个场景:如果脚本自己内部已经处理了日志逻辑,比如 Java 程序通过 log4j 写文件,Python 脚本用 logging 模块落盘,那外面 nohup 的重定向主要接管的是程序“额外打出来的那部分输出”。这种情况下更要注意先确认程序本身的日志路径在哪,别把 nohup 的日志文件当成唯一日志来源。

2.4 有些人把输出丢进 /dev/null,什么时候可以这么干

有一种写法也很常见:

nohup ./daemon.sh > /dev/null 2>&1 &

/dev/null是 Linux 里的“黑洞设备”,写到里面的数据直接丢弃。有些系统服务进程本身已经把日志写到自己的文件里了,外部输出没有保留价值,这么写能让环境干净些,也避免 nohup.out 无意义增长。

什么情况下适合丢黑洞?比如进程本身已经用专业日志库记录日志、而且程序稳定运行不需要靠标准输出辅助排查时。什么情况下别丢?比如脚本没做内部日志、全靠输出定位问题的时候,把输出丢进黑洞等于把故障线索全丢了。

给个个人建议:拿不准的时候就保留日志。磁盘空间通常不是问题,日志没了才是真问题。

3. 完整实操:启动后台任务并管理日志

3.1 准备一个可运行的实验脚本

为了把整个流程走通,这里用一个简单的模拟脚本演示。假设脚本内容如下:

#!/bin/bash echo "脚本启动,时间:$(date)" for i in $(seq 1 100); do echo "第 ${i} 次循环:$(date)" sleep 2 done echo "脚本结束,时间:$(date)"

脚本每两秒打印一行记录,总共跑 200 秒左右,足够演示日志的实时写入和后台运行。实际生产中可以替换成你自己的同步脚本、数据分析任务或者应用启动命令。

3.2 使用 nohup 启动并确认日志文件

进入脚本所在目录,执行:

cd /opt/demo/ nohup bash test.sh > /opt/demo/test.log 2>&1 &

执行后会返回一个进程号,比如[1] 12345。稍等几秒,查看日志文件内容:

tail -f /opt/demo/test.log

你会看到日志按两秒一行的频率持续刷新。这里tail -f是实时跟踪文件内容的命令,日志有任何新行都会立刻显示出来,排查任务是否正常运行非常高效。

同时用进程管理命令确认任务还活着:

ps -ef | grep test.sh | grep -v grep

能看到 PID 为 12345 的 bash test.sh 进程还在运行,就说明 nohup 生效了。

3.3 验证终端断开后进程仍然存活并写日志

这一步建议实际操作一下。你从电脑 SSH 登录服务器,启动上面的 nohup 任务后,直接退出 SSH 会话,过几分钟再重新登录。查两件事:进程在不在,日志有没有继续更新。

ps -ef | grep test.sh | grep -v grep tail -n 20 /opt/demo/test.log

进程没消失、日志时间戳在你退出期间仍在持续前进,就说明进程彻底脱离了终端会话独立运行。这套验证方式同样适用于生产环境,属于“上线后必做的自检”。

3.4 记录 PID 并掌握停进程的正确方式

nohup 启动的任务没有挂在某个终端上,不能用 Ctrl+C 直接终止(这个后面详聊)。停进程的方式是通过 PID 发信号:

kill 12345

如果进程有业务逻辑需要优雅退出,可以先尝试发 TERM 信号让它自己清理,再升级到强制结束信号:

kill -9 12345

生产环境里建议把启动命令写成一个 start.sh,把进程 PID 记录下来:

#!/bin/bash nohup java -jar app.jar > app.log 2>&1 & echo $! > app.pid

$!在 shell 里表示“最近一个后台进程的 PID”,写进 pid 文件后,后续 stop 脚本可以这样读:

kill $(cat app.pid)

这套“启动脚本记 PID + 停止脚本读 PID”的模式,比每次临时找进程号靠谱得多,尤其适合多实例部署的环境。

3.5 日志文件拿不到内容或者打不开时的先手排查

有时候 nohup 启动后,日志文件迟迟不出现,或者文件存在但里面是空的。排查顺序是这样的:

第一,先确认命令本身有没有执行成功。如果命令路径写错、脚本没执行权限,是不会生成日志的。再强调一遍:nohup: ignoring input and appending output to 'nohup.out'出现,说明 nohup 已经启动,但它只能接管“启动成功的进程”;命令本身就报错的话,日志自然没有内容。

第二,检查当前目录。你没显式重定向时默认写 nohup.out,这个文件生成在“启动命令所在的目录”,不是“脚本所在的目录”。这两个目录不一定一样,命令行里cd到哪,nohup.out 就建在哪。

第三,日志文件存在但内容不更新,多半是程序输出有缓冲。这个问题下一节详细展开,因为它是非常高频的坑。

3.6 用定时轮转避免日志文件无限膨胀

日志文件无脑增长是后台任务长期运行的另一个隐患。写满磁盘后程序可能就无法正常写日志,甚至直接影响业务。Linux 自带的 logrotate 工具可以做按大小或按天轮转,例如每天切割保留 7 份:

/opt/demo/test.log { daily rotate 7 compress delaycompress missingok notifempty }

把这段配置放到/etc/logrotate.d/demo下,logrotate 会每天自动把 test.log 改名成 test.log.1,再生成一个新的 test.log。进程写入使用的是文件描述符,文件被移动之后,新写入会继续追加到新创建的 test.log 里,这个机制和 Java 的 logback、Python 的 TimedRotatingFileHandler 思路一致。

没有 logrotate 的临时方案可以手动截断:

> /opt/demo/test.log

这条命令能把文件内容清空但文件本身不动,进程写入不中断。注意别用rm,文件被删后进程持有的句柄仍然在写那块“已删除的 inode”,磁盘空间不会释放,而且会越写越暗黑。

4. 日志记录常见问题与排查技巧实录

4.1 高频故障速查表

整理一份问题排查表,遇到类似情况可以直接对着查:

现象可能原因解决办法
启动提示nohup: ignoring input and appending output to 'nohup.out'未显式重定向,日志在 nohup.out启动时加> 自定义日志 2>&1
日志文件根本没生成脚本本身启动失败或当前目录不对先手动运行脚本看报错,再核对路径
日志文件生成但不更新程序输出有缓冲改用stdbuf -oL或程序内 flush
关掉终端后进程消失用了&没用 nohup,或 nohup 未生效确认启动命令以 nohup 开头
日志写满磁盘文件未轮转配置 logrotate 或定时截断
标准错误没写进日志重定向缺少2>&1改为> 文件 2>&1
进程还在但日志不追加文件被删除但句柄仍在写重启进程或避免 rm 日志文件

这几类问题覆盖了我这些年遇到的大部分 nohup 日志相关故障,每条都有现实案例对应。

4.2 日志不实时刷新:程序输出缓冲问题

这是最有迷惑性的一个问题。线上跑了一个 Python/Java/C 程序,nohup 命令启动没问题,进程也在,但tail -f看不到输出,隔了很久才突然刷出来一大段。

原因是程序自身有输出缓冲。很多语言在输出到“文件”或“管道”时,默认开启全缓冲而不是行缓冲。全缓冲意味着缓冲区没满或者程序没正常退出时,内容不会及时落盘。终端上的行缓冲则是一遇到换行就输出,所以你在前台跑时看到的是实时打印,一重定向到文件就变“堵塞”了。

解决办法分语言看:

  • Python 脚本:启动时加-u参数,python -u script.py可以强制无缓冲输出
  • Java 程序:日志框架通常会定时刷盘,不用额外处理;如果是 System.out,可以用System.out.flush()
  • C 程序:用stdbuf -oL命令改变缓冲策略,stdbuf -oL ./a.out表示按行输出

实际生产里,如果程序有自己的日志系统(log4j、logback、logging 模块),nohup 捕获的输出只是辅助手段,缓冲问题影响有限。但如果程序只靠标准输出打日志,这个坑必须提前处理好。

4.3 前台启动的 nohup 为什么还能被 Ctrl+C 打断

有人试过不加&直接跑nohup command,发现 Ctrl+C 还能把进程结束,于是怀疑 nohup 没生效。其实是理解偏差:nohup 忽略的是挂断信号,不是中断信号。终端按 Ctrl+C 发送的是 INT 信号,nohup 不负责处理这个。

要真正让进程脱离终端又不被打断,必须nohup command &组合使用。只 nopause 不加&,进程仍然占用当前终端;你一旦关掉终端,SIGHUP 会被忽略,进程活下来,但你在终端里按 Ctrl+C 它就没了。

这个区别在生产环境的启动脚本里经常出事故:有人在测试环境前台启动一切正常,上线时把命令拷贝到 start.sh,忘了加&,结果部署后终端会话一退出,虽然进程还活着,但运维想终止任务时发现它挂在那个终端上无法正常管理,找起来非常费劲。

4.4 停机时日志文件最后一行不完整

很常见的一个现象:进程被 kill 后,日志文件的最后一行只有半截,没有换行符。比如输出写了一半就收到强制结束信号。这通常是程序还在缓冲没写入文件,kill -9直接打断导致缓冲区数据丢失。

解决思路有两个层面:

程序层面,用日志库时设置 autoflush,或定期刷盘。运维层面,优先用kill发送 TERM 信号而不是kill -9,给程序预留机会处理未写完的内容。如果任务对日志完整性要求很高,比如银行交易类应用,还得在应用层做事务日志,而不是依赖标准输出流。

4.5 nohup 启动后命令却立刻退出,如何排查

有些脚本 nohup 启动后,日志文件生成了一下,进程马上没了。这时先去查日志,如果里面记录了一段报错信息,说明脚本启动后自己退出。如果日志只有启动行然后就没了,可能是脚本执行被环境变量、依赖条件卡住。

这类问题唯一的有效路径就是“先前台跑,排除疑问”。直接在终端执行同样的命令,不叠加 nohup 和&,看它在屏幕上输出什么、报什么错、能跑多久。前台能稳定运行再套 nohup,这是排查这类问题的基本顺序。跳过这一步直接猜原因,十有八九浪费时间。

顺手提一个常见细节:nohup后面的命令如果是一个需要交互输入的程序,它会在没有任何输入来源的环境下因为读不到输入而报错退出。nohup 默认忽略输入就是为了避免进程因为从空终端读输入而挂起。

5. 结合团队实际场景的落地建议

5.1 nohup 适合什么场景,不适合什么场景

用 nohup 最舒服的场景是“临时跑一个长任务”:比如数据迁移、压测脚本、批处理任务,跑完就走,不需要常驻监控。这类任务用 nohup 启动成本最低,一行命令就解决问题。

不适合的场景是“核心业务服务”。核心服务需要开机自启、崩溃自动拉起、集中日志管理,这些是 systemd 或者容器编排工具的活。nohup 服务挂了不会自己恢复,也没有依赖管理,日志散落在各个目录,不适合承担长期稳定运行的职责。

可以这么判断:任务是“一次性”还是“常驻型”?一次性任务优先 nohup,常驻服务交给 systemd。

5.2 日志文件命名规范与应用目录隔离

团队协作时,日志命名和路径如果没有规范,排查问题就是灾难。我见过的统一做法是:

  • 每个应用建独立日志目录:/data/logs/应用名/
  • 日志文件名关联任务名和日期:task-$(date +%Y%m%d).log
  • 启动脚本中固定服务名,禁止裸启动命令

比如:

nohup ./run_task.sh >> /data/logs/task/task-$(date +%Y%m%d).log 2>&1 &

日志按天分文件,配合 logrotate 做轮转,整套管理下来清晰很多。这个规范不在于用什么工具,而在于“提前定好谁写在哪儿、写多久、写完怎么处理”,避免人人和任务之间互相覆盖日志文件。

5.3 结合 tmux 做“既要后台又要可交互”的场景

还有一种需求经常被提出来:希望进程在后台运行,但有时候想切进来看一眼,甚至临时敲点命令。nohup 只能把进程推出去,做不了“随时交互”。这种情况建议用 tmux。

tmux 可以理解为一个“虚拟终端会话管理器”。会话在后台保持,你随时能重新接入;里面有多个窗口,可以看到进程当前的状态;即使你断开连接,会话里的进程也不会退出。对比一下:

工具后台运行日志文件随时进入交互适用场景
nohup支持支持不支持临时长任务
tmux支持依赖内部命令支持需要交互式操作的后台任务
systemd支持journalctl不支持常驻服务管理

如果你经常和数据开发、算法工程打交道,会发现 tmux 是另一个常用工具:模型训练动辄跑十几个小时,跑完还能切回去看输出,比 nohup 体验好不少。但它的日志记录依赖会话内工具,不像 nohup 一句话就能把输出落盘。

5.4 一个相对完整的启动脚本模板

结合上面的经验,给一套可以参考的脚本结构。任务启动脚本 start.sh:

#!/bin/bash APP_NAME="sync_data" LOG_DIR="/data/logs/${APP_NAME}" PID_FILE="/var/run/${APP_NAME}.pid" mkdir -p "${LOG_DIR}" LOG_FILE="${LOG_DIR}/${APP_NAME}-$(date +%Y%m%d).log" nohup ./run_task.sh >> "${LOG_FILE}" 2>&1 & echo $! > "${PID_FILE}" echo "进程已启动,PID: $(cat ${PID_FILE})" echo "日志文件: ${LOG_FILE}"

停止脚本 stop.sh:

#!/bin/bash PID_FILE="/var/run/sync_data.pid" if [ -f "${PID_FILE}" ]; then PID=$(cat "${PID_FILE}") kill "${PID}" 2>/dev/null && echo "已发送终止信号给进程 ${PID}" rm -f "${PID_FILE}" else echo "未找到 PID 文件,请手动确认进程状态" fi

脚本的逻辑不复杂,核心价值在于把日志路径、PID 记录、启动与停止目录等容易出错的细节固化下来,避免每次启动时靠人脑记。

6. 最后一些个人实操体会

用了这么多年 nohup,最大的体会就是“日志路径一定要显式指定”。默认 nohup.out 虽方便,但是在当前目录随机生成,时间一久根本找不到哪份日志对应哪个任务,尤其是服务器上任务多的时候,会乱成一团。固定目录、按日命名、启动脚本记录 PID,这些看起来“多写几行”的操作,能让排查问题的成本下降一个量级。

还有一个小技巧值得分享:启动完成以后,第一件事就是用tail -f看一眼日志是否正常滚动,确认进程真的进入了工作状态,而不是等到第二天发现任务早就挂了却毫无痕迹。这个习惯每多坚持一次,就能少踩一次“启动时没报错但其实没跑起来”的坑。

关于日志文件,也提醒一句:重定向的顺序、追加与覆盖的选择、缓冲导致的延迟,这些都是小事,但恰恰是小问题积累起来,才让本来简单的 nohup 使用变得棘手。把这些细节记录到团队的手册里,新人接手时照着做,能少走不少弯路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进