Chapter 9 — udev and Hotplug: Dynamic Device Management

The Problem udev Solves

In the early Linux days, /dev was a static directory with device files pre-created at install time. There were thousands of files for every possible device, most unused. Plug-and-play didn’t exist.

udev (userspace /dev) solves this: it manages /dev dynamically. Device files are created when hardware is detected and removed when hardware is unplugged. It also runs rules that set permissions, create symlinks, rename devices, and launch programs in response to hardware events.

The Event Flow

When hardware appears (boot or hotplug), the kernel generates a uevent:

Hardware detected (PCI scan, USB plug, driver load)
        │
        ▼
Kernel generates uevent (netlink message to userspace)
        │
        ▼
udevd (udev daemon) receives the uevent
        │
        ▼
udevd evaluates rules in /etc/udev/rules.d/ and /lib/udev/rules.d/
        │
        ├── Creates device file in /dev/
        ├── Sets permissions and ownership
        ├── Creates symlinks (e.g. /dev/disk/by-id/...)
        └── Optionally runs a script/program

Monitoring Events with udevadm

# Watch all device events in real time
udevadm monitor

# Watch only kernel events (no udev processing)
udevadm monitor --kernel

# Watch only udev-processed events
udevadm monitor --udev

# Filter by subsystem
udevadm monitor --subsystem-match=usb

Plug in a USB device and watch the events flood past. You’ll see:

Querying Device Properties

# Show all udev properties for a device
udevadm info /dev/ttyUSB0
udevadm info --attribute-walk /dev/ttyUSB0   # includes parent devices

# Show by sysfs path
udevadm info /sys/bus/usb/devices/1-1

Output includes:

P: /devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0/ttyUSB0/tty/ttyUSB0
N: ttyUSB0
S: serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_...
E: DEVNAME=/dev/ttyUSB0
E: ID_VENDOR=Silicon_Labs
E: ID_VENDOR_ID=10c4
E: ID_MODEL=CP2102_USB_to_UART_Bridge_Controller
E: ID_MODEL_ID=ea60
E: ID_SERIAL=Silicon_Labs_CP2102...

These E: lines are the udev environment variables available in rules.

Writing udev Rules

Rules live in /etc/udev/rules.d/ (user rules, higher priority) and /lib/udev/rules.d/ (system rules). Files are processed in alphabetical order; start your filename with a number to control order.

A rule is a line of comma-separated key-value pairs. Match keys (uppercase) select devices; assignment keys (lowercase in some) take action.

# /etc/udev/rules.d/99-mydevice.rules

# Give all CP2102 USB-serial adapters to the 'dialout' group
SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", \
    GROUP="dialout", MODE="0660"

# Create a stable symlink for a specific device by serial number
SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", \
    ATTRS{serial}=="0001", SYMLINK+="ttyMyDevice"

# Run a script when a USB storage device is inserted
SUBSYSTEM=="block", KERNEL=="sd[b-z]", ACTION=="add", \
    RUN+="/usr/local/bin/usb_inserted.sh"

Reload rules without rebooting:

sudo udevadm control --reload-rules
sudo udevadm trigger

Stable Device Names

One problem udev solves: device names like /dev/ttyUSB0 depend on the order devices are detected. If you have two USB-serial adapters, which one is ttyUSB0 and which is ttyUSB1 can change between boots.

udev creates stable symlinks based on stable attributes:

ls -la /dev/disk/by-id/
# usb-SanDisk_Ultra_XXXXXXXX-0:0 -> ../../sdb

ls -la /dev/serial/by-id/
# usb-Silicon_Labs_CP2102_0001-if00-port0 -> ../../ttyUSB0

Use these paths in your code for stability:

import serial
# Instead of /dev/ttyUSB0 (fragile), use:
port = serial.Serial("/dev/serial/by-id/usb-Silicon_Labs_CP2102_0001-if00-port0")

Monitoring Hardware Events from Python

The pyudev library wraps libudev and lets Python code react to hardware events:

pip install pyudev
import pyudev

context = pyudev.Context()
monitor = pyudev.Monitor.from_netlink(context)
monitor.filter_by(subsystem="usb")

for device in iter(monitor.poll, None):
    print(f"Action: {device.action}")
    print(f"Device: {device.device_path}")
    if device.action == "add":
        vid = device.get("ID_VENDOR_ID", "?")
        pid = device.get("ID_MODEL_ID", "?")
        print(f"USB device added: {vid}:{pid}")

For async applications, pyudev has asyncio support:

import asyncio
import pyudev

async def watch_usb():
    context = pyudev.Context()
    monitor = pyudev.Monitor.from_netlink(context)
    monitor.filter_by(subsystem="usb")
    monitor.start()

    loop = asyncio.get_event_loop()
    while True:
        device = await loop.run_in_executor(None, monitor.poll)
        print(f"{device.action}: {device.sys_name}")

asyncio.run(watch_usb())

Listing Existing Devices

import pyudev

context = pyudev.Context()

# All USB devices
for device in context.list_devices(subsystem="usb"):
    vid = device.get("ID_VENDOR_ID")
    pid = device.get("ID_MODEL_ID")
    name = device.get("ID_MODEL")
    if vid and pid:
        print(f"{vid}:{pid}  {name}")

# All tty devices
for device in context.list_devices(subsystem="tty"):
    if device.get("ID_BUS") == "usb":
        print(f"{device['DEVNAME']}  {device.get('ID_SERIAL', '')}")

Triggering Events for Testing

You can replay device events without unplugging hardware:

# Re-trigger events for all USB devices
sudo udevadm trigger --subsystem-match=usb

# Trigger for a specific device
sudo udevadm trigger /sys/bus/usb/devices/1-1

This is useful for testing udev rules without physical plug/unplug cycles.


Previous: Chapter 8 — Memory-Mapped I/O

Next: Chapter 10 — Reading Hardware from Python

Back to Table of Contents