为什么 /dev/null 是一个文件?为什么它的功能不作为一个简单的程序来实现?

Ank*_*r S 116 executable devices files null

我试图了解 Linux 上特殊文件的概念。然而,/dev根据我的知识,当它的功能可以通过 C 中的几行代码实现时,在其中包含一个特殊文件似乎很愚蠢。

此外,您可以以几乎相同的方式使用它,即通过管道进入null而不是重定向到/dev/null. 将其作为文件是否有特定原因?使它成为一个文件不会导致许多其他问题,例如太多程序访问同一文件吗?

小智 156

除了使用字符专用设备的性能优势外,主要优势是模块化。/dev/null 几乎可以在任何需要文件的上下文中使用,而不仅仅是在 shell 管道中。考虑接受文件作为命令行参数的程序。

# We don't care about log output.
$ frobify --log-file=/dev/null

# We are not interested in the compiled binary, just seeing if there are errors.
$ gcc foo.c -o /dev/null  || echo "foo.c does not compile!".

# Easy way to force an empty list of exceptions.
$ start_firewall --exception_list=/dev/null
Run Code Online (Sandbox Code Playgroud)

在这些情况下,使用程序作为源或接收器会非常麻烦。即使在 shell 管道的情况下,stdout 和 stderr 也可以独立地重定向到文件,这对于将可执行文件作为接收器来说是很难做到的:

# Suppress errors, but print output.
$ grep foo * 2>/dev/null
Run Code Online (Sandbox Code Playgroud)

  • 此外,您不要只在 shell 的命令中使用 `/dev/null`。您可以在提供给软件的其他参数中使用它——例如在配置文件中。---堡垒这个软件很方便。它不需要区分 `/dev/null` 和一个普通文件。 (14认同)
  • @aschepler 不重定向到接收器可执行文件很困难。如果空接收器不是文件,那么编写可以写入/读取两个文件_和_空接收器的应用程序会更加复杂。除非你在谈论一个世界,在这个世界里,一切都是文件,一切都是可执行文件?这与 *nix OS 中的模型非常不同。 (6认同)
  • @ioctl 关于 shell;zsh 和 bash 至少都允许你做类似`grep localhost /dev/ /etc/hosts 2> >(sed 's/^/STDERR:/' > errfile ) > >(sed 's/^/STDOUT :/' > outfile)`,结果分别处理了 `errfile` 和 `outfile` (2认同)

Dop*_*oti 62

公平地说,它本身不是一个常规文件;这是一个字符特殊设备

$ file /dev/null
/dev/null: character special (3/2)
Run Code Online (Sandbox Code Playgroud)

它作为设备而不是文件或程序运行意味着重定向输入或输出是一种更简单的操作,因为它可以附加到任何文件描述符,包括标准输入/输出/错误。

  • `猫文件| null` 会产生很多开销,首先是设置管道、产生进程、在新进程中执行“null”等。此外,`null` 本身会在循环中使用相当多的 CPU 将字节读入一个后来被丢弃的缓冲区......内核中`/dev/null`的实现只是更有效的方式。另外,如果你想传递 `/dev/null` 作为参数而不是重定向怎么办?(你可以在 bash 中使用 `<(...)`,但这甚至更重!) (24认同)
  • 这些例子是错误的。`dd of=-` 写入名为 `-` 的文件,只需省略 `of=` 写入标准输出,因为这是 `dd` 默认写入的位置。管道到 `false` 不起作用,因为 `false` 不读取其标准输入,因此 `dd` 将被 SIGPIPE 杀死。对于丢弃其输入的命令,您可以使用...`cat > /dev/null`。此外,可能与瓶颈无关的比较可能是这里的随机数生成。 (20认同)
  • `dd` 等的 AST 版本在检测到目标是 /dev/null 时甚至不会费心进行写入系统调用。 (8认同)
  • 对于架空演示来说,这是一个非常昂贵的设置。我建议改用`/dev/zero`。 (5认同)
  • 如果您必须通过管道传输到名为 `null` 的程序而不是使用重定向到 `/dev/null`,是否有一种简单明了的方法来告诉 shell 运行程序,同时将其 stderr 发送到 null? (4认同)
  • 一个程序_可以_用于间接寻址,但重定向(例如`cat file > /dev/null`)会用`file`的内容覆盖可执行文件而不是重定向输出。 (3认同)
  • @MarkPlotnick:大概 shell 语法会进化出像 `foo 2>(bar)` 或 `foo 2|(bar)` 这样的语法。(如果你想知道,在当前的 `bash` 4.4 中,`echo 2>(cat)` 会打印 `2/dev/fd/63`,因为 bash 会处理替换,但 echo 不会将其 args 视为文件名。 (并且连接前导的 `2` 无济于事)。) (2认同)
  • @psmears,无论如何,至少在 Linux 上,编写此类 `null` 命令的最有效方法可能是在 `/dev/null` 上使用 `splice()` 以至少避免必须复制的内容如果您使用 `read()` 来刷新管道的内容,就会将管道返回到用户空间。 (2认同)
  • @StéphaneChazelas:是的,确实是 :) 不幸的是 GNU `cat` 不使用 `splice()`(我知道它是 Linux 特定的,但其他 GNU 工具确实利用了 Linux 特定的功能,例如 tail 使用 inotify)。它也没有使用 MarkPlotnick 提到的优化...... (2认同)

mtr*_*eur 54

我怀疑原因与塑造 Unix(以及 Linux)的愿景/设计以及由此产生的优势有很大关系。

毫无疑问,不启动额外的进程有一个不可忽视的性能优势,但我认为还有更多:早期的 Unix 有一个“一切都是文件”的比喻,如果你看一下,它有一个不明显但优雅的优势它是从系统的角度,而不是从 shell 脚本的角度。

假设您有null命令行程序和/dev/null设备节点。从 shell 脚本的角度来看,该foo | null程序实际上是真正有用方便的,并且foo >/dev/null需要更长的时间来输入并且看起来很奇怪。

但这里有两个练习:

  1. 让我们null使用现有的 Unix 工具来实现该程序/dev/null- 简单:cat >/dev/null。完毕。

  2. 你可以实现/dev/null的方面null

您完全正确的是,仅丢弃输入的 C 代码是微不足道的,因此可能尚不清楚为什么为任务提供可用的虚拟文件很有用。

想一想:几乎每一种编程语言都已经需要处理文件、文件描述符和文件路径,因为它们从一开始就是 Unix 的“一切都是文件”范式的一部分。

如果您拥有的只是写入标准输出的程序,那么该程序并不关心您是将它们重定向到吞下所有写入的虚拟文件,还是将管道重定向到吞下所有写入的程序中。

现在,如果您的程序采用文件路径来读取或写入数据(大多数程序都这样做) - 并且您想向这些程序添加“空白输入”或“丢弃此输出”功能 - 好吧,/dev/null这是免费的。

请注意,它的优雅之处在于降低了所有相关程序的代码复杂性 - 对于您的系统可以作为具有实际“文件名”的“文件”提供的每个常见但特殊的用例,您的代码可以避免添加自定义命令-line 选项和自定义代码路径来处理。

好的软件工程通常依赖于找到好的或“自然”的隐喻,以一种更容易思考保持灵活的方式抽象问题的某些元素,这样您就可以解决基本相同范围的更高级别问题,而不必不断地将时间和精力花在重新实施相同较低级别问题的解决方案上。

“一切都是文件”似乎是访问资源的一个这样的比喻:您调用open分层命名空间中的给定路径,获取对对象的引用(文件描述符),并且您可以对文件描述符进行readwrite等。您的 stdin/stdout/stderr 也是刚好为您预先打开的文件描述符。您的管道只是文件和文件描述符,而文件重定向可让您将所有这些部分粘合在一起。

Unix 成功的部分原因在于这些抽象如何很好地协同工作,并且/dev/null最好将其理解为整体的一部分。


PS 值得一看 Unix 版本的“一切都是文件”以及诸如此类/dev/null的东西,这是迈向更灵活和强大的隐喻概括的第一步,该隐喻已在随后的许多系统中实现。

例如,在 Unix 中,类似文件的特殊对象/dev/null必须在内核本身中实现,但事实证明,以文件/文件夹形式公开功能已经足够有用了,从那时起,已经有多个系统为程序提供了一种方式要做到这一点。

第一个是 Plan 9 操作系统,它是由一些制造 Unix 的人制造的。后来,GNU Hurd 对它的“翻译器”做了类似的事情。与此同时,Linux 最终得到了 FUSE(现在它也已经传播到其他主流系统)。

  • @PeterCordes 答案的重点是从不了解设计的立场开始。如果每个人都已经理解了设计,这个问题就不存在了。 (8认同)
  • 作为一名年轻的工程师,我听到的第一件事是关于 Unix 的“一切都是文件”,我发誓你能听到大写字母。尽早掌握这个想法会使 Unix/Linux 看起来更容易理解。Linux 继承了大部分设计理念。我很高兴有人提到它。 (2认同)
  • @PeterCordes,DOS 通过使魔术文件名 [`NUL`](https://blogs.msdn.microsoft.com/oldnewthing/20031022-00/?p=42073) 出现在每个目录中来“解决”打字问题,即你只需要输入`> NUL`。 (2认同)

小智 14

出于性能原因,我认为/dev/null字符设备(其行为类似于普通文件)而不是程序。

如果是程序,则需要加载、启动、调度、运行,然后停止和卸载程序。您所描述的简单 C 程序当然不会消耗大量资源,但我认为在考虑大量(例如数百万)重定向/管道操作时会产生显着差异,因为流程管理操作在大规模上成本高昂它们涉及上下文切换。

另一个假设:管道进入程序需要接收程序分配内存(即使之后直接丢弃)。因此,如果您通过管道导入该工具,则会消耗双倍内存,一次是在发送程序上,另一次是在接收程序上。

  • 这不仅仅是设置成本,而是每次写入管道都需要内存复制,以及到读取程序的上下文切换。(或者至少在管道缓冲区已满时进行上下文切换。读取器在读取数据时必须进行另一次复制)。这在设计 Unix 的单核 PDP-11 上是不可忽视的!今天的内存带宽/复制比当时便宜得多。对在 `/dev/null` 上打开的 FD 的 `write` 系统调用可以立即返回,甚至无需从缓冲区读取任何数据。 (10认同)

Mat*_*lis 7

除了“一切都是一个文件”以及大多数其他答案所基于的任何地方的易用性之外,还有@user5626466 提到的性能问题。

为了在实践中展示,我们将创建一个简单的程序,名为nullread.c

#include <unistd.h>
char buf[1024*1024];
int main() {
        while (read(0, buf, sizeof(buf)) > 0);
}
Run Code Online (Sandbox Code Playgroud)

并编译它 gcc -O2 -Wall -W nullread.c -o nullread

(注意:我们不能在管道上使用 lseek(2),所以排空管道的唯一方法是从中读取直到它为空)。

% time dd if=/dev/zero bs=1M count=5000 |  ./nullread
5242880000 bytes (5,2 GB, 4,9 GiB) copied, 9,33127 s, 562 MB/s
dd if=/dev/zero bs=1M count=5000  0,06s user 5,66s system 61% cpu 9,340 total
./nullread  0,02s user 3,90s system 41% cpu 9,337 total
Run Code Online (Sandbox Code Playgroud)

而使用标准/dev/null文件重定向,我们可以获得更好的速度(由于提到的事实:更少的上下文切换,内核只是忽略数据而不是复制数据等):

% time dd if=/dev/zero bs=1M count=5000 > /dev/null
5242880000 bytes (5,2 GB, 4,9 GiB) copied, 1,08947 s, 4,8 GB/s
dd if=/dev/zero bs=1M count=5000 > /dev/null  0,01s user 1,08s system 99% cpu 1,094 total
Run Code Online (Sandbox Code Playgroud)

(这应该是那里的评论,但太大了,完全不可读)


Phi*_*ost 6

您提出的问题好像通过使用空程序代替文件可以简单地获得某些东西。也许我们可以摆脱“魔法文件”的概念,而只有“普通管道”。

但是考虑一下,管道也是文件。它们通常没有命名,因此只能通过它们的文件描述符进行操作。

考虑这个有点人为的例子:

$ echo -e 'foo\nbar\nbaz' | grep foo
foo
Run Code Online (Sandbox Code Playgroud)

使用 Bash 的进程替换,我们可以通过更迂回的方式完成同样的事情:

$ grep foo <(echo -e 'foo\nbar\nbaz')
foo
Run Code Online (Sandbox Code Playgroud)

替换grepfor echo,我们可以在封面下看到:

$ echo foo <(echo -e 'foo\nbar\nbaz')
foo /dev/fd/63
Run Code Online (Sandbox Code Playgroud)

<(...)构造只是用文件名替换,并且 grep 认为它正在打开任何旧文件,它恰好被命名为/dev/fd/63. 这/dev/fd是一个神奇的目录,它为访问它的文件所拥有的每个文件描述符创建命名管道。

我们可以通过mkfifo创建一个显示在ls所有内容中的命名管道来减少魔法,就像一个普通文件:

$ mkfifo foofifo
$ ls -l foofifo 
prw-rw-r-- 1 indigo indigo 0 Apr 19 22:01 foofifo
$ grep foo foofifo
Run Code Online (Sandbox Code Playgroud)

别处:

$ echo -e 'foo\nbar\nbaz' > foofifo
Run Code Online (Sandbox Code Playgroud)

看哪,grep 将输出foo.

我认为一旦你意识到管道和常规文件以及像 /dev/null 这样的特殊文件都只是文件,很明显,实现一个空程序会更加复杂。内核必须以任何一种方式处理对文件的写入,但在 /dev/null 的情况下,它可以将写入放在地板上,而使用管道,它必须实际将字节传输到另一个程序,然后必须实际上阅读它们。