Everything is a file

UNIX is designed around a simple concept: everything is a file. But the ones I enjoy most are the oddballs — special files that do curious things yet can still be poked, prodded, and cat-ed like any other. Let’s explore a few of them.

an 8-bit style wizard in blue robes holding a glowing, open book
no wizards were harmed in the making of this post

File Types

“Some stirring may be necessary to achieve proper consistency.”
— fortune(6)

The phrase everything is a file isn’t just a slogan — it’s how UNIX presents nearly every interface:

  • Regular files — your documents, configs, logs.
  • Directories — just special files mapping names to inodes.
  • Character devices — like /dev/null or /dev/tty, providing byte streams to hardware or virtual drivers.
  • Block devices — like /dev/sda (a disk), where the kernel maps file I/O into fixed-size blocks.
  • Sockets — files that represent network endpoints (check /var/run/ on a Unixy system).
  • Pipes (FIFOs) — files that connect processes (mkfifo makes one).
  • Procfs/sysfs entries — “files” that show you kernel state when you cat them.

That’s why tools like cat, echo, or dd work on such a wild variety of things — whether you’re streaming bytes from a terminal, a pseudo-random generator, or the kernel itself.

└─$ ls -al /dev/null
crw-rw-rw- 1 root root 1,3 Aug 16 16:00 /dev/null
#           ^    ^    ^  ^^^
#           |    |    |  major,minor device numbers
#           |    |    group
#           |    owner
#           hardlink count
#character device


# A block device: your first disk
└─$ ls -l /dev/sda
brw-rw---- 1 root disk 8,0 Aug 29 10:00 /dev/sda
# b = block device

# A directory: just another kind of file
└─$ ls -ld /etc
drwxr-xr-x 123 root root 4096 Aug 29 09:59 /etc
# d = directory

# A FIFO (named pipe): file that connects processes
└─$ mkfifo mypipe && ls -l mypipe
prw-r--r-- 1 kevin kevin 0 Aug 29 10:01 mypipe
# p = pipe

# A socket: file that represents a communication endpoint
└─$ ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Aug 29 10:00 /var/run/docker.sock
# s = socket

Let’s explore!

/dev/null — The Bit Bucket

A special file that represents nothing. Writing to /dev/null discards data forever; reading it returns immediate EOF.

In the upstream Linux kernel tree (Linus’s repo), /dev/null lives in drivers/char/mem.c. The device’s behavior is wired through the file_operations table:

Browse: drivers/char/mem.c (mainline)

/* ... inside drivers/char/mem.c ... */
static ssize_t read_null(struct file *file, char __user *buf,
                         size_t count, loff_t *ppos)
{
    return 0; /* EOF immediately */
}

static ssize_t write_null(struct file *file, const char __user *buf,
                          size_t count, loff_t *ppos)
{
    return count; /* pretend we wrote everything */
}

static const struct file_operations null_fops = {
    .read  = read_null,
    .write = write_null,
    /* other ops may be present depending on kernel version */
};

Why it matters

Because /dev/null is “just a file,” any tool that can write to a file can be silenced or stress-tested without special flags.

Practical tricks

Pro-Tip: Silence everything (stdout and stderr):

some-noisy-tool > /dev/null 2>&1

Measure syscall overhead (it’ll still touch the kernel fast path):

strace -c cat /dev/null

High-speed “write” benchmark (no disk I/O happens):

dd if=/dev/zero of=/dev/null bs=64M count=128 status=progress

Drop output in pipelines without branching:

make 2>build.err > /dev/null

Pretend-success sink in scripts (command returns 0 if the last stage does):

generate-stats | tee /dev/null

Odds & ends

  • Historically, /dev/null is a character device (major 1, minor varies by table) implemented by the kernel’s “memory” driver alongside /dev/zero, /dev/full, and friends.
  • The exact function names around null_fops may differ slightly across kernel versions (e.g., llseek/splice_write entries); the essence remains: reads return 0, writes report success.

Canonical source: drivers/char/mem.c in Linus’s tree

Some detailed explanations I like


/dev/zero — The Infinite Stream of Zeros

Need endless null bytes? /dev/zero is your friend. Every read returns \0 forever.

Why use it?
  • Create blank disk images or memory files.
  • Initialize storage with a known pattern.
  • Quick way to generate padding.

Examples:

# Create a 100 MB blank file
dd if=/dev/zero of=blank.img bs=1M count=100

# Wipe a disk partition with zeros (careful!)
dd if=/dev/zero of=/dev/sdX bs=1M status=progress

/dev/full — The “Disk Full” Device

The counterpart to the zero device: /dev/full rejects every write with ENOSPC (“No space left on device”).
Reads return zeros (like /dev/zero), but writes always fail.

Why use it?
  • Test how software behaves when disks are full without filling up a disk for real.
  • Ensure your error handling doesn’t silently corrupt data.
  • Force applications to trigger out-of-space recovery paths.

Examples:

# Simulate a write failure
echo test > /dev/full
# bash: echo: write error: No space left on device

# Pipe data into /dev/full to trigger error handling in a script
tar cf - /etc | cat > /dev/full

/dev/random and /dev/urandom — Entropy on Tap

Both /dev/random and /dev/urandom draw from the kernel’s cryptographic random number generator, which is constantly fed with entropy from noisy hardware events: interrupt timing jitter, disk and network latencies, keyboard/mouse input, and—if available—CPU hardware RNG instructions like Intel’s RDRAND or RDSEED. These unpredictable signals are mixed into a pool, then stretched into high-quality random data with a cryptographic PRNG.

There are actually two “random” devices on Linux:

  • /dev/random historically blocks if the kernel’s entropy pool is low. On modern Linux (5.6+), it only blocks until the RNG is fully initialized (very early at boot).
  • /dev/urandom never blocks, stretching whatever entropy is available.
  • Since Linux 5.6, both use the same ChaCha20-based core, with /dev/random acting mostly as a “blocking front end.”

Source code

Key structures:

  • random_fops (for /dev/random)
  • urandom_fops (for /dev/urandom)
/* snippet from drivers/char/random.c (modern kernels) */
const struct file_operations random_fops = {
    .read_iter   = random_read_iter,
    .write_iter  = random_write_iter,
    .poll        = random_poll,
    .unlocked_ioctl = random_ioctl,
    .compat_ioctl   = compat_ptr_ioctl,
    .fasync      = random_fasync,
    .llseek      = noop_llseek,
    .splice_read = copy_splice_read,
    .splice_write= iter_file_splice_write,
};

const struct file_operations urandom_fops = {
    .read_iter   = urandom_read_iter,
    .write_iter  = random_write_iter, /* same writer path */
    .unlocked_ioctl = random_ioctl,
    .fasync      = random_fasync,
    .llseek      = noop_llseek,
    .splice_read = copy_splice_read,
    .splice_write= iter_file_splice_write,
};

Behind these fops (file operations) are the kernel’s cryptographic RNG (Random Number Generator) functions, feeding data from entropy pools mixed with device interrupts, timings, and other noise sources.

Why use it?
  • Secure key generation (ssh-keygen, gpg, etc).
  • Random test data (head -c 16 /dev/urandom).
  • Checking system entropy levels via /proc/sys/kernel/random/entropy_avail.

Examples:

# Generate a 16-character password
tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16 ; echo

# Check entropy pool size
cat /proc/sys/kernel/random/entropy_avail
# Note: on modern kernels this usually sits at 256 by design; it doesn’t mean “low entropy.”


# Grab 32 random bytes as hex
od -An -tx1 -N32 /dev/random

# 20 random digits (0–9)
tr -dc '0-9' < /dev/urandom | head -c 20 ; echo

# 20 random letters (A–Z, a–z)
tr -dc 'A-Za-z' < /dev/urandom | head -c 20 ; echo

# simulate a 6-sided die roll
echo $(( ( $(od -An -N2 -i /dev/urandom) % 6 ) + 1 ))

an 8-bit, arcade sprite style wizard in black robes, casting a lightning bolt. playful tone

Other Oddballs in /dev

Not everything in /dev is as widely recognized as /dev/null or /dev/urandom. Many entries serve specialized purposes: some expose useful system interfaces, others exist mainly for testing or debugging, and a few can be hazardous if misused.


/dev/tty — Your Terminal

Always points to your controlling terminal.

echo "Hello from script land" > /dev/tty

Handy when stdout is redirected and you still want to talk to the user.


/dev/pts/* — Pseudo Terminals

Created dynamically for each terminal session (SSH, tmux, etc.).

echo "Wake up, Neo..." > /dev/pts/2

Yes, you can write text directly into someone’s session—if you have permission.
Pseudo-terminal devices are owned by the session user and typically mode rw--w---- (0620), group tty. That means only the owner (or root) can write to it.


/dev/console — The Big Screen

The system console. Kernel boot logs and critical errors go here.
You can also send your own (assuming you are root):

echo "System maintenance in 5 minutes" | sudo tee /dev/console

An example of this in the wild can be seen when scheduling a reboot with
sudo shutdown -r +10. Under the hood, shutdown calls wall(1) (or an equivalent) to broadcast a warning message. That message is written both to /dev/console and to all connected TTYs.

This behavior is a legacy of the multi-user era, when it was essential to notify other logged-in users before rebooting or performing disruptive maintenance. Even today, it’s a practical demonstration of how the console and TTY devices are just files that can be written to.


/dev/kmsg — Talk to the Kernel Log

Write straight into dmesg from userspace:

echo "Injected log message from userspace" | sudo tee /dev/kmsg

Great for debugging custom scripts or testing log monitoring (as root) although logger(1) is the more portable and cleaner alternative.


/dev/mem and /dev/kmem — Raw Memory

Give direct access to physical and kernel virtual memory.
Mostly disabled on modern systems for security. Historically used for debuggers, kernel hacking… and crashing your system.

⚠️ Danger: one wrong write and the kernel panics.


/dev/loop* — Loopback Block Devices

Loop devices let you treat an ordinary file as if it were a real disk.
Anything you write into the mounted filesystem is actually stored inside that file, with the kernel handling the translation.

# Create a 10MB blank file
dd if=/dev/zero of=disk.img bs=1M count=10

# Map to a loop device and make an ext4 filesystem
sudo losetup /dev/loop0 disk.img
sudo mkfs.ext4 /dev/loop0

# Mount, write some data, then unmount
sudo mount /dev/loop0 /mnt
echo "hello loopback" | sudo tee /mnt/hello.txt
sudo umount /mnt
sudo losetup -d /dev/loop0

At this point, disk.img is an ext4 filesystem in a file.
If you peek inside with hexdump or strings, you can see its structure:

# Look for ext4 magic numbers
hexdump -C disk.img | grep '53 ef'
# Typical output:
# 00000400  53 ef 01 00 ...

# Or scan for human-readable bits
strings disk.img | head
# Output might show journal headers, superblock info, etc.

Reattach it to a loop device later and your hello.txt will still be there.
This is the same trick used for ISO files, qcow2 VM images, and container layers.


/dev/net/tun — Virtual Networking

The TUN/TAP clone device. Userland programs open /dev/net/tun and use an ioctl(TUNSETIFF) to request either:

  • TUN (layer-3, IP packets)
  • TAP (layer-2, Ethernet frames)

Commonly used by VPNs like WireGuard and OpenVPN, and by virtualization tools that need virtual NICs.
(Docker’s default networking uses veth pairs and bridges, not TUN/TAP.)


/dev/shm — Shared Memory

Technically a tmpfs mounted under /dev. It’s RAM-backed storage, cleared on reboot.

echo "fast scratch data" > /dev/shm/test.txt
cat /dev/shm/test.txt

Pro-Tip: /dev/shm is like a built-in RAM disk for temp files.
Many tools honor the TMPDIR variable, so you can speed them up by pointing it here:

# Run tests with fast temp storage in RAM
TMPDIR=/dev/shm pytest -q

# Or give SQLite an all-RAM temp area
SQLITE_TMPDIR=/dev/shm sqlite3 mydb.sqlite

Great for test runs, builds, and scratch data that doesn’t need to survive a reboot.


an 8-bit, arcade sprite style wizard in red casting a fireball. playful tone

/proc — The Kernel’s Diary

Unlike /dev, which is about devices, /proc is a virtual filesystem exposing kernel internals.
Files here are interfaces into live kernel state, assembled dynamically whenever you query them.

Every process gets its own subdirectory (/proc/<pid>), plus global kernel stats.

The /proc filesystem implementation lives in the Linux kernel tree under:

🔗 fs/proc/ — Linux source (Elixir cross-referencer)


Common tricks

Why use it?
  • Inspect process details without ps or top.
  • Read kernel parameters on the fly.
  • Script system monitoring without extra tools.

Examples:

# Show command line of your shell, where $$ is a special variable that expands to the current shell's process ID (PID)
cat /proc/$$/cmdline

# Show process status (fields like State, Threads, Memory), where 12345 is any running PID
cat /proc/12345/status | head

# System uptime (in seconds)
awk '{print $1}' /proc/uptime

# Kernel version
cat /proc/version

# List all loaded modules
cat /proc/modules

Tweakable knobs

Many settings in /proc/sys are writable, same as sysctl.
This is the interface Linux exposes for runtime performance tuning, though most users never need to touch it — the kernel ships with sensible defaults (often tuned differently between server and desktop distributions).

# Enable IPv4 forwarding (required for routers, VPNs, containers)
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward

# Check current swappiness (0–100, default ~60)
cat /proc/sys/vm/swappiness
# Lower = keep apps in RAM longer, swap only when memory is tight
# Higher = free RAM sooner, keep larger disk cache

# Set swappiness to 10 (more "desktop-friendly" behavior)
echo 10 | sudo tee /proc/sys/vm/swappiness

# Adjust maximum number of open file descriptors
cat /proc/sys/fs/file-max
echo 2097152 | sudo tee /proc/sys/fs/file-max

# Tune TCP keepalive time (seconds of idle before probes are sent)
cat /proc/sys/net/ipv4/tcp_keepalive_time
echo 300 | sudo tee /proc/sys/net/ipv4/tcp_keepalive_time

📚 Where to find them all

The full catalog of tunables lives under /proc/sys, grouped by subsystem (vm/, net/, kernel/, fs/, …).

Canonical documentation lives in the kernel source tree:

⚠️ Changes made directly in /proc/sys last only until reboot.
To make them permanent, set them via sysctl (e.g. sysctl -w vm.swappiness=10) and add entries in /etc/sysctl.conf or drop-in files under /etc/sysctl.d/.


Oddities worth knowing

Some /proc files are especially handy to peek at directly:

  • /proc/cpuinfo — CPU model, flags, and per-core info.

    grep 'model name' /proc/cpuinfo | uniq
    

    (What lscpu uses under the hood.)

  • /proc/meminfo — granular memory stats (RAM, swap, buffers, caches).

    head -5 /proc/meminfo
    

    (Basis for the free command.)

  • /proc/loadavg — load averages as shown by uptime. First three fields are 1, 5, and 15-minute averages.

    cat /proc/loadavg
    
  • /proc/filesystems — list of supported filesystem types.

    cat /proc/filesystems | column
    

    (Prefixed with nodev if no block device is required.)

  • /proc/sysrq-trigger — write magic letters here to invoke SysRq kernel actions.

    echo b | sudo tee /proc/sysrq-trigger   # reboot immediately
    echo m | sudo tee /proc/sysrq-trigger   # dump memory info to dmesg
    

    ⚠️ Dangerous if you don’t know the key sequences — it’s a raw escape hatch.


Source code

Implemented in the kernel’s fs/proc/ directory:



Bonus Round: BSD/macOS sysctl

“It’s more fun to be a pirate than to join the navy.”
— Steve Jobs

While Linux uses /proc, the BSD family (FreeBSD, NetBSD, OpenBSD) and Darwin/macOS expose kernel state through a sysctl tree.

At the user level you call the sysctl(3) function, whose prototype is:

int sysctl(int *name, u_int namelen,
           void *oldp, size_t *oldlenp,
           void *newp, size_t newlen);
  • name is an integer array describing the MIB path (e.g. { CTL_HW, HW_NCPU }).
  • oldp / oldlenp point to a buffer for the current value.
  • newp / newlen optionally provide a new value to set.

Inside the kernel, nodes are registered in a tree of struct sysctl_oid objects (FreeBSD/macOS). Each OID describes a tunable or info node (name, type, handler function) that supplies the value or applies a change.


Example: BSD/macOS sysctl calls in C

int mib[2];
size_t len;
int ncpu;

mib[0] = CTL_HW;
mib[1] = HW_NCPU;
len = sizeof(ncpu);

if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1)
    perror("sysctl");

printf("CPUs: %d\n", ncpu);

That’s the C version of what sysctl hw.ncpu does on the shell.

On BSD/macOS you can also use the convenience wrapper sysctlbyname(3):

int ncpu;
size_t len = sizeof(ncpu);

if (sysctlbyname("hw.ncpu", &ncpu, &len, NULL, 0) == -1)
    perror("sysctlbyname");

printf("CPUs: %d\n", ncpu);

Practical macOS examples (CLI)

# Show number of CPU cores
sysctl hw.ncpu

# Show total RAM (in bytes)
sysctl hw.memsize

# Kernel version and build string
sysctl kern.version

# Boot time (seconds since epoch + human-readable date)
sysctl kern.boottime

# Current max files limit
sysctl kern.maxfiles

# Raise max files (temporary, until reboot)
sudo sysctl -w kern.maxfiles=65536

💡 On macOS, many performance tunables live under kern.*, hw.*, and net.*. Like Linux /proc/sys, changes with sysctl -w are not persistent — they reset on reboot unless set via launchd plist or sysctl config mechanisms.

Unlike on Linux, /etc/sysctl.conf is ignored. Use a LaunchDaemon or a startup script instead:

For reference:

Conclusion

I hope you enjoyed this quick tour of some of the more fascinating bits of the Unix filesystem. It’s fun to think about the oddballs all together rather than as isolated footnotes. Unix has a lot of special files—but the beauty is that they’re still just files.

Have a favorite weird Unix virtual file? Something I left out? I’d love to hear about it!
Email me: feedback@adminjitsu.com