最近遇到一个好玩的事,在使用 Linux 命令执行任务【Java 程序或者 Python 程序】时,需要把输出内容日志重定向到文件中,并且保持任务后台执行,这样就可以继续执行其它的命令,不占用 SSH 工具的 session。而且,如果等不了任务的运行,直接退出 SSH 登录即可,任务会在后台继续执行,下次重新登录时可以继续查看任务的状态、分析日志的内容。这里面会涉及到输出重定向、设备文件、输出类型的概念,本文记录这个问题以及涉及的相关知识点。
基础概念
首先,通过一个很常见的具体的例子来说明基础概念,让读者有一个深刻印象。在 Linux 机器上面运行程序的时候,很多人都很熟练地使用类似于 nohup run_your_program > /dev/null 2>&1 &,一些人一看就明白了,还有一些人可能会使用,但是不太了解什么意思。当然,大多数人肯定都查过,了解过相关知识,只不过有时候不在意它,久而久之就忘记了。下面我就以这个例子为样本分析各个符号的含义。
- 起始的
nohup和结尾的&是一对命令,常常放在一起使用,表示让运行的进程忽略 SIGHUP 信号,并进入后台运行,这样就可以保证这个进程一直运行下去,不受用户退出、系统 SIGHUP 信号的影响 run_your_program,表示运行的程序命令,可以是 Java、Shell、Python、Go 等>,表示 重定向 ,可以把输入或者输出重定向到一个地方,后面一般跟着的是文件>重定向符号前面缺省了默认值 1,完整应该是1>,表示 标准输出文件 ,即stdout/dev/null,表示 空设备文件 ,它是一个特殊的文件,表示什么都没有,它跟在重定向符号后面则表示把运行程序产生的 标准输出文件 重定向到空设备文件,即不会输出 标准输出文件 的内容2>&1,根据前面的解释,这些符号可以放在一起理解,2表示 标准错误文件 ,即stderr,>仍旧是重定向,&1则表示重定向的文件和 1 一样,即把 标准错误文件 也重定向到/dev/null中,即不会输出 标准错误文件 的内容
想必读者已经看明白了,上面的描述已经很清楚,但是有些地方我觉得还是需要再总结完善一下。
对于 nohup 与 & 的使用,还需要了解一下控制信号、后台进程相关的知识点,这样才能知其然并且知其所以然。关于控制信号的知识点,可以参考我另外一篇博文:Linux 之 kill 命令入门实践 ,关于后台进程的知识点,也可以参考我另外一篇博文:Linux 让进程在后台运行的几种方法 。
1、2 这 2 个数字,是文件描述符,都是有特殊含义的,1 是缺省值,其实还有一个 0。一般情况下,Linux/Unix 系统在启动进程时,会打开三个文件,由这三个文件描述符来表示,它们的具体含义如下:
- 标准输入文件,
stdin,文件描述符为 0,进程默认从stdin读取数据,一般是从键盘输入读取 - 标准输出文件,
stdout,文件描述符为 1,进程默认向stdout输出数据,一般是输出到终端界面 - 标准错误文件,
stderr,文件描述符为 2,进程默认向stder输出错误数据【例如异常日志】,一般是输出到终端界面
这里再多提一点,有时候读者还会见到 &> /dev/null 这种重定向的使用,这里的 & 其实表示的是 1、2 的合集,即把 标准输出文件 、 标准错误文件 都重定向到 /dev/null 中,效果等价于使用 1> /dev/null 2>&1,包括 IO 效率也是一致的【涉及到文件的管道,下文会有验证过程】。
/dev/null 是一个极为特殊的设备文件,输出到这里的数据是不可见的,也就是数据会被丢弃,如果尝试从这个文件中读取数据,什么也读不到。
如果读者希望在运行程序时,程序产生的日志、计算结果都不要在屏幕上输出,那么就可以选择将 标准输出文件 、 标准错误文件 都重定向到 /dev/null,此时无论程序产生了什么异常,你都看不到了。总的来说,这个空设备文件虽然看上去很奇怪,似乎没有什么价值,但是如果将所有的输出重定向这里,就可以达到禁止输出的效果。
好,至此概念讲解完毕,接下来会使用更加详细的命令来演示重定向的神奇功能,并且还会对比一些看起来差不多的命令。
本文中涉及的 Shell 脚本已经被我上传至 GitHub,读者可以提前下载查看:test_redirect 相关脚本 ,脚本命名与下文中描述一致。
举例演示
仍旧拿标准的命令格式 run_your_program > /dev/null 2>&1 来举例,为了适配 Linux 机器以及简化程序【读者可以直接使用我的 Shell 脚本在任何一台 Linux 机器上面运行】,我直接使用 Shell 脚本来演示。
我写了一个简单的脚本:test_redirect.sh,只有 6 行内容【有 3 行内容是打印日志的,方便查看输出】,如下:
1 | echo '========list all file in current path' |
这个脚本只做了三件事,其中,第 2 行列出当前目录的所有文件、文件夹,第 4 行列出当前目录下的 not-exist-dir 子目录中的所有文件、文件夹【当然,这是一个不存在的目录,目的是为了让脚本有标准错误信息】,第 6 行输出当前的日期。
直接运行打印在终端屏幕
使用 sh test_redirect.sh 直接运行脚本,由于没有重定向的操作,默认就是把标准输出、标准错误全部打印在屏幕上面,如图可以看到输出内容【其中第 4 行的报错信息 No such file or directory 属于标准错误信息】。
我来仔细分析一下屏幕上输出的内容,3 行以 ======== 开头的内容就不多说了。
- 第 2 行的内容是
ls -a产生的,列出了当前目录的所有文件、文件夹 - 第 4 行的内容是
ls ./not-exist-dir产生的,注意它是一个系统报错,也就是标准错误,用来提示用户命令执行失败【访问文件夹失败】,原因是文件夹不存在 - 第 6 行的内容是
date产生的,输出当前系统的时间
重定向到空设备文件
接下来做一个操作,把标准输出重定向到 /dev/null,标准错误仍旧打印在屏幕上面,运行命令改为 sh test_redirect.sh > /dev/null 即可。
运行后可以看到,屏幕上面只有标准错误信息,没有标准输出信息,这是因为标准输出信息被重定向到空设备文件【参数 > /dev/null】,在屏幕上是看不到了。
接着把标准输出、标准错误都重定向到 /dev/null,运行命令改为 sh test_redirect.sh > /dev/null 2>&1 即可。
运行后可以看到,屏幕上面没有打印任何信息,而且由于被重定向到空设备文件,信息无法找回。
重定向到文本文件
为了保存程序的输出信息【持久化】,方便以后排查问题,在实际场景中不会把标准输出、标准错误直接输出到屏幕,更不会重定向到空设备文件【这种操作会导致无法查看输出的信息,相当于永远丢失】,一般我会指定一个日志文件,用来存放程序所有的输出信息,只要文件还在,随时可以查看。
运行命令 sh test_redirect.sh > ./test_redirect.log,可以把标准输出重定向到 test_redirect.log 文件,标准错误仍旧打印在屏幕上面。
使用命令 sh test_redirect.sh > ./test_redirect.log 2>&1,可以把标准输出、标准错误都重定向到 test_redirect.log 文件,没有任何信息打印在屏幕上面。
各种各样的重定向
以上都是常规的重定向输出,比较常用,读者看一遍也就懂了,下面再列举一些奇怪的、令人疑惑的重定向输出,需要细细分析才能理解其中的含义。
奇葩操作方式
如果使用显示指定的重定向命令来运行程序 sh test_redirect.sh 1> ./test_redirect.log 2> ./test_redirect.log【简称为: 奇葩操作方式 】,也就是在命令中显示地指定标准输出、标准错误全部重定向到 test_redirect.log 文件中,那么它和 sh test_redirect.sh > ./test_redirect.log 2>&1【简称为: 正常操作方式 】的效果一样吗?
从上图的运行结果来看,两者效果 大概一样 ,都是把标准输出、标准错误全部保存在 test_redirect.log 日志文件中,读者肯定也是这样想象的。
如果仔细观察一下,发现完全不一样,虽然日志文件中的内容有部分和 正常操作方式 产生的一致,但是明显少了几行,而且顺序还错乱了,最明显的就是 No such file or directory 这个标准错误信息为什么在第 1 行就出现了。由于运行机器环境的原因,读者如果在自己的机器上面测试一下,可能会发现结果和我的不一样,甚至自己前后运行几次的结果也不一样,遇到这种现象是正常的,不要怀疑人生。此外,如果有读者碰巧遇到了和 正常操作方式 一样的结果,也不要用来反驳我的结果,更不能草率地得出结论:这两种操作方式的效果一致。因为这只是读者你运气好,碰到了这个结果,其实不是你想象的那样。
总的来说, 奇葩操作方式 产生的结果一切皆有可能,内容缺失、位置错乱、内容正常这些结果都有可能产生,那它背后的原因是什么呢,让我来探究一番。
首先,根据目前的现象,我只能猜测是输出时有 2 个文件流对 test_redirect.log 日志文件有竞争【类似于多线程竞争一个锁、多个进程竞争一个 CPU 资源】,导致输出的内容相互覆盖、部分内容丢失,位置也就错乱了。那怎么验证这个猜测呢,以及使用 正常操作方式 时为什么没有这个问题呢?
为了验证这个猜测,必须知道程序在运行时对文件做了什么操作,我可以使用 strace 这个命令来追踪程序对文件的操作。
由于我没有找到使用 strace 直接追踪重定向命令的方法,例如如果使用命令 strace sh test_redirect.sh > ./test_redirect.log 2>&1,其实这里面的重定向是对于 strace 生效的,即把 strace 的输出信息重定向到文件中了,从而追踪不到重定向时对文件的操作,这不是我想要的结果【添加 -o 参数也不行】。于是,我只好采取了一种迂回的思路:先把完整的重定向命令整理到 Shell 脚本中,然后使用 strace 追踪运行脚本的过程。
我整理出 2 个 Shell 脚本,如下:
1、 正常操作方式 对应的 Shell 脚本,strace_normal.sh,脚本内容如下
1 | sh test_redirect.sh > ./test_redirect.log 2>&1 |
2、 奇葩操作方式 对应的 Shell 脚本,strace_strange.sh,脚本内容如下
1 | sh test_redirect.sh 1> ./test_redirect.log 2>./test_redirect.log |
接着开始使用 strace 命令进行验证,由于输出内容过多,先使用 -o 参数保存在文件中,再查看,-f 参数是为了追踪子进程的:
1、使用 strace -o strace_normal.log -f sh strace_normal.sh,追踪 正常操作方式 的系统调用,输出到 strace_normal.log 文件中,主要为了追踪对文件的操作,运行完成后使用 cat strace_normal.log 查看系统调用的内容。
由于输出信息内容太多,都是一些看不懂的系统调用,所以只需要看和重定向时操作的日志文件、文件描述符有关的内容即可,使用 cat strace_normal.log |grep 'test_redirect.log\|dup' 过滤掉无关内容。
1 | 32828 dup2 (3, 255) = 255 |
我关心的重点内容也就是这几行了,可以明显看到打开了 test_redirect.log 日志文件【open 调用】,然后有 2 次文件描述符的复制【dup2 调用】,分别把 3 复制给 1、1 复制给 2,这样操作后 stdout、stderr 都会写入到 test_redirect.log 日志文件,但是文件只被打开了 1 次。
关于 dup 的概念:
dup、dup2:复制一个文件描述符,有 2 种调用方式
int dup (int oldfd)
int dup2 (int oldfd, int newfd)
2、使用 strace -o strace_strange.log -f sh strace_strange.sh,追踪 奇葩操作方式 的系统调用,详细过程省略,只查看重要的系统调用,使用 cat strace_strange.log |grep 'test_redirect.log\|dup' 过滤输出信息。
1 | 59916 dup2 (3, 255) = 255 |
明显可以看到一个不同,调用了 2 次 open,也就是打开了 2 次日志文件。
通过上面的对比分析验证,结论不言而喻,我来简单总结一下:
1> ./test_redirect.log 2> ./test_redirect.log:把标准输出、标准错误都直接重定向到 test_redirect.log 日志文件,但是 test_redirect.log 日志文件会被打开 2 次,即产生了 2 个文件输出流,导致标准输出、标准错误互相覆盖,也就出现了上面比较奇怪的现象。
> ./test_redirect.log 2>&1:把标准输出、标准错误都直接重定向到 test_redirect.log 日志文件,但是标准错误输出时没有打开日志文件的操作,而是直接继承了标准输出的文件流,这样的话 test_redirect.log 日志文件只被打开了 1 次,也就不会出现上面比较奇怪的现象。
另外,如果从 IO 效率方面来看,显然 正常操作方式 的效率更高。
一个特殊的符号
这里要说的特殊符号是 & 符号,如果把它作为文件描述符使用,它表示标准输出、标准错误的集合,示例命令为 &> ./test_redirect.log,它表示把标准输出、标准错误全部重定向到 test_redirect.log 日志文件中,即和 正常操作方式 的效果一致。
我也可以使用上面的 strace 命令验证它的 IO 效率也是和 正常操作方式 一致的,我需要像前面那样,新建一个 Shell 脚本,strace_special.sh,内容如下:
1 | sh test_redirect.sh &> ./test_redirect.log |
使用 strace -o strace_special.log -f sh strace_special.sh,追踪这种重定向方式的系统调用,详细过程省略,只查看重要的系统调用,使用 cat strace_special.log |grep 'test_redirect.log\|dup' 过滤输出信息。
1 | 70626 dup2 (3, 255) = 255 |
结果不言而喻,系统调用时对文件的操作和 正常操作方式 是一致的。
重定向顺序产生的影响
有的读者可能会遇到一种更加奇葩的操作方式,示例命令为 sh test_redirect.sh 2>&1 1> ./test_redirect.log,即把文件描述符 1、2 的重定向操作调换一下位置,先指定 2 和 1 一样,再指定 1 重定向到文件。
通过上面的学习总结,读者应该已经可以猜出这种重定向操作的结果:标准错误重定向到终端屏幕,标准输出重定向到 test_redirect.log 日志文件,而不是两者都重定向到日志文件。
这是为什么呢?其实是因为在指定 2 和 1 一样的时候,1 仍然是重定向到终端屏幕的,那 2 也就是跟着重定向到终端屏幕,接着指定 1 的时候,1 才改变重定向的操作,但是 2 仍然保持不变,这个思路和编程语言中的中间变量类似。
读者也可以使用前面的 strace 命令追踪这种重定向方式对文件的操作、对文件描述的复制过程,然后就明白为什么是这样的结果了,我的验证结果如下:
1 | 14004 dup2 (3, 255) = 255 |
常用重定向总结区分
下面整理一些常用的重定向【文件描述符使用术语表达】,读者必须要区分开来,不要乱用:
run_your_program:没有使用重定向,stdout、stderr全部输出到终端屏幕run_your_program > /dev/null:stdout输出到空设备文件、stderr输出到终端屏幕run_your_program > /dev/null 2>&1:stdout、stderr全部输出到空设备文件run_your_program > log_file:stdout输出到日志文件、stderr输出到终端屏幕run_your_program > log_file 2>&1:stdout、stderr全部输出到日志文件run_your_program > log_file 2> log_file:stdout、stderr全部输出到日志文件,但是输出内容会混乱,这种方式不建议使用run_your_program 2>&1 1> log_file.log:stdout输出到日志文件、stderr输出到终端屏幕,有点迷惑人,不建议使用,要确认的确懂这个重定向的真实含义才能使用run_your_program 2>&1:stdout、stderr全部输出到终端屏幕,相当于没有重定向,没必要使用【如果系统默认没有把stdout输出到终端屏幕,需要使用这种方式】run_your_program &> log_file:stdout、stderr全部输出到日志文件,&表示stdout、stderr2 个的集合
注意事项
1、& 和后台作业
在这里如果只使用 nohup 不使用 & 把程序后台运行【仍旧是前台运行】,表面上看把输出日志重定向文件中了,屏幕不再滚动打印出来大量的文本内容。其实,程序此时仍旧在占用着键盘的输入流,你的终端命令行在等待着输入,你无法使用键盘进行其它的命令操作,而且你不能使用 ctrl + c 的方式中断,否则程序会退出。
此时如果想把进程调到后台运行,可以使用 ctrl + z 暂停进程【同时调到后台,变为暂停状态的作业】,然后使用 bg 让作业在后台继续运行,这样就可以手动把运行在前台的进程调整到后台,而没有影响到进程的运行。如果后台的作业比较多,先使用 jobs 查看作业的编号,使用 bg 作业编号 指定某个作业在后台运行。
2、Python 脚本的输出缓存机制
在执行 Python 脚本时,把打印的日志都重定向到一个日志文件中,发现日志内容并没有及时更新【在 Python 脚本中 print 的日志内容】,实时查看日志文件的内容【使用 tail -f log_file.log 命令】,发现不会像 Java 程序那样实时刷出来新的内容,而是会卡住一段时间,然后突然一大段日志出来。造成这种现象的原因是 Python 有输出流缓存机制,不会把输出内容实时写入输出流,而是等待缓冲区积累一定的内容再操作,这样一来,重定向到文件中的内容总是一批一批的。
当然,可以选择关闭这个选项,在执行 Python 脚本时,使用 -u 参数就可以强制把输出内容实时写入到输出流,也就可以实时重定向到日志文件了,命令示例 nohup python -u your_python_file > log_file.log 2>&1 &。
3、读者可以继续探索一下 tee 命令的使用,它可以帮助我们把 stdout、stderr 即输出到终端屏幕、又输出到文件,关键是不用写多行命令。同时,它还有一个优点,即使用 tee 命令是不影响原来的 IO 效率的。
4、本文中涉及的 Shell 脚本已经被我上传至 GitHub,读者可以下载查看:test_redirect 相关脚本 ,脚本命名与上文中描述一致。

