尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Linux日志重定向全攻略:从2>1到nohup、logrotate的实战避坑
在 Linux 下做开发或者运维你一定遇到过这样的需求程序跑起来了一堆 print 和调试信息哗哗往屏幕上滚可等你想回头翻几分钟前的记录屏幕早就翻篇了或者一个脚本要在后台跑一整晚第二天起来根本不知道中间发生了什么。解决办法就是标题里写的那句话——把程序运行时的打印重定向写进指定文件。听起来很简单但“重定向”三个字背后藏着一堆细节标准输出和标准错误有什么区别21为什么总出现在命令末尾日志文件怎么越来越大进程还活着日志却一直是空的这些不搞明白很容易出现“日志文件生成了里面却啥也没有”这类灵异事件。这篇内容就是把这些事从头到尾理顺。如果你是刚接触命令行的新手看完之后至少知道下一次该怎么写如果你已经写过不少脚本里面几个坑也值得对一遍。我会用实际命令、踩过的坑和排查思路来讲尽量让你读完直接能上手。1. 动手之前先把重定向的对象看清楚1.1 三根管子标准输入、标准输出与标准错误每个程序启动的时候操作系统都会给它准备好三个标准通道文件描述符 0 是标准输入stdin通常对应键盘文件描述符 1 是标准输出stdout正常打印的信息走这里文件描述符 2 是标准错误stderr错误提示、异常堆栈走这里。终端上明明显示的都是“字”为什么要分两路因为正常结果和异常告警压根是两类东西。你要的是把业务数据存进 result.log把报错信息存进 error.log或者干脆丢弃错误只看正常输出。如果它们混在同一个通道里这些事情都做不到。很多同学第一次用重定向习惯性写python app.py log.txt结果发现报错一行不落全打在屏幕上于是以为命令没被执行。其实程序正常地“崩溃”了而且错误信息根本没进日志文件。这个“灵异现象”的原因特别简单这个符号默认只重定向文件描述符 1文件描述符 2 仍然指向终端。我习惯把这三个通道比作厨房里的三根水管0 是进水1 和 2 是两路出水一热一冷。重定向操作就是接线你想让热水进桶、冷水进下水道还是两股水都进同一个桶全看你怎么接。1.2 为什么命令里总在写 21这是命令行驶里最常见的谜之符号。拆解一下实际上是1的缩写意思是把文件描述符 1 指向后面的文件。21的意思是让文件描述符 2 指向文件描述符 1 当前指向的地方注意是“当前”。这是理解和翻车的关键。看两行对比# 正确stdout 和 stderr 都进 all.log python app.py all.log 21 # 错误写法stdout 进 all.logstderr 还在终端 python app.py 21 all.log先执行 all.log文件描述符 1 已经指向 all.log再执行21文件描述符 2 指向“文件描述符 1 当前指向的地方”也就是 all.log。两个通道最终进了同一个文件。反过来先执行21此时文件描述符 1 还指向终端所以文件描述符 2 指向终端然后再执行 all.log只有文件描述符 1 被改到文件文件描述符 2 依然在终端。命令长得差不多结果差之千里。bash 里还有一个简写command all.log表示 stdout 和 stderr 都进文件表示追加。写起来短但它不是 POSIX 标准语法在 sh 环境里不一定好用。团队脚本里我会建议统一写 file 21可读性更好别人一看就知道发生了什么。1.3 覆盖还是追加日志的两种写法是截断写入文件先被清空再写入新内容。是追加写入在文件末尾继续写不清空历史。很多一次性任务用无所谓但日志记录类操作必须用。比如你有一个每五分钟跑一次的监控脚本如果用上一次的数据全没了日志只剩最后一次运行结果排查问题的时候啥都看不到。操作行为适用场景command file清空后覆盖导出结果、生成最终报告command file追加到末尾日志记录、持续运行的任务command file 21stdout 与 stderr 清空后归档明确只保留本次运行command file 21stdout 与 stderr 追加归档跨多次运行保留完整历史我自己的原则不确定用哪个的时候直接上。日志文件大不了多占点磁盘但历史记录丢了可能连事故还原都做不到。2. 把程序输出写进指定文件的三种姿势2.1 只抓标准输出一条命令搞定简单场景最基础的用法是命令后直接加重定向python report.py data_report.txt适合什么场景程序是一个工具型脚本你只关心它打印出来的结果不关心错误输出或者你已经确定这个程序不会产生 stderr 内容。一次性任务比如数据导出、文件转换完全够用。局限性也很明显如果程序在运行中报错错误信息只会闪现在终端文件里只有正常输出部分。很多时候你拿到手的日志看起来完美实际程序早就跑飞了。所以做日志记录时我几乎不用这种单流重定向除非我已经在代码里把所有要记录的信息统一用 print 打到了 stdout。2.2 捕获完整足迹把标准输出与标准错误合并写进同一个文件真正做日志记录推荐下面这种写法python app.py app.log 21这样程序所有输出无论是 normal print 还是 traceback都会落进同一个文件不会出现“日志里看不到报错”的问题。bash 用户也可以写python app.py app.log只是我平时还是更喜欢显式写21一眼能看出意图。如果你想把两类信息分开管理也可以python app.py out.log 2 err.log正常输出和错误输出分别进两个文件适合那种业务日志和错误告警需要分开归档的场景。有一点要注意不要写成python app.py same.log 2 same.log两个文件描述符同时以“覆盖”模式指向同一个路径很容易出现内容互相覆盖的怪现象。要合并就用21要分开就用两个不同文件名。还有一种非常实用的 shell 脚本内全局重定向写法#!/usr/bin/env bash exec /opt/app/logs/run-$(date %F).log 21 echo 任务启动: $(date %F %T) python worker.py echo 任务结束退出码: $?exec重定向可以把脚本的 stdout 和 stderr 整体改到日志文件脚本里后续所有命令的输出都会自动写入日志不用每条命令都加后缀。日志名按日期区分每天一个文件既方便查询又顺便做了基础轮转。2.3 又要看又要存用 tee 做实时镜像有些场景你既要实时看程序输出又要存档。比如调试脚本或者跑一段长时间训练任务你想盯着进度条还想保留完整输出。这时候直接就不行了因为它把输出送进了文件而不上终端。正确姿势是用teepython app.py 21 | tee -a run.logtee这个名字很形象像水管上的三通输入从这里过来一路送终端一路写文件。-a参数表示追加不加的话默认覆盖文件。注意必须先把 stderr 并到 stdout 上因为管道|只会接收 stdout如果不加21错误信息会绕过管道直接打到终端文件里依旧缺失关键报错。这里有一个隐蔽的坑管道中最后一条命令的退出码是整个管道的退出码。tee几乎总是成功返回 0所以即使程序本身失败$?也可能是 0脚本继续往下跑错误被悄悄吞掉。解决办法是在脚本开头设置set -o pipefail python app.py 21 | tee -a run.log || echo 任务失败退出码: ${PIPESTATUS[0]}pipefail让管道退出码取所有命令中的非零值这样脚本能拿到真正的失败状态。这个操作在调试后台日志时尤其重要我一度因为这个原因错失了好几次任务失败告警。3. 后台任务与定时任务的日志管理实操3.1 关掉终端也不丢日志nohup 与后台运行开发环境随手跑个脚本关终端无所谓但生产环境里你不可能坐那儿盯一天。最经典的组合是nohup python app.py app.log 21 拆开看三个部分把命令放到后台执行终端立刻返回提示符。nohup让进程忽略 SIGHUP 信号。终端关闭时会向它的子进程发送挂断信号默认行为是终止进程nohup 会屏蔽这个信号让进程继续跑。 app.log 21把程序的打印落盘。很多人会问和 nohup 能不能分开其实这里的情况很多nohup python app.py app.log 21 是标准写法nohup在命令开头在命令末尾别写反了。如果命令里不加重定向nohup 默认会把输出写到当前目录下的nohup.out文件。这个文件攒久了会越来越大而且路径不在你预期时很难找。所以每次用 nohup 我都会显式指定日志文件至少不会让日志散落到莫名的地方。启动之后怎么确认进程状态常用的有ps -ef | grep app.py tail -f app.logtail -f是实时看日志的好帮手排查后台任务时基本靠它。3.2 定时任务里的输出默认去了奇怪的地方写 crontab 任务时很多人不知道cron 默认会把你任务产生的一切输出用邮件发给系统账户。如果你没配邮件服务这些输出会堆积在本机/var/mail/user或/var/spool/mail/下时间一长可能占满磁盘。更致命的是你根本不会主动去看邮件日志等于失踪。正确姿势是显式重定向0 2 * * * cd /opt/app python batch.py /opt/app/logs/batch-$(date \%Y\%m\%d).log 21注意几个细节日志路径用绝对路径cron 执行环境的工作目录通常是用户主目录用相对路径的话文件会出现在你完全想象不到的地方。%在 crontab 里有特殊含义需要转义成\%所以上面写成$(date \%Y\%m\%d)。日志目录要提前创建好cron 不会帮你建目录。如果你确定任务不需要留任何输出可以简单丢弃0 2 * * * /opt/app/monitor.sh /dev/null 21/dev/null是 Linux 的“黑洞”扔进去的内容直接消失。配合21stderr 一并丢弃。有时候你看到一个任务一直不成功却没有任何日志多半就是这里把调试信息一起扔掉了。3.3 日志会无限膨胀轮转是必须考虑的事日志一直追加磁盘终究会被写满。磁盘满导致程序起不来这在我的经历里是最常见的“非代码故障”。解决办法是日志轮转最省心的工具是系统自带的 logrotate。在/etc/logrotate.d/下放一个配置比如/opt/app/logs/app.log { daily rotate 7 compress missingok notifempty copytruncate }含义逐条说daily每天轮转一次。rotate 7保留 7 份历史更早的自动删除。compress旧的日志压缩成 gz省空间。missingok文件不存在时不报错。notifempty文件为空不轮转避免产生一堆空文件。copytruncate先复制文件内容再原地截断原文件进程不需要重启。为什么单独强调copytruncate因为如果你的程序是通过 shell 重定向打开日志文件的进程一直持有旧文件的文件句柄。直接mv app.log app.log.1再新建 app.log程序依然往旧的 inode 上写数据新日志不会进新文件。日志没变小磁盘空间还在涨文件却“消失”了。copytruncate的逻辑是先把当前内容复制到历史文件然后把原文件原地清空程序的文件句柄还是指向同一个路径的同一个 inode后续输出自然继续写进原文件。省掉了重启进程的麻烦。4. 代码内部重定向让程序自己决定日志写哪4.1 Python 里的三种写法shell 重定向好归好但毕竟是在程序外面包一层。有些场景更适合让程序自己决定日志路径尤其是当你希望日志行为跟调用方式无关时。我这里拿 Python 举例因为写脚本的人里 Python 占比最高。第一种最直接的粗暴替换import sys sys.stdout open(/opt/app/logs/app.log, a, encodingutf-8) print(这行会写进日志文件)把sys.stdout换成文件对象之后所有print默认输出就会落到文件里。问题在于想同时打屏不行想切回默认输出还得保存原对象而且文件缓冲还得自己管理。玩具脚本临时用可以生产里不推荐。第二种print的file参数with open(app.log, a, encodingutf-8) as f: print(处理完成, filef)这种写法好控制局部用不会影响全局输出。缺点是每条输出都要手动指定文件代码会显得啰嗦。第三种也是我推荐的方式用标准库loggingimport logging logging.basicConfig( levellogging.INFO, format%(asctime)s %(levelname)s %(message)s, handlers[ logging.FileHandler(/opt/app/logs/app.log, encodingutf-8), logging.StreamHandler() ] ) logging.info(任务开始) logging.error(这里发生了一个错误)好处很明显自带时间戳、级别、来源模块排查问题友好得多。FileHandler写文件StreamHandler同时打终端两不误。按级别过滤DEBUG、INFO、WARNING、ERROR 分得一清二楚。本身会处理缓冲问题线程安全也有保障。如果还想让日志文件路径可在命令行指定加一个参数解析就行import argparse parser argparse.ArgumentParser() parser.add_argument(--log-file, default/opt/app/logs/app.log) args parser.parse_args() logging.basicConfig(filenameargs.log_file, levellogging.INFO)这样使用者可以用python app.py --log-file logs/run.log指定路径比教他去理解21简单多了也更符合“程序自己管理日志”的思路。提醒一点大量用print的项目代码里到处是零散的调试输出日志没有分级、没有时间戳出了问题只能人肉分析。我的做法是尽早迁到 logging哪怕只是basicConfig配一个FileHandler长期收益也比 print 重定向大得多。4.2 其他语言的日志方案速览Java 这边有几个层次。简单粗暴方式是System.setOut和System.setErr把标准输出流重定向到文件生产项目一般直接用 log4j2 或 slf4j logback配置文件里指定 RollingFile天然支持按大小/日期轮转。Node.js 里console.log重定向文件也不是不行但更常见的是用 winston、pino 这类日志库支持多 transport有的还能输出 JSON 结构方便后续接入日志系统。Shell 脚本内部可以这样#!/usr/bin/env bash exec (tee -a run.log) 21这里用进程替换把 stdout 同时指向终端和日志文件相当于在脚本内部实现了tee的效果。核心判断标准我总结成一句话短生命周期脚本shell 重定向够用常驻服务一定要上代码级日志框架。后者的关键优势不是“写文件”而是“分级 轮转 可查”。重定向只是把输出接走日志框架则是把输出管理起来。5. 高频故障速查与避坑心得5.1 日志文件根本没生成或者生成在奇怪的地方最常见的原因是相对路径。你在/home/user/project/下执行python app.py run.log文件当然在当前目录。但如果你在 crontab 里写了 run.logcron 的工作目录通常不是项目目录文件就会掉到用户主目录甚至其它地方。排查第一步永远是pwd确认当前路径然后改成绝对路径。第二个常见原因是权限问题。 /var/log/app.log通常需要 root 权限普通用户会直接报 Permission denied。建议先touch /path/to/test.log验证目录可写再跑真实命令。如果命令本身启动后立刻报错退出文件已经创建但内容为空这不算“没生成”而是错误输出根本没被重定向进来。这时候回去检查命令里有没有21。5.2 日志文件有但内容迟迟不更新这个问题非常容易误导人。你明明看到程序在正常跑但cat日志文件就是没内容。原因多数是缓冲。stdout 的缓冲策略取决于它连的是什么连终端时是行缓冲每遇到换行就刷新连文件时是块缓冲攒到 4KB 或 8KB 才刷一次。所以重定向到文件后程序输出不会立刻出现在文件里尤其是 Python 脚本。最简单的解决办法是python -u app.py app.log 21-u让 Python 的 stdout/stderr 变成无缓冲内容立即落盘。C/C 或其他动态库程序可以用stdbufstdbuf -oL -eL ./some_binary app.log 21-oL意思是 stdout 行缓冲-eL对应 stderr。注意stdbuf对静态链接的二进制不一定生效而且频繁刷盘会带来性能损耗高吞吐场景不要无脑加折中方案是让程序自己定期 flush 或按日志量控制。排查时还可以看程序退出那一刻日志是否补全了最后一段。如果退出后内容才出现基本可以断定是缓冲没刷新。5.3 日志文件被清空了进程却还在往旧文件写另一个经典场景运行中的程序持续写 app.log你想清理一下执行了 app.log文件变空了。但磁盘空间不降反升日志数据还在继续增长。为什么因为程序打开文件时记录的是 inode而不是路径。 app.log只是把路径对应的文件截断成了 0 字节程序手里的旧文件句柄仍然指向原始 inode新写的日志都被写进那个“已经被截断”的文件里。更隐蔽的是有人直接把日志文件rm了程序继续往已删除的 inode 写。磁盘空间被占着但你在文件系统里看不到这个文件。排查手段是用 lsof 看进程打开的文件lsof -p 进程PID | grep app.log如果输出里带(deleted)标记就是上面说的这种情况。解决思路有三个用 logrotate 的copytruncate不换 inode 原地截断。程序内实现日志按日期切换重新打开新文件。手动处理时先mv app.log app.log.old然后kill -HUP PID让进程重新打开日志文件之后再删旧文件。千万别在生产环境直接rm一个正在被写入的日志文件这是我最想强调的一条。5.4 错误信息完全没进日志症状日志文件里只有正常输出报错堆栈却在终端一闪而过。原因就是 stdout 和 stderr 没有被合并重定向常见于只写了 app.log而漏了21。还有一种情况是顺序写反了command 21 app.log前面已经解释过这个写法的结果是 stdout 进文件stderr 留终端。确认一眼命令的顺序即可。如果是脚本内已经exec log 21程序调用的第三方的程序不遵循继承输出也会漏。比如很多工具直接写/dev/tty或者自己的日志文件shell 重定向管不到它们。下面这个表格基本可以覆盖大多数排查场景症状可能原因处理方式日志文件不在预期位置相对路径 工作目录不符用绝对路径pwd 确认日志文件没生成权限不足或目录不存在touch 测试写权限创建目录文件为空但程序在跑stdout 块缓冲未刷新python -u / stdbuf / 程序内 flush只有正常输出没有报错没写 21或顺序写反统一写成 file 21文件被删后磁盘还在涨进程持有旧文件句柄lsof 确认kill -HUP 或 copytruncate日志中文乱码编码不一致设置 UTF-8Python 加 PYTHONIOENCODINGutf-8多个进程写同一日志互相覆盖日志名区分进程或用程序日志框架6. 几个让我少熬夜的日志习惯6.1 日志落盘的五个默认习惯先说我自己写脚本时默认遵守的习惯算不上什么高深理论但确实让我少踩了很多次坑。第一日志路径一律绝对路径。不管是 shell 重定向还是代码里的 FileHandler绝对路径 先 mkdir -p杜绝“文件去哪了”的悬案。第二脚本开头固定加set -o pipefail。只要脚本里用了管道这个开关就开着否则管道退出码会掩盖真正的失败。这个习惯救过我很多次。第三能上 logging 就不依赖 print。至少给日志加时间戳和级别。时间戳这东西排查问题的时候是命根子。第四日志从第一天配好 logrotate。不要等磁盘满了才想起来。轮转配置不超过十行省心程度却是十级。第五遇到日志“灵异事件”第一反应不是去改代码而是先lsof -p PID看进程到底在写哪个文件。很多日志问题是“在正确的路径找不到正确的文件”不是你代码逻辑错了而是文件句柄和你的预期不在一起。6.2 遇到日志问题先问这三个问题最后分享一个特别实用的小技巧。不管你是刚写脚本还是已经做了很久运维只要程序日志表现不正常先问自己三个问题第一程序往哪个文件里写用lsof -p PID查实际打开的文件路径不要通过“感觉”去猜。第二写进去的内容为什么没看到缓冲权限还是路径错了。缓冲最常见用python -u或stdbuf试一下立刻见分晓。第三旧日志去哪了文件是否被轮转、被删除、被截断。运行中的进程持有的句柄可能已经指向一个“看不见”的文件。把输出落盘这件事看着简单其实牵涉到 IO 缓冲、文件描述符、进程生命周期这些底层概念。搞懂一次以后无论写脚本还是排障都会顺手很多。上面这些做法可能不华丽但我自己日常就是这么用的也确确实实少熬了几个夜。希望对你也有帮助。
RELATED

相关推荐

Pytest-Bdd + Playwright:用 Background 收敛重复步骤,让测试维护更轻松

Pytest-Bdd + Playwright:用 Background 收敛重复步骤,让测试维护更轻松

在我自己写的 Pytest-Bdd-Playwright 系列教程里,前面光是“如何用自然语言描述业务规则”“怎么把 Given/When/Then 映射成自动化代码”这些基础话题,就已经骗了不少收藏。今天这篇是第 15 篇,专门聊 Background(背景&#xff09…

📅 2026/10/18 10:37:36
SQL注入与XSS攻击实战案例解析:从漏洞原理到安全防护

SQL注入与XSS攻击实战案例解析:从漏洞原理到安全防护

1. 从三个事故现场说起:SQL 注入和 XSS 到底怎么“钻”进来的先讲个我实际参与过的场景。某次给一个内部系统做安全巡检,开发同学信誓旦旦地说代码用了框架、都是 ORM,应该没问题。结果我用一个单引号就把后台登录页打穿了,前后没…

📅 2026/10/18 10:37:36
零碳工厂建设标准与源网荷储一体化:智慧能源管理平台落地指南

零碳工厂建设标准与源网荷储一体化:智慧能源管理平台落地指南

1. 零碳工厂建设标准拆解:到底在评什么零碳工厂这个概念,最近两年在制造业里可以说是被问得最多的话题之一。很多企业找我聊的时候,上来第一句话就是:“我们想做零碳工厂,但是零碳工厂建设标准是什么?是不是…

📅 2026/10/18 10:37:36
MORE NEWS

更多资讯

📰

AI赋能广告设计:从Brief到全套物料的50%效率提升实战

AD设计这个缩写,在广告行业里基本就是指广告设计(Advertisement Design),覆盖品牌主视觉、电商海报、信息流广告素材、社媒长图、门店物料这些。我在这个行业做了不少年,坦白讲,两年前我对AI介入设计是持怀…

📰

Flutter服务插件选型指南:前台服务与后台任务的机制与坑点

1. 先搞清楚一个前提:前台服务和后台服务在系统层面差在哪聊Flutter的服务插件之前,我建议先把Android生态里的基本概念捋顺。因为Flutter的服务方案本质上都是对Android Service机制的封装,你如果不懂底层限制,选插件就纯靠猜&am…

📰

降AIGC算法与人机协同:让AI辅助写作更有“人味”的实践机制

有一次我把一份方案初稿发给合作方,对方回了句:“这篇是不是AI写的?”我盯着屏幕愣了几秒,因为稿子的确是我用AI辅助赶出来的。从那一刻我开始认真琢磨一件事:AI辅助写作最大的成本不是生成速度,而是如何让…

📰

基于深度学习的目标检测道路坑洼识别:YOLOv8训练与部署实践

毕业设计选“基于深度学习的道路坑洼识别”这个方向的同学,最近明显多了起来。原因不难理解:既能用到Python、深度学习、图像识别这些当下最热的技术词汇,又有一个清晰可落地的应用场景,数据也相对容易获取。但真正动手做起来&…

📰

机器学习与深度学习道路坑洼识别:从数据集到部署的完整实践指南

在开始写之前,我先说句题外话。每年这个时间,都有很多同学在课程设计和毕业设计的选题边缘反复横跳,既想找个有区分度的方向,又怕最后做不出来。道路坑洼识别这个题目,说实话,方向是好的——机器学习和深度…

📰

基于Python与CNN卷积神经网络的不同衣服颜色识别实战全流程

做“深度学习毕设”最怕的不是模型复杂,而是题目看着简单、跑起来全是坑。就拿“衣服颜色识别”这个方向来说,很多人第一反应是:这不就是图像分类吗?直接丢进卷积神经网络训练一顿就行。实际上手才发现,纯色衣服和花衬…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬