我试图了解Linux上特殊文件的概念。但是,据我所知,在/dev中有一​​个特殊文件似乎很愚蠢,因为它的功能可以用C中的几行代码实现。

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

评论

顺便说一句,大部分开销也是为什么cat foo | bar(在规模上)比bar
grep blablubb file.txt 2> / dev / null && dosomething无法将null用作程序或函数。

阅读Plan 9操作系统以了解“一切都是文件”的愿景在哪里,您可能会感到启发(或者至少是精打细算),这比查看将资源作为文件可用的强大功能要容易得多。一旦您看到系统完全包含了该概念(而不是像现代Linux / Unix那样大部分/部分),就会出现路径。

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

关于“它的功能可以由C中的几行实现”:您不会相信,但是它由C中的几行实现!例如,/ dev / null的read函数的主体由“ return 0”组成(这意味着它不执行任何操作,而且我想会导致EOF):(来自静态github.com/torvalds/linux /blob/master/drivers/char/mem.c)ssize_t read_null(结构文件* file,char __user * buf,size_t count,loff_t * ppos){返回0; }(哦,我刚刚看到@JdeBP已经指出了这一点。无论如何,这是图示:-)。

#1 楼

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


评论


另外,您不只是在Shell的命令中使用/ dev / null。您可以在提供给软件的其他参数中使用它,例如配置文件中。 ---堡软件这很方便。它不需要/ dev / null和常规文件之间的区别。

– pabouk
18年4月17日在8:48



我不确定我很难理解有关单独重定向以接收可执行文件的部分。在C语言中,您只需像对其他处理管道那样执行管道,派生和执行操作,只需更改用于建立连接的dup2调用即可,对吗?的确,大多数shell并没有提供最漂亮的方法来执行此操作,但是想必如果我们没有那么多的“设备即文件”模式,并且/ dev和/ proc中的大多数内容都被视为可执行文件,则shell会拥有与我们现在重定向一样轻松地进行了设计。

–骗子
18年4月17日在23:43

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

–立方
18年4月18日在14:02

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

– ioctl
18年4月19日在1:14

@ioctl关于外壳;至少zsh和bash都将允许您执行grep localhost / dev / / etc / hosts之类的操作2>>(sed's / ^ / STDERR:/'> errfile)>>(sed's / ^ / STDOUT: /'> outfile),则分别处理errfile和outfile

– Matija Nalis
18年4月19日在10:17

#2 楼

公平地说,它本身不是常规文件;它是字符专用设备:

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


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

评论


猫文件| null将有很多开销,首先是建立管道,生成进程,在新进程中执行“ null”等。此外,null本身会在循环中使用大量CPU,将字节读取到缓冲区中,后来被丢弃了……在内核中/ dev / null的实现更有效。另外,如果要传递/ dev / null作为参数而不是重定向该怎么办? (您可以在bash中使用<(...),但这甚至更重了!)

–filbranden
18年4月16日在16:31

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

– Mark Plotnick
18年4月16日在19:09



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

–克莱里斯-谨慎乐观-
18年4月16日在23:19

这些例子是错误的。 dd of =-写入名为-的文件,只需省略of =即可将其写入stdout,因为dd默认是在此处写入。用管道输送到false无效,因为false不会读取其stdin,因此dd将被SIGPIPE杀死。对于放弃其输入的命令,可以使用... cat> / dev / null。同样,可能与瓶颈无关的比较可能是此处的随机数生成。

–StéphaneChazelas
18年4月17日在8:58



当AST等AST版本检测到目标为/ dev / null时,甚至不必理会写syscall。

– Mark Plotnick
18年4月17日在11:48

#3 楼

我怀疑为什么与塑造Unix(进而是Linux)的愿景/设计有很多关系,以及它所产生的优势。

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

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

但是这里有两个练习:


让我们使用现有的Unix工具和null-easy:/dev/null来实现cat >/dev/null程序。完成。
可以按照/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(目前也已经传播到其他主流系统)。

评论


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

–橙色狗
18年4月17日在11:46

@mtraceur:未经root许可挂载映像文件?显示了一些证据表明FUSE可能不需要root用户,但我不确定。

– Peter Cordes
18年4月17日在17:48



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

–mtraceur
18年4月17日在17:55

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

– StephenG
18年4月17日在20:55

@ PeterCordes,DOS通过使魔术文件名NUL出现在每个目录中来“解决”了键入问题,即您需要输入的所有内容都是> NUL。

–克里斯蒂安·丘皮图
18年4月18日在13:01

#4 楼

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

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

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

评论


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

– Peter Cordes
18年4月17日在2:41

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

–kkm
18年4月19日在2:14

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

– Peter Cordes
18年4月19日在3:18

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

– Peter Cordes
'18 Apr 19在3:23



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

– Peter Cordes
18年4月19日在3:31

#5 楼

除了“一切都是文件”,并因此在大多数其他答案所基于的地方都易于使用之外,还存在@ 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
18年4月20日在8:40



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

– sourcejedi
18年4月20日在9:24

哦,有趣的是,这是一个具有2MB L2缓存的Silvermont,因此您的dd缓冲区+接收缓冲区不合适;您可能正在处理内存带宽,而不是最后一级的缓存带宽。使用512k缓冲区甚至64k缓冲区可能会获得更好的带宽。 (根据我桌面上的strace,写入和读取返回1048576,所以我认为这意味着我们只需要为每个MiB而不是每个64k @@ jesjedi支付用户<-> TLB无效+分支预测刷新的内核成本(我认为,幽灵缓解成本最高)

– Peter Cordes
18年4月20日在9:34



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

– Peter Cordes
18年4月21日在23:43

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

– sourcejedi
18年4月22日在10:13



#6 楼

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

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

考虑一下这个人为的示例:

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


使用Bash的过程替换可以通过更round回的方式完成相同的事情:

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


grep替换为echo,我们可以看到下面的内容:

$ 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)
18年4月21日在20:30

嗯好点子。好吧,这是由Shell实现的,因此您的Shell可能与我长大的Bourne Shell不同。

–莱尔
18年4月21日在20:44

一个区别是echo不会从stdin读取,而grep会读取,但是我不认为shell在执行它们之前会怎么知道。

–莱尔
18年4月21日在20:46

strace确实使这一点更加清晰:对我来说。你用bash完全正确。 '<(...)'构造与
–莱尔
18年4月21日在20:59

#7 楼

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

评论


这听起来像胡话。编写无错误的内核驱动程序并不比编写读取+丢弃其标准输入的无错误程序容易。它不需要是setuid或其他任何东西,因此对于/ dev / null或建议的输入丢弃程序,攻击向量都是相同的:获取以root身份运行的脚本或程序来做一些奇怪的事情(例如try在/ dev / null中查找或在同一进程中多次打开它(或IDK什么),或者在一个怪异的环境中调用/ bin / null,等等)。

– Peter Cordes
18年4月21日在23:51

#8 楼

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

为了更清楚起见,这是Linux实现:字符设备在读取或写入时调用函数。写入/dev/null时将调用write_null,而读取时将调​​用read_null(在此处注册)。

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

评论


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

– Ankur S
18-4-23在15:24



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

–马修·莫伊(Matthieu Moy)
18年4月23日在16:24

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

– Ankur S
18年4月23日在16:35

#9 楼

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

LINUX / UNIX有一些这样的东西-可供人们使用

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

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

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

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

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