为什么/ dev / null是文件?为什么不将其功能实现为简单程序?


114

我试图理解Linux上特殊文件的概念。但是,据/dev我所知,当可以用C中的几行代码实现其功能时,在其中包含特殊文件似乎很愚蠢。

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


20
顺便说一句,这笔开销中的cat foo | bar大部分也是为什么(大规模)比差得多的原因bar <foocat是一个微不足道的程序,但即使是一个微不足道的程序也会产生成本(其中一些成本特定于FIFO语义-因为程序无法seek()在FIFO中使用,例如,可以通过寻求有效实施的程序最终可能会进行更昂贵的操作当给定管道时;使用类似的字符设备/dev/null可以伪造这些操作,或者使用真实文件可以执行这些操作,但是FIFO不允许进行任何类型的上下文感知处理。
查尔斯·达菲

13
grep blablubb file.txt 2>/dev/null && dosomething不能将null用作程序或函数。
rexkogitans

16
您可能会发现(了解或至少是拓宽了)Plan 9操作系统的启发性知识,以了解“一切都是文件”愿景的去向-更加容易地看到将资源作为文件可用的力量一旦您看到系统完全接受了该概念(而不是像现代Linux / Unix那样,大部分/部分),就可以使用路径。
mtraceur '18

25
除了没有人指出内核空间中运行的设备驱动程序 带有“几行C”的程序外,到目前为止,没有一个答案实际上解决了“访问同一文件的程序太多”的假设。在这个问题上。
JdeBP '18

12
RE“它的功能可以通过使用C线的少数实现”:你不会相信,但它通过用C线极少数实现!例如,readfor 的函数主体/dev/null由“ return 0”组成(这意味着它不执行任何操作,并且我想得出的结果是EOF):(来自静态github.com/torvalds/linux/blob/master/ drivers / char / mem.cssize_t read_null(struct file *file, char __user *buf, size_t count, loff_t *ppos) { return 0; }(哦,我刚刚看到@JdeBP已经指出了这一点。无论如何,这是图示:-)。
彼得·施耐德

Answers:


153

除了使用特殊字符设备带来的性能优势外,主要优势还在于模块化。/ 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

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

# Suppress errors, but print output.
$ grep foo * 2>/dev/null

14
另外,您/dev/null不仅要在shell的命令中使用。您可以在软件提供的其他参数中使用它,例如配置文件中。--- Fort这个软件非常方便。它/dev/null与常规文件之间没有区别。
pabouk '18

我不确定我很难理解有关分开重定向以接收可执行文件的部分。在C语言中,您只需执行一个pipeforkexecve其他过程管道,只需更改用于dup2建立连接的调用,对吗?这是真的最弹不提供的最漂亮的方式来做到这一点,但据推测,如果我们没有这么多的设备作为文件模式,大部分的东西/dev/proc被视为可执行文件,炮弹将已设计方法就像我们现在重定向一样轻松地做到这一点。
aschepler '18

6
@aschepler不重定向到接收器可执行文件很困难。就是说,如果空接收器不是文件,那么编写可以同时从两个文件空接收器进行写入/读取的应用程序将更加复杂。除非您所谈论的世界不是一切都是文件,而是一切都是可执行文件?这将是与* nix OS中的模型截然不同的模型。
立方

1
@aschepler你忘了wait4!您是正确的,当然可以使用POSIX API将stdout和stderr传递到不同的程序,并且有可能发明一种巧妙的shell语法来将stdout和stderr重定向到不同的命令。但是,我现在还不知道任何这样的shell,更大的一点是/ dev / null可以很好地适合现有工具(在很大程度上适用于文件),而/ bin / null则不能。我们还可以想象有一些IO API,可以使gcc轻松地(安全地!)输出到程序中,就像输出文件一样,但这不是我们所处的情况
。– ioctl

2
@ioctl关于外壳;zsh和bash都至少允许您执行类似的操作grep localhost /dev/ /etc/hosts 2> >(sed 's/^/STDERR:/' > errfile ) > >(sed 's/^/STDOUT:/' > outfile),结果需要单独处理,errfile以及outfile
Matija Nalis

62

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

$ file /dev/null
/dev/null: character special (3/2)

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


24
cat file | null首先要建立一个管道,产生一个进程,在新进程中执行“ null”,等等,null这会带来很多开销。此外,它本身会在循环中使用大量CPU,将字节读入缓冲区,然后再发送给缓冲区只是被丢弃... /dev/null在内核中的实现以这种方式更加有效。另外,如果您想/dev/null作为参数传递而不是重定向怎么办?(您可以<(...)在bash中使用,但这甚至会更重!)
filbranden

4
如果您必须通过管道传递给名为的程序,null而不是使用重定向至的方法/dev/null,是否有一种简单明了的方法来告诉Shell运行程序,同时仅将其stderr发送为null?
Mark Plotnick

5
对于开销演示来说,这是一个非常昂贵的设置。我建议/dev/zero改用。
克莱里斯

20
这些例子是错误的。dd of=-写入一个名为的文件-,只需省略of=写入标准输出,因为dd默认情况下是写入。false由于false无法读取标准输入而无法进行配管,因此dd将被SIGPIPE杀死。对于放弃它的输入,您可以使用一个命令cat > /dev/null。同样,可能与瓶颈无关的比较可能是此处的随机数生成。
斯特凡Chazelas

8
AST版本的ddetc.甚至在检测到目标为/ dev / null时都不会打扰系统调用。
Mark Plotnick

54

我怀疑为什么与Unix(进而是Linux)的构想/设计有很多关系,以及它的优势。

毫无疑问,不进行额外的处理会带来不可忽略的性能优势,但我认为还有更多的好处:早期的Unix有一个“一切都是文件”的比喻,如果您看一下,它具有非显而易见但优雅的优势。从系统角度来看,而不是从Shell脚本角度来看。

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

但是这里有两个练习:

  1. 让我们null使用现有的Unix工具/dev/null-easy:实现该程序cat >/dev/null。做完了

  2. 您可以/dev/null按照实施null吗?

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

考虑一下:几乎每种编程语言都已经需要使用文件,文件描述符和文件路径,因为它们从一开始就是Unix“一切都是文件”范例的一部分。

如果您所拥有的只是写到stdout的程序,那么该程序将不在乎您是否将它们重定向到可吞噬所有写操作的虚拟文件,或将管道重定向到可吞噬所有写操作的程序。

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

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

好的软件工程通常依赖于找到好的或“自然的”隐喻来抽象问题的某些元素,从而使问题变得更容易思考,仍保持灵活性,因此您可以解决基本上相同范围的高级问题,而不必花时间和精力在不断地为相同的较低级别的问题实施解决方案上。

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

Unix之所以取得成功,部分原因在于这些抽象如何协同工作,并且/dev/null最好是整体上的一部分。


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

例如,在Unix中,类似文件的特殊对象/dev/null必须在内核本身中实现,但是事实证明,以文件/文件夹形式公开功能是很有用的,此后便形成了多个为程序提供方法的系统要做到这一点。

第一个是Plan 9操作系统,它是由与Unix相同的一些人开发的。后来,GNU Hurd对其“翻译器”进行了类似的操作。同时,Linux最终获得了FUSE(目前也已经传播到其他主流系统)。


8
@PeterCordes的答案是从不了解设计的位置开始。如果每个人都已经了解了设计,那么这个问题将不复存在。
OrangeDog

1
@mtraceur:未经root许可挂载映像文件?显示了一些证据表明FUSE可能不需要root,但是我不确定。
彼得·科德斯

1
@PeterCordes RE:“似乎很奇怪”:这不是对设计的道歉,只是对如果您不考虑其下的系统实现并且尚未对系统有过犹豫的印象,那是一种承认。全设计优势。我试图通过“从shell脚本的角度”打开该句子,并在前面提到几个句子时,就暗示了它与系统角度之间的对比。进一步考虑,“看起来很奇怪”会更好,因此我将对其进行调整。我欢迎提出进一步的措词建议,以使其更清楚而不过于冗长。
mtraceur

2
作为一个年轻的工程师,我被告知与Unix有关的第一件事是“一切都是文件”,我发誓您会听到大写字母。尽早掌握这个想法使Unix / Linux看起来更容易理解。Linux继承了大多数设计哲学。我很高兴有人提到它。
StephenG

2
@ PeterCordes,DOS通过使魔术文件名NUL出现在每个目录中来“解决”键入问题,即您只需要输入即可> NUL
Cristian Ciupitu

15

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

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

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


10
这不仅是安装成本,还包括管道中的每次写入都需要内存复制,以及将上下文切换到读取程序。(或至少在管道缓冲区已满时进行上下文切换。读取器read在处理数据时必须进行另一次复制)。在设计Unix的单核PDP-11上,这是不可忽略的!今天的内存带宽/复制比以前便宜得多。一个write系统调用的FD打开/dev/null可以马上返回,甚至没有从缓冲区中读取任何数据。
彼得·科德斯

@PeterCordes,我的笔记是切线的,但自相矛盾的是,今天的内存写入比以往任何时候都更昂贵。一个8核CPU可能在一个时钟时间内执行16个整数运算,而一个端到端的存储器写入将在例如16个时钟(4GHz CPU,250 MHz RAM)中完成。这就是256的因数。现代CPU的RAM就像PDP-11 CPU的RL02,几乎就像外围存储单元!:)当然不是那么简单,但是所有命中高速缓存的东西都会被写出,而无用的写操作将剥夺其他计算的重要缓存空间。
kkm

@kkm:是的,浪费了内核管道缓冲区和null程序中读取缓冲区大约2x 128kiB的L3缓存占用空间,但是大多数多核CPU并非一直都在所有内核都忙着运行,所以运行该null程序的CPU时间大部分是免费的。在所有核心都已钉住的系统上,无用的管道更为重要。但是不可以,“热”缓冲区可以被重写很多次而无需刷新到RAM,因此我们主要是在争夺L3带宽,而不是高速缓存。这不是很好,特别是在SMT(超线程)系统上,同一物理上的其他逻辑内核正在竞争...
Peter Cordes '18

....但是您的内存计算非常有缺陷。现代CPU具有大量的内存并行性,因此,即使DRAM的延迟约为200-400个核心时钟周期且L3> 40,带宽约为8个字节/时钟。(令人惊讶的是,具有四通道内存的多核Xeon与四核台式机相比,到L3或DRAM的单线程带宽更差,因为它受一个核可以保持运行的请求的最大并发性的限制。 max_concurrency / latency:为什么Skylake在单线程内存吞吐量方面比Broadwell-E好得多?
Peter Cordes '18

...另请参阅7-cpu.com/cpu/Haswell.html,以了解比较四核与18核的Haswell数字。无论如何,是的,如果现代的CPU不必等待内存,它们每个时钟就可以完成大量的工作。您的数字似乎每个时钟只有2个ALU操作,例如1993年的Pentium或现代的低端双发行ARM。Ryzen或Haswell 可能每个时钟每个内核执行4个标量整数ALU ops + 2个内存ops ,或者使用SIMD执行更多操作。例如,Skylake-AVX512在每个内核上具有(每个内核)2个时钟的吞吐量vpaddd zmm:每条指令16个32位元素。
彼得·科德斯

7

除了“一切都是文件”,并因此在大多数其他答案所基于的地方都易于使用之外,还存在@ user5626466提到的性能问题。

为了展示实际效果,我们将创建一个简单的程序nullread.c

#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文件重定向,我们可以获得更快的速度(由于提到的事实:较少的上下文切换,内核仅忽略数据而不是复制数据等):

% 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

(这应该是此处的评论,但太大了,将完全无法阅读)


您在什么硬件上测试?与我在Skylake i7-6700k(DDR4-2666)上获得的23GB / s相比,4.8GB / s的速度非常低,但是该缓冲区应该在L3缓存中保持高温。因此,成本的很大一部分是Spectre导致系统调用的成本很高+启用了Meltdown缓解功能,这对管道系统造成了双重伤害,因为管道缓冲区小于1M,因此写入/读取系统调用更多。不过,性能差异近10倍比我预期的要差。 在我的Skylake系统上,性能为23GB / s。 3.3GB / s,运行x86-64 Linux 4.15.8-1-ARCH,因此是6.8的一倍。哇,系统调用现在很昂贵
Peter Cordes

1
@PeterCordes具有64k管道缓冲区的 3GB / s 表示每秒2x 103124 syscalls ...以及上下文切换的数量,呵呵。在服务器cpu上,每秒有200000 syscalls,由于工作集很少,您可能期望PTI产生约8%的开销。(我所参考的图形不包括PCID优化,但是对于小型工作集来说可能并不重要)。因此,我不确定PTI是否会在其中产生重大影响? brendangregg.com/blog/2018-02-09/...
sourcejedi

1
哦,有趣的是,它是一个Silvermont,带有2MB的L2缓存,因此您的dd缓冲区+接收缓冲区不合适。您可能正在处理内存带宽,而不是最后一级的缓存带宽。使用512k缓冲区甚至64k缓冲区可能会获得更好的带宽。(根据strace我的桌面上,writeread正在返回1048576,所以我认为这意味着我们只支付用户< - >。TLB失效+分支预测的核心成本齐平每一次MIB,而不是按64K,@sourcejedi它的幽灵我认为成本最高的缓解措施)
Peter Cordes

1
@sourcejedi:启用Spectre缓解后syscall,立即返回的成本ENOSYS在Skylake上启用Spectre缓解后约为1800个周期wrmsr根据@BeeOnRope的测试,其中大部分是使BPU无效的开销。禁用缓解功能后,用户->内核->用户往返时间约为160个周期。但是,如果您占用大量内存,则缓解崩溃也很重要。大页面应该有所帮助(需要重新加载的TLB条目更少)。
彼得·科德斯

1
@PeterCordes在单核unix系统上,我们肯定会看到每64K 1个上下文切换,或者无论管道缓冲区是什么,这都会很痛...实际上,我还看到[具有2个cpu内核的相同数量的上下文切换] (unix.stackexchange.com/questions/439260/…); 还必须将每个64k的睡眠/唤醒周期计为上下文切换(到名义上的“空闲进程”)。保持流水线进程在同一个CPU上的速度实际上要快两倍以上。
sourcejedi

6

您提出的问题似乎是,通过使用空程序代替文件可能会简化一些操作。也许我们可以摆脱“魔术文件”的概念,而只有“普通管道”。

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

考虑下面这个人为的例子:

$ echo -e 'foo\nbar\nbaz' | grep foo
foo

使用Bash的流程替换,我们可以通过一种更round回的方式来完成同一件事:

$ grep foo <(echo -e 'foo\nbar\nbaz')
foo

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

$ echo foo <(echo -e 'foo\nbar\nbaz')
foo /dev/fd/63

<(...)结构只是替换为文件名,而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

别处:

$ echo -e 'foo\nbar\nbaz' > foofifo

并且,grep将输出foo

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


@Lyle是吗?那为什么回声打印/dev/fd/63呢?
Phil Frost

嗯 好点子。嗯,这是由Shell实现的,所以您的Shell可能与我长大的Bourne Shell不同。
Lyle

一个区别是echo不会从stdin读取,而grep会读取,但是我想不到shell在执行它们之前如何知道这一点。
Lyle '18

1
strace确实使这一点更加清晰:对我来说。你用bash完全正确。'<(...)'构造与<filename截然不同。嗯 我学到了一些东西。
Lyle

0

我认为,除了历史范式和绩效之外,还有一个安全问题。无论多么简单,限制具有特权执行凭证的程序的数量都是系统安全性的基本原则。/dev/null由于系统服务的使用,当然必须要求替换者具有这种特权。现代安全框架在阻止漏洞利用方面做得非常出色,但并非万无一失。以文件访问的内核驱动设备很难利用。


这听起来像胡话。编写无错误的内核驱动程序并不比编写读取+丢弃其标准输入的无错误程序容易。它并不需要为setuid或任何东西,所以两个/dev/null或建议输入丢弃程序,攻击矢量是相同的:获取以root身份运行做一些奇怪的脚本或程序(如尝试lseek/dev/null或打开它是从同一进程多次执行的,或者IDK是什么。或者/bin/null在一个怪异的环境中进行调用,等等)。
彼得·科德斯

0

正如其他人已经指出的那样,/dev/null 由行代码屈指可数的程序。这些代码行只是内核的一部分。

更清楚地说,这是Linux的实现:字符设备在读取或写入时调用函数。写入/dev/null调用write_null,而读取调用read_null在此注册。

从字面上看是少数几行代码:这些函数什么都不做。仅当您计数读写以外的功能时,您才需要多于几行代码。


也许我应该说得更准确些。我的意思是为什么将它实现为char设备而不是程序。无论哪种方式都将花费几行,但是程序的实现肯定会更简单。正如其他答案所指出的那样,这样做有很多好处。效率和可移植性是其中的主要因素。
Ankur S

当然。我刚刚添加了这个答案,是因为看到实际的实现很有趣(我自己最近发现了它),但真正的原因是其他人确实指出了这一点。
Matthieu Moy

我也是!我最近开始在linux中学习设备,答案非常有
启发性

-2

我希望您也了解/ dev / chargen / dev / zero以及其他类似的东西,包括/ dev / null。

LINUX / UNIX中有一些-已提供,因此人们可以充分利用WELL书面代码标记。

Chargen旨在生成特定且重复的字符模式-速度非常快,并且将突破串行设备的极限,并且将有助于调试已编写的,在某些测试或其他测试中失败的串行协议。

零旨在填充现有文件或输出大量零

/ dev / null只是另一个具有相同想法的工具。

工具箱中的所有这些工具意味着您有一半的机会让现有程序执行独特的操作,而无需考虑它们(您的特定需求)作为设备或文件替换的可能性。

让我们设置一个竞赛,看看在您的LINUX版本中,只有很少的字符设备,谁才能产生最令人兴奋的结果。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.