I still remember the first time I broke a chroot environment because I forgot that /dev inside it was empty. No /dev/null, no /dev/zero, no /dev/console — just an empty directory. The fix was mknod, and it’s one of those commands that looks intimidating the first time you read its man page but turns out to be refreshingly simple once you understand what a device node actually is. In this guide I’m going to walk through everything I know about mknod: what it does, how it works under the hood, every parameter it accepts, and where it actually gets used in real system administration work.
What mknod Actually Does
mknod creates a filesystem entry called a “special file” or “device node.” Unlike a regular file that points to data blocks on disk, a device node points to a kernel driver. When a program opens /dev/sda, it isn’t reading a file in the traditional sense — it’s talking directly to the block driver the kernel has registered for that device, identified by a major and minor number pair.
I like to think of device nodes as phone extensions into the kernel. The node itself carries almost no data; it’s essentially a labeled connector. The label is the pair of numbers: the major number, which tells the kernel which driver to route the request to, and the minor number, which tells that driver which specific device or sub-device to use.
Syntax
mknod [OPTION]... NAME TYPE [MAJOR MINOR]
NAME— the path and filename for the new nodeTYPE— the kind of special file:b(buffered/block),coru(unbuffered/character), orp(FIFO/named pipe)MAJOR MINOR— required forb,c, andu; omitted entirely forp
Parameters and Options
I tested these directly on an Ubuntu 24.04 system with GNU coreutils’ mknod:
| Option | Long form | Description |
|---|---|---|
-m MODE | --mode=MODE | Sets the permission bits of the new node instead of the default a=rw minus umask |
-Z | Sets the SELinux security context to the default type | |
--context[=CTX] | Sets SELinux or SMACK context explicitly | |
--help | Prints usage and exits | |
--version | Prints version information |
One thing worth flagging: your shell may ship its own built-in mknod. Some minimal shells and busybox environments implement a reduced version, so always check type mknod if behavior looks off.
The Three Node Types Explained
Block Devices (b)
Block devices read and write data in fixed-size chunks and support random access with buffering/caching by the kernel’s page cache. Hard disks, SSDs, and partitions are block devices. /dev/sda, /dev/nvme0n1, /dev/loop0 — all block devices, all created historically with mknod b.
Character Devices (c or u)
Character devices transfer data as a stream, byte by byte, with no buffering layer imposed by the block layer. Terminals, serial ports, and pseudo-devices like /dev/null, /dev/zero, and /dev/random are character devices. c and u are functionally identical in modern Linux — u is a historical BSD-ism kept for portability.
FIFOs / Named Pipes (p)
A FIFO is different from the other two: it’s not backed by a driver at all. It’s a kernel-buffered communication channel that exists as a named entry in the filesystem, allowing two unrelated processes to communicate by opening the same path — one for reading, one for writing. No major/minor number applies here.
Tested Examples
I ran these on a live container to confirm behavior before writing this article.
Creating a FIFO
$ mknod /home/claude/testfifo p
$ ls -l /home/claude/testfifo
prw-r--r-- 1 root root 0 Jul 31 01:38 /home/claude/testfifo
Notice the leading p in the permission string — that’s how ls -l marks a named pipe. You can use it exactly like mkfifo would:
mkfifo mypipe # equivalent convenience command
mknod mypipe p # identical result via mknod
In one terminal:
cat < mypipe
In another:
echo "hello through the pipe" > mypipe
The reader receives the message instantly. This is the mechanism behind shell constructs like process substitution and is still used by daemons that expose a simple control channel (many logging and monitoring tools listen on a named pipe for commands).
Creating a Character Device
I recreated a working /dev/null clone using its real major/minor pair (1, 3):
$ mknod /home/claude/testnull c 1 3
$ ls -l /home/claude/testnull
crw-r--r-- 1 root root 1, 3 Jul 31 01:38 /home/claude/testnull
The c at the start of the permission field marks it as a character device, and ls -l shows 1, 3 in place of a file size — that’s the major and minor number pair, not bytes.
You can confirm the standard number assignments for your kernel in /usr/src/linux/Documentation/admin-guide/devices.txt (or the online kernel documentation), or by inspecting an existing node:
ls -l /dev/null
# crw-rw-rw- 1 root root 1, 3 Jan 1 00:00 /dev/null
Creating a Block Device Node
mknod /dev/mydisk b 8 0
This would create a node pointing at whatever driver is registered under major number 8 (historically the SCSI/SATA disk driver), minor 0 (the first disk). In practice you’d almost never do this by hand today — see the udev section below.
Setting Permissions at Creation Time
mknod -m 660 /dev/mydevice c 250 0
This creates the node with rw-rw---- permissions directly, saving a follow-up chmod call.
How mknod Works Internally
When you run mknod, it ultimately calls the mknod(2) system call. The kernel creates a new inode in the target directory’s filesystem with a special file type set in the inode’s mode field (S_IFBLK, S_IFCHR, or S_IFIFO). For block and character devices, the major/minor pair is stored in the inode’s rdev field rather than pointing to any data blocks.
Later, when a process opens that path, the VFS (Virtual Filesystem Switch) layer notices the special file type and, instead of routing the open() through a normal filesystem read path, hands control to chrdev_open() or blkdev_open() in the kernel, which looks up the major number in the kernel’s device driver table (chrdev_table or the block device registry) and invokes that driver’s open file operation. The filesystem is essentially just storing a pointer/label; the real work happens in driver code that has nothing to do with disk I/O for the node itself.
This is also why device nodes are cheap: they occupy an inode but zero data blocks, and why moving a device node with mv across filesystems does not “copy data” the way you’d expect for a regular file — it just recreates the special entry.
Why You Rarely Run mknod by Hand Anymore
Modern Linux systems use udev (managed by systemd-udevd) to populate /dev dynamically. When the kernel detects a new device — a USB drive is plugged in, for example — it emits a uevent, and udevd listens for that event and creates the appropriate node automatically, complete with symlinks like /dev/disk/by-uuid/.... This is why /dev on a running system is actually a tmpfs or devtmpfs mount, rebuilt fresh at every boot.
So where does mknod still matter?
- Container and chroot environments — when you build a minimal chroot, a container base filesystem, or a recovery environment, there’s no udev running inside to populate
/dev. You often need to manually create/dev/null,/dev/zero,/dev/console, and similar nodes. - Docker image builds — some minimal base images ship without expected device nodes, and Dockerfiles occasionally use
mknodto add them. - Kernel module and driver development — when you write a new character device driver and want to test it before udev rules are configured, you register a major number and create the node manually.
- FIFOs in shell scripting — this is the one case
mknod p(or its friendlier aliasmkfifo) still sees everyday use, for building simple IPC pipelines.
Practical System Administration Example
Here’s a real pattern I’ve used when building a minimal chroot for a rescue environment:
mkdir -p /mnt/rescue/dev
cd /mnt/rescue/dev
mknod -m 666 null c 1 3
mknod -m 666 zero c 1 5
mknod -m 666 full c 1 7
mknod -m 666 random c 1 8
mknod -m 666 urandom c 1 9
mknod -m 600 console c 5 1
mknod -m 666 tty c 5 0
I usually wrap this in a small script tied to my rescue-image build pipeline:
#!/bin/bash
set -euo pipefail
DEV_DIR="${1:-/mnt/rescue/dev}"
mkdir -p "$DEV_DIR"
declare -A nodes=(
[null]="c 1 3 666"
[zero]="c 1 5 666"
[full]="c 1 7 666"
[random]="c 1 8 666"
[urandom]="c 1 9 666"
[console]="c 5 1 600"
[tty]="c 5 0 666"
)
for name in "${!nodes[@]}"; do
read -r type major minor mode <<< "${nodes[$name]}"
mknod -m "$mode" "$DEV_DIR/$name" "$type" "$major" "$minor"
done
echo "Device nodes created in $DEV_DIR"
This is far more reliable than copying /dev entries with cp -a, because copying can silently fail to preserve the special file semantics on some filesystems, and it avoids dragging in the full 3,000+ node contents of a real running /dev.
Troubleshooting
“Operation not permitted” — you need root privileges (or CAP_MKNOD) to create block/character device nodes. FIFOs, by contrast, can be created by unprivileged users.
“File exists” — mknod will not overwrite an existing path; remove it first with rm if you’re recreating a node.
Device works but permission denied when opened — check the mode you set with -m and the node’s ownership. Also check whether the underlying kernel driver for that major number is even loaded; a correctly created node pointing to an unloaded module will fail on open with ENODEV or ENXIO.
Node created but doesn’t behave like the real device — double check the major/minor numbers against the running kernel’s actual assignment. These can differ across kernel versions and configurations, especially for less common device classes; the safest approach is copying the numbers from an existing populated /dev (ls -l /dev/whatever) rather than hardcoding from memory.
mknod vs Related Commands
| Command | Purpose |
|---|---|
mknod | Low-level creation of any special file type (block, char, FIFO) |
mkfifo | Convenience wrapper limited to FIFO creation, easier to remember |
udevadm trigger | Asks udev to re-process existing kernel devices and (re)create their nodes dynamically |
mount -t devtmpfs | Mounts the kernel-managed device filesystem that populates /dev automatically at boot |
If all you need is a named pipe, reach for mkfifo — it’s clearer to read in scripts. Reserve mknod for cases where you specifically need block or character nodes, or where a script needs to stay POSIX-portable across systems that may lack mkfifo.
Security Implications
Device nodes are a real attack surface. A block device node pointing at a physical disk, if created with world-writable permissions inside a container that shares the host’s device namespace, can allow a process to read or write raw disk data, bypassing filesystem permissions entirely. This is exactly why container runtimes like Docker and Podman restrict CAP_MKNOD by default and use device cgroups to control what a container is allowed to access, even if it could create a node.
As a rule I follow: never grant CAP_MKNOD to a container unless it genuinely needs to create device files, and always pair custom nodes with the tightest permission mode that still does the job — 600 or 660, not 666, unless you have a specific reason (like the standard /dev/null semantics) for world read/write.
Distribution Compatibility
mknod is part of GNU coreutils on virtually every mainstream distribution — Debian, Ubuntu, Fedora, RHEL/CentOS, Arch, openSUSE. Alpine Linux and other BusyBox-based systems (common in minimal containers) ship a BusyBox implementation of mknod with a nearly identical interface, though -Z/SELinux context options won’t apply there since BusyBox doesn’t implement SELinux support. Behavior across all of these is consistent enough that scripts written for one will work unmodified on the others.
Summary
mknod is a small command with an outsized conceptual footprint: it’s your direct line to the kernel’s device driver framework. On a modern desktop or server you’ll rarely touch it because udev handles /dev for you, but the moment you step into a chroot, a minimal container image, or kernel driver development, it becomes indispensable. Understanding major/minor numbers, the difference between block and character semantics, and the FIFO special case will serve you well any time you need to reason about how Linux actually talks to hardware.
References
- GNU Coreutils Manual —
mknodinvocation: https://www.gnu.org/software/coreutils/manual/html_node/mknod-invocation.html - Linux man-pages project —
mknod(1)andmknod(2): https://man7.org/linux/man-pages/ - Linux Kernel Documentation — Device drivers and major/minor number allocation: https://www.kernel.org/doc/html/latest/admin-guide/devices.html
- Debian Administrator’s Handbook —
/devand udev: https://debian-handbook.info/
