“究竟在干什么”是一系列关于软件背后运作原理的文章,每一篇文章旨在讲解一些在日常编程实践中常见但可能并不为人所熟知的技术细节,抛砖引玉,期待激发读者朋友的更多思考。
序言
每当需要ssh
登录到服务器并运行一个比较花时间的脚本时(比如临时从生产环境导出数据),为了能够知道脚本是否运行结束,或者是否出错退出,我都会将脚本的输出内容重定向到文件中
1 | node foobar.js > /tmp/foobar.log 2> /tmp/foobar.err |
如果不在乎将正常的打印和错误混在一起,可以写成
1 | node foobar.js > /tmp/foobar.log 2>&1 |
上面代码中的2
和1
分别是标准错误(C语言中的stderr
)和标准输出(C语言中的stdout
)的文件描述符,2>&1
的意思便是将打印到标准错误中的内容转移到标准输出中去——这个转移在shell中的术语便叫做重定向(redirection)。
2>&1
该放哪里?
bash
的man
文档中有一个名为REDIRECTION
的章节专门介绍了重定向相关的内容,其中有一段有意思的内容
用ls
不方便做演示,我准备了下面这一段Node.js代码
1 | console.error('Print to standard error.'); |
将代码保存到文件foobar.js
中。
如果将2>&1
写在后面,那么foobar.log
中会包含两行
1 | ➜ /tmp node foobar.js > /tmp/foobar.log 2>&1 |
否则,foobar.log
中只含有一行内容,另一行会出现在终端上
1 | ➜ /tmp node foobar.js 2>&1 > /tmp/foobar.log |
那么为什么会这样呢?
重定向的时候,shell在做些什么?
以执行node foobar.js > /tmp/foobar.log
为例,当shell发现命令中含有重定向的符号时,便开始忙碌起来。
shell首先用open
函数打开文件/tmp/foobar.log
,拿到一个文件描述符(一个非负整数)。Node.js的fs
模块中有一个open
方法,在调用成功时,也是往回调函数传入文件描述符
1 | const fs = require('fs'); |
比较奇妙的是,多次运行时拿到的文件描述符总是相同的
1 | ➜ /tmp date; node open.test.js |
说回重定向。shell拿到文件描述符后,便调用dup2
函数。既然有dup2
,那么就有dup
。dup
接收一个文件描述符作为参数,返回一个新的文件描述符。而dup2
则接收两个参数,它可以作为让第二个参数的数字成为一个新的文件描述符,指向与第一个参数相同的文件。
用图形可以更好地表达dup2
的实现原理。下图是一个进程没有重定向时的状态,每个文件描述符都指向它们原本对应的文件
作为数字的文件描述符,相当于是文件描述符表的数组下标。调用dup2
后,就变成了
可以将dup2
理解为:把文件描述符表的一个元素(以dup2
的第一个参数作为下标),按位复制到另一个元素中(以dup2
的第二个参数作为下标)。
这样一来,凡是写往文件描述符1
的数据,其实都写到了文件/tmp/foobar.log
中。
所以,如果命令中重定向操作是2>&1 > /tmp/foobar.log
,那么文件描述符表中下标1和2的元素并不会指向相同的文件
如果重定向操作是> /tmp/foobar.log 2>&1
,则如下图所示
因此,此时不管是写往文件描述符1还是2,最终都重定向到了/tmp/foobar.log
中。
后记
如果想要严谨地知道bash
是如何处理重定向的,可以在GitHub的这个Bash源代码镜像上直接查看,找到根目录下的redir.c文件即可。
此外,对于上面的示意图,维基百科的File descriptor词条也有一幅更严谨的版本。