114

I am trying to understanding the concept of special files on Linux. However, having a special file in /dev seems plain silly when its function could be implemented by a handful of lines in C to my knowledge.

Moreover you could use it in pretty much the same manner, i.e. piping into null instead of redirecting into /dev/null. Is there a specific reason for having it as a file? Doesn't making it a file cause many other problems like too many programs accessing the same file?

  • 20
    Incidentally, much of this overhead is also why cat foo | bar is much worse (at scale) than bar <foo. cat is a trivial program, but even a trivial program creates costs (some of them specific to FIFO semantics -- because programs can't seek() inside FIFOs, for example, a program that could be implemented efficiently with seeking can end up doing much more expensive operations when given a pipeline; with a character device like /dev/null it can fake those operations, or with a real file it can implement them, but a FIFO doesn't allow any kind of contextually-aware handling). – Charles Duffy Apr 16 '18 at 19:46
  • 13
    grep blablubb file.txt 2>/dev/null && dosomething could not work with null being a program or a function. – rexkogitans Apr 16 '18 at 20:39
  • 16
    You might find it enlightening (or at least mind-expanding) to read about the Plan 9 operating system to see where the "everything is a file" vision was going - it becomes a little easier to see the power of having resources available as file paths once you see a system fully embracing the concept (rather than mostly/partially, like modern Linux/Unix do). – mtraceur Apr 16 '18 at 21:06
  • 25
    As well as no-one pointing out that a device driver running in kernel space is a program with "a handful of lines of C", none of the answers so far have actually addressed the supposition of "too many programs accessing the same file" in the question. – JdeBP Apr 17 '18 at 8:00
  • 12
    Re "its function could be implemented by a handful of lines in C": You wouldn't believe it, but it is implemented by a handful of lines in C! For example, the body of the read function for /dev/null consists of a "return 0" (meaning it doesn't do anything and, I suppose, results in an EOF): (From static github.com/torvalds/linux/blob/master/drivers/char/mem.c) ssize_t read_null(struct file *file, char __user *buf, size_t count, loff_t *ppos) { return 0; } (Oh, I just see that @JdeBP made that point already. Anyway, here is the illustration :-). – Peter A. Schneider Apr 17 '18 at 16:39
153

In addition to the performance benefits of using a character-special device, the primary benefit is modularity. /dev/null may be used in almost any context where a file is expected, not just in shell pipelines. Consider programs that accept files as command-line parameters.

# 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

These are all cases where using a program as a source or sink would be extremely cumbersome. Even in the shell pipeline case, stdout and stderr may be redirected to files independently, something that is difficult to do with executables as sinks:

# Suppress errors, but print output.
$ grep foo * 2>/dev/null
  • 14
    Also you do not use /dev/null just in the shell's commands. You can use it in other parameters supplied to a software --- for example in configuration files. --- Fort the software this is very convenient. It does not need to make a difference between /dev/null and a regular file. – pabouk Apr 17 '18 at 8:48
  • I'm not sure I understand the part about separate redirections to sink executables being difficult. In C, you just do a pipe, fork, and execve like any other process piping, just with changes to the dup2 calls that set up the connections, right? It's true most shells don't offer the prettiest ways to do that, but presumably if we didn't have so much device-as-file pattern and most of the things in /dev and /proc were treated as executables, shells would have been designed with ways to do it as easily as we redirect now. – aschepler Apr 17 '18 at 23:43
  • 6
    @aschepler It's that not redirecting to sink executables is difficult. It's that writing applications that can write to/read from both files and the null sink would be more complicated if the null sink wasn't a file. Unless you're talking about a world where instead of everything being a file, everything is an executable? That'd be a very different model than what you have in *nix OS. – Cubic Apr 18 '18 at 14:02
  • 1
    @aschepler You forgot wait4! You are correct, it is certainly possible to pipe stdout and stderr to different programs using POSIX apis, and it may be possible to invent a clever shell syntax for redirecting stdout and stderr to different commands. However, I'm not aware of any such shell right now, and the larger point is that /dev/null fits neatly into existing tooling (which largely works with files), and /bin/null wouldn't. We could also imagine some IO API that makes it as easy for gcc to (securely!) output to a program as to a file, but that's not the situation we are in. – ioctl Apr 19 '18 at 1:14
  • 2
    @ioctl regarding shells; both zsh and bash at least will allow you to do things like grep localhost /dev/ /etc/hosts 2> >(sed 's/^/STDERR:/' > errfile ) > >(sed 's/^/STDOUT:/' > outfile), resulting is separately processed errfile and outfile – Matija Nalis Apr 19 '18 at 10:17
62

In fairness, it's not a regular file per se; it's a character special device:

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

It functioning as a device rather than as a file or program means that it's a simpler operation to redirect input to or output from it, as it can be attached to any file descriptor, including standard input/output/error.

  • 24
    cat file | null would have a lot of overhead, first in setting up a pipe, spawning a process, executing "null" in the new process, etc. Also, null itself would use quite a bit of CPU in a loop reading bytes into a buffer that is later just discarded... The implementation of /dev/null in the kernel is just more efficient that way. Also, what if you want to pass /dev/null as an argument, instead of a redirection? (You could use <(...) in bash, but that's even way heavier handed!) – filbranden Apr 16 '18 at 16:31
  • 4
    If you had to pipe to a program named null Instead of using redirection to /dev/null, would there be a simple, clear way to tell the shell to run a program while sending just its stderr to null? – Mark Plotnick Apr 16 '18 at 19:09
  • 5
    This is a really expensive setup for an overhead demonstration. I would suggest using /dev/zero instead. – chrylis Apr 16 '18 at 23:19
  • 20
    Those examples are wrong. dd of=- writes to a file called -, just omit the of= to write to stdout as that's where dd writes by default. Piping to false wouldn't work as false doesn't read its stdin, so dd would be killed with a SIGPIPE. For a command that discards its input you can use... cat > /dev/null. Also the comparison who probably be irrelevant as the bottle neck would probably be the random number generation here. – Stéphane Chazelas Apr 17 '18 at 8:58
  • 8
    The AST versions of dd etc. don't even bother doing a write syscall when they detect the destination is /dev/null. – Mark Plotnick Apr 17 '18 at 11:48
54

I suspect the why has a lot to do with the vision/design that shaped Unix (and consequently Linux), and the advantages stemming from it.

No doubt there's a non-negligible performance benefit to not spinning up an extra process, but I think there's more to it: Early Unix had an "everything is a file" metaphor, which has a non-obvious but elegant advantage if you look at it from a system perspective, rather than a shell scripting perspective.

Say you have your null command-line program, and /dev/null the device node. From a shell-scripting perspective, the foo | null program is actually genuinely useful and convenient, and foo >/dev/null takes a tiny bit longer to type and can seem weird.

But here's two exercises:

  1. Let's implement the program null using existing Unix tools and /dev/null - easy: cat >/dev/null. Done.

  2. Can you implement /dev/null in terms of null?

You're absolutely right that the C code to just discard input is trivial, so it might not yet be obvious why it's useful to have a virtual file available for the task.

Consider: almost every programming language already needs to work with files, file descriptors, and file paths, because they were part of Unix's "everything is a file" paradigm from the beginning.

If all you have are programs that write to stdout, well, the program doesn't care if you redirect them into a virtual file that swallows all writes, or a pipe into a program that swallows all writes.

Now if you have programs that take file paths for either reading or writing data (which most programs do) - and you want to add "blank input" or "discard this output" functionality to those programs - well, with /dev/null that comes for free.

Notice that the elegance of it is that is reduces the code complexity of all involved programs - for each common-but-special usecase that your system can provide as a "file" with an actual "filename", your code can avoid adding custom command-line options and custom code paths to handle.

Good software engineering often depends on finding good or "natural" metaphors for abstracting some element of a problem in a way that becomes easier to think about but remains flexible, so that you can solve basically the same range of higher-level problems without having to spend the time and mental energy on reimplementing solutions to the same lower-level problems constantly.

"Everything is a file" seems to be one such metaphor for accessing resources: You call open of a given path in a heirarchical namespace, getting a reference (file descriptor) to the object, and you can read and write, etc on the file descriptors. Your stdin/stdout/stderr are also file descriptors that just happened to be pre-opened for you. Your pipes are just files and file descriptors, and file redirection lets you glue all these pieces together.

Unix succeeded as much as it did in part because of how well these abstractions worked together, and /dev/null is best understood as part of that whole.


P.S. It's worth looking at the Unix version of "everything is a file" and things like /dev/null as the first steps towards a more flexible and powerful generalization of the metaphor that has been implemented in many systems that followed.

For example, in Unix special file-like objects like /dev/null had to be implemented in the kernel itself, but it turns out that it's useful enough to expose functionality in file/folder form that since then multiple systems have been made that provide a way for programs to do that.

One of the first was the Plan 9 operating system, made by some of the same people who made Unix. Later, GNU Hurd did something similar with its "translators". Meanwhile, Linux ended up getting FUSE (which has spread to the other mainstream systems by now as well).

  • 8
    @PeterCordes the point of the answer is starting from a position of not understanding the design. If everyone already understood the design, this question would not exist. – OrangeDog Apr 17 '18 at 11:46
  • 1
    @mtraceur: Mount an image file without root permission? shows some evidence that FUSE might not require root, but I'm not sure. – Peter Cordes Apr 17 '18 at 17:48
  • 1
    @PeterCordes RE: "seems weird": It's not an apology for the design, just an acknowledgement of how it can seem if you're not thinking of the system implementation under it, and haven't yet had the eureka moment about the system-wide design advantages. I tried to make that clear by opening that sentence with "from a shell scripting perspective", and alluding to the contrast between that vs. a system perspective a couple sentences prior. On further thought "can seem weird" is better, so I'll tweak it to that. I welcome further wording suggestions to make it clearer without making it too verbose. – mtraceur Apr 17 '18 at 17:55
  • 2
    The very first thing I was told as a young engineer in relation to Unix was "Everything Is A File" and I swear you could hear the capitals. And getting hold of that idea early makes Unix/Linux seem a lot more easy to understand. Linux inherited most of that design philosophy. I'm glad someone mentioned it. – StephenG Apr 17 '18 at 20:55
  • 2
    @PeterCordes, DOS "solved" the typing problem by making the magic filename NUL appear in every directory, i.e. all you have to type is > NUL. – Cristian Ciupitu Apr 18 '18 at 13:01
15

I think /dev/null is a character device (that behaves like an ordinary file) instead of a programm for performance reasons.

If it would be a program it would require loading, starting, scheduling, running, and afterwards stopping and unloading the program. The simple C program you are describing would of course not consume a lot of ressources, but I think it makes a significant difference when considering a large number (say millions) of redirect / piping actions as process management operations are costly on a large scale as they involve context switches.

Another assumption: Piping into an program requires memory to be allocated by the receiving program (even if it is discarded directly afterwards). So if you pipe into the tool you have the double memory consumption, once on the sending program and again on the receiving program.

  • 10
    It's not just the setup cost, it's that every write into a pipe requires memory copying, and a context switch to reading program. (Or at least a context switch when the pipe buffer is full. And the reader has to do another copy when it reads the data). This is not negligible on a single-core PDP-11 where Unix was designed! Memory bandwidth / copying is much cheaper today than it was then. A write system call to an FD open on /dev/null can return right away without even reading any data from the buffer. – Peter Cordes Apr 17 '18 at 2:41
  • @PeterCordes, my note is tangential, but it's possible that, paradoxically, memory writes today are more expensive than ever. An 8-core CPU potentially performs 16 integer operations in a clock time, while an end-to-end memory write would complete in e. g. 16 clocks (4GHz CPU, 250 MHz RAM). That's the factor of 256. RAM to the modern CPU is like an RL02 to the PDP-11 CPU, almost like a peripheral storage unit! :) Not as straightforward, naturally, but everything hitting the cache will get written out, and useless writes would deprive other computations of the ever important cache space. – kkm Apr 19 '18 at 2:14
  • @kkm: Yes, wasting about 2x 128kiB of L3 cache footprint on a pipe buffer in the kernel and a read buffer in the null program would suck, but most multi-core CPUs don't run with all cores busy all the time, so the CPU time to run the null program is mostly free. On a system with all cores pegged, useless piping is a bigger deal. But no, a "hot" buffer can be rewritten many times without getting flushed to RAM, so we're mostly just competing for L3 bandwidth, not cache. Not great, especially on a SMT (hyperthreading) system where other logical core(s) on the same physical are competing... – Peter Cordes Apr 19 '18 at 3:18
  • .... But your memory calculation is very flawed. Modern CPUs have lots of memory parallelism, so even though latency to DRAM is something like 200-400 core clock cycles and L3>40, bandwidth is ~8 bytes / clock. (Surprisingly, single-threaded bandwidth to L3 or DRAM is worse on a many-core Xeon with quad-channel memory vs. a quad-core desktop, because it's limited by the max concurrency of requests one core can keep in flight. bandwidth = max_concurrency / latency: Why is Skylake so much better than Broadwell-E for single-threaded memory throughput?) – Peter Cordes Apr 19 '18 at 3:23
  • ... See also 7-cpu.com/cpu/Haswell.html for Haswell numbers comparing quad-core vs. 18-core. Anyway, yes modern CPUs can get a ridiculous amount of work done per clock, if they aren't stuck waiting for memory. Your numbers appear to only be 2 ALU operations per clock, like maybe a Pentium from 1993, or a modern low-end dual-issue ARM. A Ryzen or Haswell potentially performs 4 scalar integer ALU ops + 2 memory ops per core per clock, or far more with SIMD. e.g. Skylake-AVX512 has (per core) 2-per-clock throughput on vpaddd zmm: 16 32-bit elements per instruction. – Peter Cordes Apr 19 '18 at 3:31
7

Aside from "everything is a file" and hence ease of usage everywhere that most other answers are based on, there is also performance issue as @user5626466 mentions.

To show in practice, we'll create simple program called nullread.c:

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

and compile it with gcc -O2 -Wall -W nullread.c -o nullread

(Note: we cannot use lseek(2) on pipes, so only way to drain the pipe is to read from it until it is empty).

% 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

whereas with standard /dev/null file redirection we get much better speeds (due to facts mentioned: less context switching, kernel just ignoring data instead of copying it etc):

% 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

(this should be a comment there, but is too big for that and would be completely unreadable)

  • What hardware did you test on? 4.8GB/s is pretty low compared to the 23GB/s I get on a Skylake i7-6700k (DDR4-2666, but the buffer should stay hot in L3 cache. So a good portion of the cost is system calls being expensive with Spectre + Meltdown mitigation enabled. That hurts doubly for piping, because pipe buffers are smaller than 1M, so that's more write / read system calls. Nearly 10x perf difference is worse than I expected, though. On my Skylake system it's 23GB/s vs. 3.3GB/s, running x86-64 Linux 4.15.8-1-ARCH, so that's a factor of 6.8. Wow, system calls are expensive now) – Peter Cordes Apr 20 '18 at 8:40
  • 1
    @PeterCordes 3GB/s with 64k pipe buffers suggests 2x 103124 syscalls per second... and that number of context switches, heh. On a server cpu, with 200000 syscalls per second, you might expect ~8% overhead from PTI, since there is very little working set. (The graph I'm referencing doesn't include the PCID optimization, but maybe that's not so significant for small working sets). So I'm not sure PTI has a big impact there? brendangregg.com/blog/2018-02-09/… – sourcejedi Apr 20 '18 at 9:24
  • 1
    Oh interesting, so it's a Silvermont with 2MB of L2 cache, so your dd buffer + receive buffer don't fit; you're probably dealing with memory bandwidth instead of last-level cache bandwidth. You might get better bandwidth with 512k buffers or even 64k buffers. (According to strace on my desktop, write and read are returning 1048576, so I think that means we're only paying the user<->kernel cost of TLB invalidation + branch-prediction flush once per MiB, not per 64k, @sourcejedi. It's Spectre mitigation that has the most cost, I think) – Peter Cordes Apr 20 '18 at 9:34
  • 1
    @sourcejedi: With Spectre mitigation enabled, the cost of a syscall that returns right away with ENOSYS is ~1800 cycles on Skylake with Spectre mitigation enabled, most of it being the wrmsr that invalidates the BPU, according to @BeeOnRope's testing. With mitigation disabled, the user->kernel->user round trip time is ~160 cycles. But if you are touching lots of memory, Meltdown mitigation is significant, too. Hugepages should help (fewer TLB entries need to be reloaded). – Peter Cordes Apr 21 '18 at 23:43
  • 1
    @PeterCordes On a single-core unix system, we would surely see 1 context switch per 64K, or whatever your pipe buffer was, and that would hurt... actually I also see the [same number of context switches with 2 cpu cores ](unix.stackexchange.com/questions/439260/…); it must also be counting a sleep/wake cycle for each 64k as a context switch (to a nominal "idle process"). Keeping the pipeline processes on the same cpu actually worked more than twice as fast. – sourcejedi Apr 22 '18 at 10:13
6

Your question is posed as if something would be gained perhaps in simplicity by using a null program in lieu of a file. Perhaps we can get rid of the notion of "magic files" and instead have just "ordinary pipes".

But consider, a pipe is also a file. They're normally not named, and so can only be manipulated through their file descriptors.

Consider this somewhat contrived example:

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

Using Bash's process substitution we can accomplish the same thing through a more roundabout way:

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

Replace the grep for echo and we can see under the covers:

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

The <(...) construct is just replaced with a filename, and grep thinks it's opening any old file, it just happens to be named /dev/fd/63. Here, /dev/fd is a magic directory that makes named pipes for every file descriptor possessed by the file accessing it.

We could make it less magic with mkfifo to make a named pipe that shows up in ls and everything, just like an ordinary file:

$ mkfifo foofifo
$ ls -l foofifo 
prw-rw-r-- 1 indigo indigo 0 Apr 19 22:01 foofifo
$ grep foo foofifo

Elsewhere:

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

and behold, grep will output foo.

I think once you realize pipes and regular files and special files like /dev/null are all just files, it's apparent implementing a null program is more complex. The kernel has to handle writes to a file either way, but in the case of /dev/null it can just drop the writes on the floor, whereas with a pipe it has to actually transfer the bytes to another program, which then has to actually read them.

  • @Lyle Yeah? Then why does echo print /dev/fd/63? – Phil Frost Apr 21 '18 at 20:30
  • Hm. Good point. Well, this is implemented by shells, so perhaps your shell is different from the old Bourne shell I grew up with. – Lyle Apr 21 '18 at 20:44
  • One difference is that echo doesn't read from stdin, while grep does, but I can't think how the shell would know that before it execs them. – Lyle Apr 21 '18 at 20:46
  • 1
    And strace does make this clearer: for me. you have it exactly right, with bash. The '<(...)' construct is doing something quite different from <filename. Hm. I learned something. – Lyle Apr 21 '18 at 20:59
0

I would argue that there is a security issue beyond historical paradigms and performance. Limiting the number of programs with privileged execution credentials, no matter how simple, is a fundamental tenet of system security. A replacement /dev/null would certainly be required to have such privileges due to use by system services. Modern security frameworks do an excellent job preventing exploits, but they aren't foolproof. A kernel driven device accessed as a file is much more difficult to exploit.

  • This sounds like nonsense. Writing a bug-free kernel driver is no easier than writing a bug-free program that reads+discards its stdin. It doesn't need to be setuid or anything, so for both /dev/null or a proposed input-discarding program, the attack vector would be the same: get a script or program that runs as root to do something weird (like try to lseek in /dev/null or open it multiple times from the same process , or IDK what. Or invoke /bin/null with a weird environment, or whatever). – Peter Cordes Apr 21 '18 at 23:51
0

As others already pointed out, /dev/null is a program made of a handful of lines of code. It's just that these lines of code are part of the kernel.

To make it clearer, here's the Linux implementation: a character device calls functions when read or written to. Writing to /dev/null calls write_null, while reading calls read_null, registered here.

Literally a handful lines of code: these functions do nothing. You'd need more lines of code than fingers on your hands only if you count functions other than read and write.

  • Maybe I should have phrased it more precisely . I meant why implement it as a char device instead of a program. It would be a few lines either way but the program implementation would be decidedly simpler. As the other answers have pointed out, there are quite a few benefits to this; efficiency and portability chief among them. – Ankur S Apr 23 '18 at 15:24
  • Sure. I just added this answer because seeing the actual implementation was fun (I discovered it recently myself), but the real reason is what others pointed out indeed. – Matthieu Moy Apr 23 '18 at 16:24
  • Me too! I recently started learning devices in linux and the answers were quite informative – Ankur S Apr 23 '18 at 16:35
-2

I hope that you are also aware of the /dev/chargen /dev/zero and others like them including /dev/null.

LINUX/UNIX has a few of these - made available so that people can make good use of WELL WRITTEN CODE FrAGMEnTS.

Chargen is designed to generate a specific and repeating pattern of characters - it is quite fast and would push the limits of serial devices and it would help debug serial protocols that were written and failed some test or other.

Zero is designed to populate an existing file or output a whole lot of zero's

/dev/null is just another tool with the same idea in mind.

All of these tools in your toolkit means that you have half a chance at making an existing program do something unique without regard to their (your specific need) as devices or file replacements

Lets set up a contest to see who can produce the most exciting result given only the few character devices in your version of LINUX.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.