Chapter 4 — /dev: Where Hardware Meets the Filesystem

The /dev Directory

/dev is where the Linux kernel exposes hardware as files. Every device file in /dev is a special file that the kernel created when a driver registered a device. Opening, reading, writing, or calling ioctl() on these files is how userspace interacts with hardware.

ls -la /dev/ | head -30
crw-rw-rw- 1 root tty  5,  0 Mar 15 10:23 tty
crw--w---- 1 root tty  4,  0 Mar 15 10:23 tty0
crw-rw-rw- 1 root dialout 188, 0 Mar 15 11:02 ttyUSB0
brw-rw---- 1 root disk  8,  0 Mar 15 10:23 sda
brw-rw---- 1 root disk  8,  1 Mar 15 10:23 sda1
crw-rw-rw- 1 root root  1,  3 Mar 15 10:23 null
crw-rw-rw- 1 root root  1,  8 Mar 15 10:23 random
crw-rw-rw- 1 root root  1,  9 Mar 15 10:23 urandom
crw-rw-rw- 1 root root  1,  5 Mar 15 10:23 zero

The first character of the permissions string tells you the type:

Major and Minor Numbers

Every device file has two numbers: major and minor.

crw-rw-rw- 1 root dialout 188, 0 ... ttyUSB0
                            ↑    ↑
                          major minor

You can look up major numbers:

cat /proc/devices
Character devices:
  1 mem
  4 tty
  5 /dev/tty
188 ttyUSB
...
Block devices:
  8 sd
259 blkext

When a driver calls register_chrdev(188, "ttyUSB", ...) in the kernel, it claims major number 188. Every time you open /dev/ttyUSB0, the kernel looks up major 188 and dispatches the call to the USB serial driver.

Creating Device Files

Device files are not regular files. They are created by:

  1. The kernel at boot — for built-in devices (using devtmpfs)
  2. udev — dynamically, when hardware is plugged in
  3. Manually — with mknod (rare, mostly for debugging)
# Create a device file manually (for illustration)
sudo mknod /tmp/test_dev c 188 99    # char device, major 188, minor 99

In practice you never need to create device files manually — udev does it automatically.

Common Device Files

Null, Zero, Random, Urandom

These are the “virtual” devices that don’t correspond to physical hardware:

# /dev/null: discards everything written, returns EOF on read
echo "discard this" > /dev/null
cat /dev/null   # returns nothing

# /dev/zero: returns infinite zero bytes
dd if=/dev/zero of=zeros.bin bs=1M count=1

# /dev/random: cryptographically secure random bytes (may block)
# /dev/urandom: non-blocking random bytes (use this)
python3 -c "import os; print(os.urandom(16).hex())"

In Python, os.urandom() reads from /dev/urandom internally.

Terminal Devices: tty, pts

ls /dev/tty*
# /dev/tty     current terminal
# /dev/tty0    first virtual console
# /dev/ttyS0   first serial (COM) port
# /dev/ttyUSB0 first USB serial adapter
# /dev/ttyACM0 first USB CDC ACM device (Arduino, etc.)

/dev/pts/ contains pseudo-terminal secondaries — one per SSH session, terminal window, or tmux pane:

ls /dev/pts/
# 0  1  2  ptmx

# Find which tty you're in
tty
# /dev/pts/1

Disk Devices

ls /dev/sd* /dev/nvme* /dev/vd* 2>/dev/null
# /dev/sda        whole disk
# /dev/sda1       first partition
# /dev/sda2       second partition
# /dev/nvme0n1    NVMe disk 0, namespace 1
# /dev/nvme0n1p1  NVMe partition 1

Memory Devices

# /dev/mem: physical memory (requires root, restricted by default)
# /dev/kmem: kernel virtual memory (deprecated)

# Check if /dev/mem access is restricted
cat /boot/config-$(uname -r) | grep CONFIG_STRICT_DEVMEM

Opening Device Files from Python

Any device file can be opened with Python’s open(). The key is using the right flags and mode.

Reading from a Character Device

# Read from /dev/urandom (always ready, never blocks)
with open("/dev/urandom", "rb") as f:
    random_bytes = f.read(16)
    print(random_bytes.hex())

Writing to a Device

# Write text to a terminal (if you own /dev/pts/1)
with open("/dev/pts/1", "w") as f:
    f.write("Hello from Python!\n")

Serial Port Interaction

import termios, tty, os

fd = os.open("/dev/ttyUSB0", os.O_RDWR | os.O_NOCTTY)
# Configure: 9600 baud, 8N1
attrs = termios.tcgetattr(fd)
attrs[4] = termios.B9600   # input baud
attrs[5] = termios.B9600   # output baud
termios.tcsetattr(fd, termios.TCSANOW, attrs)

os.write(fd, b"AT\r\n")
response = os.read(fd, 64)

In practice you’d use pyserial which wraps this for you, but understanding the underlying mechanism matters when debugging.

ioctl: The Swiss Army Knife

Many device operations don’t fit the read/write model. They need device-specific commands. ioctl (I/O control) is the syscall for this.

import fcntl, struct

# Example: get the terminal window size
TIOCGWINSZ = 0x5413
with open("/dev/tty") as f:
    buf = struct.pack("HHHH", 0, 0, 0, 0)
    result = fcntl.ioctl(f.fileno(), TIOCGWINSZ, buf)
    rows, cols, xpixels, ypixels = struct.unpack("HHHH", result)
    print(f"Terminal: {cols}x{rows}")

ioctl takes:

  1. A file descriptor (to any device file)
  2. A request code (device-specific integer constant)
  3. Optional argument (pointer-sized value or buffer)

Every type of device has its own set of ioctl codes, documented in man pages (e.g., man tty_ioctl, man ioctl_list).

Permissions and Groups

Device files have standard Unix permissions. Notice:

crw-rw-rw- 1 root dialout 188, 0 ... ttyUSB0

/dev/ttyUSB0 is owned by group dialout. To use serial ports without sudo, add your user to that group:

sudo usermod -aG dialout $USER
# Log out and back in for it to take effect

Common device groups:


Previous: Chapter 3 — Device Drivers

Next: Chapter 5 — /sys: The Kernel’s Live Hardware Map

Back to Table of Contents