# 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
#include <unistd.h>
char buf[1024*1024];
int main() {
while (read(0, buf, sizeof(buf)) > 0);
}
并编译它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
除了使用字符特殊设备的性能优势外,主要优势是模块化。/dev/null 几乎可以在任何需要文件的上下文中使用,而不仅仅是在 shell 管道中。考虑接受文件作为命令行参数的程序。
这些都是使用程序作为源或接收器非常麻烦的情况。即使在 shell 管道的情况下,stdout 和 stderr 也可以独立地重定向到文件,这对于作为接收器的可执行文件来说是很难做到的:
公平地说,它本身不是一个常规文件。这是一个字符特殊设备:
它用作设备而不是文件或程序意味着将输入重定向到它或从它输出重定向是一种更简单的操作,因为它可以附加到任何文件描述符,包括标准输入/输出/错误。
我怀疑为什么与塑造 Unix(以及随后的 Linux)的愿景/设计以及由此产生的优势有很大关系。
毫无疑问,不启动额外进程会带来不可忽视的性能优势,但我认为还有更多:早期的 Unix 有一个“一切都是文件”的比喻,如果你看一下,它有一个不明显但优雅的优势它是从系统的角度,而不是从 shell 脚本的角度。
假设您有
null
命令行程序和/dev/null
设备节点。从 shell 脚本的角度来看,该foo | null
程序实际上是真正有用和方便的,而且foo >/dev/null
输入时间稍长,而且看起来很奇怪。但这里有两个练习:
让我们
null
使用现有的 Unix 工具和/dev/null
- 简单:cat >/dev/null
. 完毕。你能
/dev/null
在 方面实施null
吗?您绝对正确,仅丢弃输入的 C 代码是微不足道的,因此可能尚不清楚为什么拥有可用于该任务的虚拟文件很有用。
考虑一下:几乎每种编程语言都需要处理文件、文件描述符和文件路径,因为它们从一开始就是 Unix 的“一切都是文件”范式的一部分。
如果您只有写入标准输出的程序,那么程序并不关心您是否将它们重定向到吞下所有写入的虚拟文件,或者将管道重定向到吞下所有写入的程序中。
现在,如果您的程序采用文件路径来读取或写入数据(大多数程序都这样做)-并且您想向这些程序添加“空白输入”或“丢弃此输出”功能-那么,
/dev/null
这是免费的。请注意,它的优雅之处在于降低了所有相关程序的代码复杂性 - 对于您的系统可以作为具有实际“文件名”的“文件”提供的每个常见但特殊的用例,您的代码可以避免添加自定义命令-line 选项和要处理的自定义代码路径。
好的软件工程通常依赖于找到好的或“自然”的隐喻,以一种更容易思考但保持灵活的方式抽象问题的某些元素,这样您就可以解决基本相同范围的高级问题,而不必不断地花时间和精力重新实现相同的较低级别问题的解决方案。
“一切都是文件”似乎是访问资源的一种隐喻:您调用
open
分层命名空间中的给定路径,获取对对象的引用(文件描述符),并且您可以在文件描述符上使用read
and等。write
您的 stdin/stdout/stderr 也是恰好为您预先打开的文件描述符。您的管道只是文件和文件描述符,文件重定向可让您将所有这些部分粘合在一起。Unix 之所以成功,部分原因在于这些抽象如何很好地协同工作,并且
/dev/null
最好将其理解为整体的一部分。PS 值得一看 Unix 版本的“一切都是文件”之类的东西
/dev/null
,是迈向更灵活、更强大的隐喻泛化的第一步,该隐喻已在随后的许多系统中实现。例如,在 Unix 中,类似文件的特殊对象
/dev/null
必须在内核本身中实现,但事实证明,以文件/文件夹形式公开功能已经足够有用,从那时起,已经制作了多个系统,为程序提供了一种方式要做到这一点。第一个是Plan 9 操作系统,它是由一些制造Unix 的人制造的。后来,GNU Hurd 用它的“翻译器”做了类似的事情。与此同时,Linux 最终获得了 FUSE(现在它也已经传播到其他主流系统)。
出于性能原因,我认为
/dev/null
是字符设备(其行为类似于普通文件)而不是程序。如果它是一个程序,它将需要加载、启动、调度、运行,然后停止和卸载程序。您所描述的简单 C 程序当然不会消耗大量资源,但我认为在考虑大量(比如数百万)重定向/管道操作时会产生显着差异,因为流程管理操作大规模成本高昂,因为它们涉及上下文切换。
另一个假设:管道进入程序需要接收程序分配内存(即使之后直接丢弃)。因此,如果您通过管道输入该工具,则会消耗双倍的内存,一次是在发送程序上,另一次是在接收程序上。
除了“一切都是文件”以及大多数其他答案所基于的任何地方都易于使用之外,还存在@user5626466 提到的性能问题。
为了在实践中展示,我们将创建一个名为的简单程序
nullread.c
:并编译它
gcc -O2 -Wall -W nullread.c -o nullread
(注意:我们不能在管道上使用 lseek(2) ,因此排空管道的唯一方法是读取它直到它为空)。
而使用标准
/dev/null
文件重定向我们可以获得更好的速度(由于提到的事实:更少的上下文切换,内核只是忽略数据而不是复制它等):(这应该是一个评论,但是对于那个来说太大了并且完全不可读)
您提出的问题好像通过使用空程序代替文件可能会简单地获得一些东西。也许我们可以摆脱“魔术文件”的概念,而只使用“普通管道”。
但是考虑一下,管道也是文件。它们通常没有命名,因此只能通过它们的文件描述符进行操作。
考虑这个有点做作的例子:
使用 Bash 的进程替换,我们可以通过更迂回的方式完成同样的事情:
替换
grep
forecho
,我们可以在幕后看到:该
<(...)
构造只是替换为文件名,并且 grep 认为它正在打开任何旧文件,它恰好被命名为/dev/fd/63
. 这/dev/fd
是一个神奇的目录,它为访问它的文件所拥有的每个文件描述符创建命名管道。我们可以通过
mkfifo
创建一个显示在ls
所有内容中的命名管道来减少魔法,就像一个普通文件一样:别处:
看哪, grep 将输出
foo
.我认为,一旦您意识到管道和常规文件以及 /dev/null 之类的特殊文件都只是文件,那么显然实现 null 程序会更加复杂。内核必须以任何一种方式处理对文件的写入,但在 /dev/null 的情况下,它可以将写入丢弃在地板上,而使用管道它必须实际将字节传输到另一个程序,然后必须实际阅读它们。
我会争辩说,除了历史范式和性能之外,还有一个安全问题。限制具有特权执行凭证的程序的数量,无论多么简单,都是系统安全的基本原则。由于系统服务的使用,替换
/dev/null
肯定需要具有这样的权限。现代安全框架在防止漏洞利用方面做得很好,但它们并非万无一失。作为文件访问的内核驱动设备更难利用。正如其他人已经指出的那样,
/dev/null
是一个由几行代码组成的程序。只是这些代码行是内核的一部分。为了更清楚,这里是 Linux 实现:字符设备在读取或写入时调用函数。写入
/dev/null
调用write_null,而读取调用read_null ,在此处注册。从字面上看,几行代码:这些函数什么都不做。仅当您计算读取和写入以外的功能时,您需要的代码行数比手上的手指还多。
我希望你也知道 /dev/chargen /dev/zero 和其他类似的,包括 /dev/null。
LINUX/UNIX 提供了其中的一些 - 使人们可以充分利用编写良好的代码片段。
Chargen 旨在生成特定且重复的字符模式 - 它非常快,并且会突破串行设备的限制,并且有助于调试已编写且未通过某些测试或其他测试的串行协议。
零旨在填充现有文件或输出大量零
/dev/null 只是另一个具有相同想法的工具。
您工具包中的所有这些工具意味着您有一半的机会让现有程序做一些独特的事情,而无需考虑它们(您的特定需求)作为设备或文件替换
让我们举办一场比赛,看看谁能在您的 LINUX 版本中仅使用少数字符设备就产生最令人兴奋的结果。