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.

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 (major1
, 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.
- 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/zero
implementation:
drivers/char/mem.c — zero_fops
/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.
- 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/full
implementation:
drivers/char/mem.c — full_fops
/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
- Both are implemented in
drivers/char/random.c
in the mainline tree:
drivers/char/random.c (mainline)
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 ))

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 theTMPDIR
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.

/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
ortop
. - 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:
- Documentation/admin-guide/sysctl/
- Each subsystem has its own file, e.g.
⚠️ Changes made directly in
/proc/sys
last only until reboot.
To make them permanent, set them viasysctl
(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 byuptime
. 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:
- fs/proc/base.c — per-process files like
/proc/<pid>/status
. - fs/proc/proc_sysctl.c — the
/proc/sys
interface. - Documentation/filesystems/proc.rst — canonical docs in-tree.
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:
- 📖
sysctl(3)
man page - 🗂️ Darwin/XNU source tree —
kern_sysctl.c
and headers like<sys/sysctl.h>
,<netinet/in.h>
,<mach/vm_param.h>
define the MIBs.
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