/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:
c — character device: data flows as a stream of bytes (serial, tty, GPIO)b — block device: data is addressed in fixed-size blocks (disks, partitions)l — symbolic linkd — directory (like /dev/pts/)Every device file has two numbers: major and minor.
crw-rw-rw- 1 root dialout 188, 0 ... ttyUSB0
↑ ↑
major minor
ttyUSB* devices have major 188.ttyUSB0 is minor 0, ttyUSB1 is minor 1.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.
Device files are not regular files. They are created by:
devtmpfs)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.
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.
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
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
# /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
Any device file can be opened with Python’s open(). The key is using the right flags and mode.
# Read from /dev/urandom (always ready, never blocks)
with open("/dev/urandom", "rb") as f:
random_bytes = f.read(16)
print(random_bytes.hex())
# 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")
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.
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:
Every type of device has its own set of ioctl codes, documented in man pages (e.g., man tty_ioctl, man ioctl_list).
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:
dialout — serial ports (ttyUSB*, ttyS*, ttyACM*)disk — raw block devicesvideo — video capture devices (video0)audio — audio devices (audio, dsp)i2c — I2C devicesspi — SPI devicesgpio — GPIO character devicePrevious: Chapter 3 — Device Drivers