Linux 输出重定向的问题

最近遇到一个好玩的事,在使用 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 让进程在后台运行的几种方法

12 这 2 个数字,是文件描述符,都是有特殊含义的,1 是缺省值,其实还有一个 0。一般情况下,Linux/Unix 系统在启动进程时,会打开三个文件,由这三个文件描述符来表示,它们的具体含义如下:

  • 标准输入文件,stdin,文件描述符为 0,进程默认从 stdin 读取数据,一般是从键盘输入读取
  • 标准输出文件,stdout,文件描述符为 1,进程默认向 stdout 输出数据,一般是输出到终端界面
  • 标准错误文件,stderr,文件描述符为 2,进程默认向 stder 输出错误数据【例如异常日志】,一般是输出到终端界面

这里再多提一点,有时候读者还会见到 &> /dev/null 这种重定向的使用,这里的 & 其实表示的是 12 的合集,即把 标准输出文件 标准错误文件 都重定向到 /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
2
3
4
5
6
echo '========list all file in current path'
ls -a
echo '========list all file in ./not-exist-dir'
ls ./not-exist-dir
echo '========print date'
date

这个脚本只做了三件事,其中,第 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
2
3
4
5
32828 dup2 (3, 255)                      = 255
32830 open ("./test_redirect.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
32830 dup2 (3, 1) = 1
32830 dup2 (1, 2) = 2
32830 dup2 (3, 255) = 255

我关心的重点内容也就是这几行了,可以明显看到打开了 test_redirect.log 日志文件【open 调用】,然后有 2 次文件描述符的复制【dup2 调用】,分别把 3 复制给 11 复制给 2,这样操作后 stdoutstderr 都会写入到 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
2
3
4
5
6
59916 dup2 (3, 255)                      = 255
59917 open ("./test_redirect.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
59917 dup2 (3, 1) = 1
59917 open ("./test_redirect.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
59917 dup2 (3, 2) = 2
59917 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
2
3
4
5
70626 dup2 (3, 255)                      = 255
70627 open ("./test_redirect.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
70627 dup2 (3, 1) = 1
70627 dup2 (1, 2) = 2
70627 dup2 (3, 255) = 255

结果不言而喻,系统调用时对文件的操作和 正常操作方式 是一致的。

重定向顺序产生的影响

有的读者可能会遇到一种更加奇葩的操作方式,示例命令为 sh test_redirect.sh 2>&1 1> ./test_redirect.log,即把文件描述符 12 的重定向操作调换一下位置,先指定 21 一样,再指定 1 重定向到文件。

通过上面的学习总结,读者应该已经可以猜出这种重定向操作的结果:标准错误重定向到终端屏幕,标准输出重定向到 test_redirect.log 日志文件,而不是两者都重定向到日志文件。
文件描述符顺序产生的影响

这是为什么呢?其实是因为在指定 21 一样的时候,1 仍然是重定向到终端屏幕的,那 2 也就是跟着重定向到终端屏幕,接着指定 1 的时候,1 才改变重定向的操作,但是 2 仍然保持不变,这个思路和编程语言中的中间变量类似。

读者也可以使用前面的 strace 命令追踪这种重定向方式对文件的操作、对文件描述的复制过程,然后就明白为什么是这样的结果了,我的验证结果如下:

1
2
3
4
5
14004 dup2 (3, 255)                      = 255
14005 dup2 (1, 2) = 2
14005 open ("./test_redirect.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
14005 dup2 (3, 1) = 1
14005 dup2 (3, 255) = 255

常用重定向总结区分

下面整理一些常用的重定向【文件描述符使用术语表达】,读者必须要区分开来,不要乱用:

  • run_your_program:没有使用重定向,stdoutstderr 全部输出到终端屏幕
  • run_your_program > /dev/nullstdout 输出到空设备文件、stderr 输出到终端屏幕
  • run_your_program > /dev/null 2>&1stdoutstderr 全部输出到空设备文件
  • run_your_program > log_filestdout 输出到日志文件、stderr 输出到终端屏幕
  • run_your_program > log_file 2>&1stdoutstderr 全部输出到日志文件
  • run_your_program > log_file 2> log_filestdoutstderr 全部输出到日志文件,但是输出内容会混乱,这种方式不建议使用
  • run_your_program 2>&1 1> log_file.logstdout 输出到日志文件、stderr 输出到终端屏幕,有点迷惑人,不建议使用,要确认的确懂这个重定向的真实含义才能使用
  • run_your_program 2>&1stdoutstderr 全部输出到终端屏幕,相当于没有重定向,没必要使用【如果系统默认没有把 stdout 输出到终端屏幕,需要使用这种方式】
  • run_your_program &> log_filestdoutstderr 全部输出到日志文件,& 表示 stdoutstderr 2 个的集合

注意事项

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 命令的使用,它可以帮助我们把 stdoutstderr 即输出到终端屏幕、又输出到文件,关键是不用写多行命令。同时,它还有一个优点,即使用 tee 命令是不影响原来的 IO 效率的。

4、本文中涉及的 Shell 脚本已经被我上传至 GitHub,读者可以下载查看:test_redirect 相关脚本 ,脚本命名与上文中描述一致。

虾丸派 wechat
扫一扫添加博主,进技术交流群,共同学习进步
永不止步
0%