This question is one of those old chestnuts that have been around for ages. What happens when you send output to /dev/null
? It's singular because the bytes just seem to disappear, like into a black hole. No other file on the computer works like that. The question has taken on something of a philosophical dimension in the debates among programmers. It's almost like asking: how do you take a piece of matter and compress it down into nothing - how is that possible?
Well, as of about a week ago I know the answer. And soon you will, too.
/dev/null
appears as a file on the filesystem, but it's not really a file. Instead, it's something more like a border crossing. On one side - our side - /dev/null
is a file that we can open and write bytes into. But from the kernel's point of view, /dev/null
is a device. A device just like a physical piece of hardware: a mouse, or a network card. Of course, /dev/null
is not actually a physical device, it's a pseudo device if you will. But the point is that the kernel treats it as a device - it uses the same vocabulary of concepts when dealing with /dev/null
as it does dealing with hardware devices.
So if we have a device - and this is not a trick question - what do we need to have to able to use it? A device driver, exactly. So for the kernel /dev/null
is a device and its behavior is defined by its device driver. That driver lives in drivers/char/mem.c
. Let's have a look!
Near the bottom of the file we find an array definition:
static const struct memdev {
const char *name;
umode_t mode;
const struct file_operations *fops;
fmode_t fmode;
} devlist[] = {
[DEVMEM_MINOR] = { "mem", 0, &mem_fops, FMODE_UNSIGNED_OFFSET },
[2] = { "kmem", 0, &kmem_fops, FMODE_UNSIGNED_OFFSET },
[3] = { "null", 0666, &null_fops, 0 },
[4] = { "port", 0, &port_fops, 0 },
[5] = { "zero", 0666, &zero_fops, 0 },
[7] = { "full", 0666, &full_fops, 0 },
[8] = { "random", 0666, &random_fops, 0 },
[9] = { "urandom", 0666, &urandom_fops, 0 },
[11] = { "kmsg", 0644, &kmsg_fops, 0 },
};
We don't need to understand all the details here, but just look at the names being defined: mem
, null
, zero
, random
, urandom
. Where have we seen these names before? We've seen them endless times as /dev/mem
, /dev/null
, /dev/zero
, /dev/random
, /dev/urandom
. And it's in this file that they are actually defined.
If we focus on the line that defines null
we can see that it mentions something called null_fops
. This is a struct that defines the behavior of /dev/null
. And the struct looks like this:
static const struct file_operations null_fops = {
.llseek = null_lseek,
.read = read_null,
.write = write_null,
.read_iter = read_iter_null,
.write_iter = write_iter_null,
.splice_write = splice_write_null,
};
The values being used to populate this struct are function pointers. So when /dev/null
is being written to the function that is responsible for this is write_null
. And when /dev/null
is being read from (it's not often we read from /dev/null
) the function responsible for that is read_null
.
Alright, we are just about to find out what happens when you write to /dev/null
. The moment you have been waiting for. Are you ready for this? Here we go:
static ssize_t write_null(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
return count;
}
write_null
gets two arguments that are of interest to us: the bytes that we sent to /dev/null
- represented by buf
, and the size of that buffer - represented by count
. And what does write_null
do with these bytes? Nothing, nothing at all! All it does is return count
to confirm that "we've received your bytes, sir". And that's it.
Writing to /dev/null
is literally calling a function that ignores its argument. As simple as that. It doesn't look like that because /dev/null
appears to us as a file, and the write causes a switch from user mode into kernel mode, and the bytes we send pass through a whole bunch of functions inside the kernel, but at the very end they are sent to the device driver that implements /dev/null
and that driver does... nothing! How do you implement doing nothing? You write a function that takes an argument and doesn't use it. Genius!
And considering how often we use /dev/null
to discard bytes we don't want to write to a (real) file and we don't want to appear in the terminal, it's actually an incredibly valuable implementation of nothing! It's the most useful nothing on the computer!