摘要:因此,此时不管是写往文件描述符1还是2,最终都 重定向 到了 /tmp/foobar.log 中。所以,如果命令中重定向操作是 2>&1 > /tmp/foobar.log ,那么文件描述符表中下标1和2的元素并不会指向相同的文件。

“究竟在干什么”是一系列关于软件背后运作原理的文章,每一篇文章旨在讲解一些在日常编程实践中常见但可能并不为人所熟知的技术细节,抛砖引玉,期待激发读者朋友的更多思考。

序言

每当需要 ssh 登录到服务器并运行一个比较花时间的脚本时(比如临时从生产环境导出数据),为了能够知道脚本是否运行结束,或者是否出错退出,我都会将脚本的输出内容重定向到文件中

node foobar.js > /tmp/foobar.log 2> /tmp/foobar.err

如果不在乎将正常的打印和错误混在一起,可以写成

node foobar.js > /tmp/foobar.log 2>&1

上面代码中的 21 分别是标准错误(C语言中的 stderr )和标准输出(C语言中的 stdout )的文件描述符, 2>&1 的意思便是将打印到标准错误中的内容 转移 到标准输出中去——这个 转移 在shell中的术语便叫做重定向(redirection)。

2>&1 该放哪里?

bashman 文档中有一个名为 REDIRECTION 的章节专门介绍了重定向相关的内容,其中有一段有意思的内容

ls 不方便做演示,我准备了下面这一段Node.js代码

console.error('Print to standard error.');
console.log('Print to standard output.');

将代码保存到文件 foobar.js 中。

如果将 2>&1 写在后面,那么 foobar.log 中会包含两行

➜  /tmp node foobar.js > /tmp/foobar.log 2>&1
➜  /tmp cat /tmp/foobar.log
Print to standard error.
Print to standard output.

否则, foobar.log 中只含有一行内容,另一行会出现在终端上

➜  /tmp node foobar.js 2>&1 > /tmp/foobar.log
Print to standard error.
➜  /tmp cat /tmp/foobar.log
Print to standard output.

那么为什么会这样呢?

重定向的时候,shell在做些什么?

以执行 node foobar.js > /tmp/foobar.log 为例,当shell发现命令中含有重定向的符号时,便开始忙碌起来。

shell首先用 open 函数打开文件 /tmp/foobar.log ,拿到一个文件描述符(一个非负整数)。Node.js的 fs 模块中有一个 open 方法,在调用成功时,也是往回调函数传入文件描述符

const fs = require('fs');

fs.open('/tmp/cuckoo.log', function (err, fd) {
  console.log(`fd for cuckoo.log is ${fd}`);
  fs.open('/tmp/cuckoo.err', function (err, fd) {
    console.log(`fd for cuckoo.err is ${fd}`);
  });
});

比较奇妙的是,多次运行时拿到的文件描述符总是相同的

➜  /tmp date; node open.test.js
Fri May 22 21:00:56 CST 2020
fd for cuckoo.log is 21
fd for cuckoo.err is 24
➜  /tmp date; node open.test.js
Fri May 22 21:00:59 CST 2020
fd for cuckoo.log is 21
fd for cuckoo.err is 24

说回重定向。shell拿到文件描述符后,便调用 dup2 函数。既然有 dup2 ,那么就有 dupdup 接收一个文件描述符作为参数,返回一个新的文件描述符。而 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词条 也有一幅更严谨的版本。

相关文章