Skip to content

01 — Linux Driver Model

Technical Overview

The Linux driver model is the unified framework by which the kernel discovers, configures, and manages hardware. Every piece of hardware in a Linux system — a PCI NIC, a USB keyboard, an I2C temperature sensor on a SoC — participates in the same three-way relationship: a bus, a device, and a driver. This triad is not merely organizational; it drives runtime binding, reference counting, power management, and the sysfs hierarchy that userspace reads.

Before the driver model was introduced in Linux 2.5 (around 2002, primary author Patrick Mochel), each subsystem maintained its own device lists, its own power management code, and its own /proc entries. The result was duplicated infrastructure across PCI, USB, ISA, and SCSI subsystems. The unified driver model collapsed that duplication into a small set of core structures that every bus type implements.


Prerequisites

  • Understanding of kernel data structures (linked lists, kobjects)
  • Basic familiarity with C struct embedding / inheritance patterns
  • Concept of reference counting and object lifetime management
  • Basic understanding of Linux file hierarchy

The Bus-Device-Driver Triad

struct bus_type

struct bus_type represents a communication channel type — PCI, USB, I2C, SPI, platform, etc. Its most critical responsibility is matching: given a device and a driver, does this driver handle this device?

struct bus_type {
    const char              *name;
    const char              *dev_name;
    struct device           *dev_root;
    const struct attribute_group **bus_groups;
    const struct attribute_group **dev_groups;
    const struct attribute_group **drv_groups;

    int (*match)(struct device *dev, struct device_driver *drv);
    int (*probe)(struct device *dev);
    void (*remove)(struct device *dev);
    int (*suspend)(struct device *dev, pm_message_t state);
    int (*resume)(struct device *dev);
    // ... power management, IOMMU ops, etc.
};

The match() callback is called every time a new device or a new driver is registered on this bus. For PCI, matching compares vendor/device IDs from the hardware against the driver's pci_device_id table. For platform devices, it compares the device's compatible string (from Device Tree) against the driver's of_match_table.

struct device

struct device is the base class for every hardware object in the kernel. It is embedded inside bus-specific structures: a PCI device is a struct pci_dev that contains a struct device dev. This embedding pattern (analogous to C++ inheritance without vtables) allows the core driver model to operate on any device through the base struct device pointer.

struct device {
    struct kobject          kobj;        // sysfs anchor + reference count
    struct device           *parent;
    struct device_private   *p;          // internal bookkeeping

    const char              *init_name;
    const struct device_type *type;

    struct bus_type         *bus;        // which bus owns this device
    struct device_driver    *driver;     // which driver is bound

    void                    *platform_data;  // driver-private data
    void                    *driver_data;    // set via dev_set_drvdata()

    struct dev_pm_info      power;       // power management state
    // ...
};

The kobject inside struct device is the foundation of sysfs integration. Every kobject maps to a sysfs directory. Because struct device embeds kobject, every device automatically gets a directory under /sys/devices/.

struct device_driver

struct device_driver represents a software module capable of managing one or more devices on a specific bus.

struct device_driver {
    const char              *name;
    struct bus_type         *bus;

    struct module           *owner;

    const struct of_device_id   *of_match_table;   // Device Tree matching
    const struct acpi_device_id *acpi_match_table;  // ACPI matching

    int (*probe) (struct device *dev);
    void (*remove)(struct device *dev);
    void (*shutdown)(struct device *dev);
    int (*suspend)(struct device *dev, pm_message_t state);
    int (*resume)(struct device *dev);
    // ...
};

ASCII Diagram: Bus-Device-Driver Relationship

                        KERNEL DRIVER CORE
  ┌─────────────────────────────────────────────────────────────┐
  │                                                             │
  │   struct bus_type (e.g., pci_bus_type)                     │
  │   ┌──────────────────────────────────────┐                 │
  │   │  match()  ─── called on every new    │                 │
  │   │               device or driver       │                 │
  │   │  probe()  ─── delegates to driver    │                 │
  │   │  remove() ─── delegates to driver    │                 │
  │   └──────────────────────────────────────┘                 │
  │           │                      │                         │
  │    ┌──────▼──────┐        ┌──────▼───────┐                │
  │    │ struct       │        │ struct        │                │
  │    │ device       │        │ device_driver │                │
  │    │ (pci_dev,    │        │ (e1000_driver │                │
  │    │  usb_device, │        │  xhci_driver) │                │
  │    │  platform..) │        │               │                │
  │    └──────────────┘        └───────────────┘                │
  │         │                         │                         │
  │         └──────── match() ────────┘                         │
  │                   returns 1?                                │
  │                   → bus calls driver->probe(device)         │
  └─────────────────────────────────────────────────────────────┘

  /sys/bus/pci/
  ├── devices/         ← symlinks to /sys/devices/...
  │   ├── 0000:00:1f.2 → ../../devices/pci0000:00/0000:00:1f.2
  │   └── ...
  └── drivers/
      ├── ahci/
      │   └── bind, unbind, new_id
      └── e1000/

Platform Devices: Non-Discoverable Hardware

PCIe and USB are self-describing buses — the kernel can enumerate attached devices by reading configuration registers. SoC (System-on-Chip) peripherals are not: the kernel cannot query an ARM SoC to ask "what UARTs do you have?". Platform devices solve this by describing hardware in a data file rather than probing for it.

Device Tree (DT) is the mechanism used on ARM, RISC-V, and MIPS platforms. It is a binary blob (.dtb) appended to or loaded alongside the kernel image, compiled from a human-readable .dts source.

/* Example: UART in Device Tree */
uart0: serial@10000000 {
    compatible = "ns16550a";
    reg = <0x10000000 0x100>;   /* base address, size */
    interrupts = <4>;
    clock-frequency = <1843200>;
    status = "okay";
};

At boot, the kernel's platform bus reads the Device Tree and calls of_platform_populate(), which creates struct platform_device objects for every node. The platform bus match() function compares the compatible string against each registered driver's of_match_table.

ACPI serves the same role on x86 UEFI platforms. ACPI tables (DSDT, SSDT) describe hardware topology, interrupt routing, and power states. The kernel's ACPI subsystem parses these and creates struct acpi_device objects.


Device Registration Flow

Hardware appears (boot, hotplug)
         │
         ▼
  device_register(dev)
  ├── device_initialize() — init kobject, set defaults
  ├── device_add()
  │   ├── kobject_add() — create sysfs directory
  │   ├── bus_add_device() — add to bus device list
  │   ├── device_create_file() — populate sysfs attrs
  │   └── bus_probe_device()
  │       └── for each driver on bus:
  │           bus->match(dev, drv) ?
  │               yes → driver_probe_device(drv, dev)
  │                       → drv->probe(dev)
  └── kobject_uevent(KOBJ_ADD) — notify userspace

driver_register() follows the symmetric path: walk existing devices on the bus and call match() / probe() for each.


sysfs: The Driver Model's Userspace Window

Every kobject in the driver model maps to a sysfs directory. The result is a browsable tree that mirrors kernel data structures:

/sys/
├── bus/
│   ├── pci/
│   │   ├── devices/     ← per-device symlinks
│   │   └── drivers/     ← per-driver directories
│   ├── usb/
│   ├── i2c/
│   └── platform/
├── devices/
│   └── pci0000:00/
│       └── 0000:02:00.0/     ← NIC
│           ├── vendor         ← 0x8086
│           ├── device         ← 0x1502
│           ├── driver → ../../../../bus/pci/drivers/e1000e
│           └── net/
│               └── eth0/
└── class/
    ├── net/
    │   └── eth0 → ../../devices/pci0000:00/0000:02:00.0/net/eth0
    └── block/
        └── sda → ../../devices/pci0000:00/...

sysfs attributes are backed by show() and store() callbacks defined via DEVICE_ATTR macros. Reading /sys/bus/pci/devices/0000:02:00.0/vendor calls the driver's show_vendor() function, which returns the PCI vendor ID.


udev: Userspace Device Manager

The kernel's device model is responsible for discovering and binding drivers. The creation of /dev nodes — the user-visible interface to devices — is delegated to userspace via udev (now part of systemd as systemd-udevd).

The flow is event-driven:

Kernel: device_add() → kobject_uevent(KOBJ_ADD)
         │
         ▼
   Netlink socket message to userspace
   (subsystem, action, device path, attributes)
         │
         ▼
   systemd-udevd receives uevent
         │
         ├── Matches udev rules (/etc/udev/rules.d/, /lib/udev/rules.d/)
         │   e.g., SUBSYSTEM=="block", KERNEL=="sd*", SYMLINK+="disk/by-id/..."
         │
         ├── Calls mknod() to create /dev/sdX
         ├── Sets ownership and permissions (e.g., group "disk")
         ├── Creates symlinks (/dev/disk/by-id/, /dev/disk/by-uuid/)
         └── Optionally runs helper programs (e.g., kpartx, hdparm)

udev rules are evaluated in filename order. Rules can match on subsystem, kernel name, device attributes, parent device attributes, and more. The ATTR{idVendor} key reads directly from sysfs.


Kernel Module Auto-Loading

The MODULE_DEVICE_TABLE macro in a driver source compiles device ID tables into the module's .ko file. The depmod utility extracts these tables and builds /lib/modules/$(uname -r)/modules.alias, a flat file mapping device identifiers to module names.

When udev receives a KOBJ_ADD event for a device with no bound driver, it reads the device's modalias attribute from sysfs and passes it to modprobe:

# The kernel sets modalias automatically:
$ cat /sys/bus/usb/devices/1-1/modalias
usb:v046DpC52Bd1800dc00dsc00dp00ic03isc01ip01in00

# udev rule triggers:
# ACTION=="add", SUBSYSTEM=="usb", RUN+="/sbin/modprobe"

# modprobe consults modules.alias:
$ modprobe --resolve-alias usb:v046DpC52B*
hid-logitech-hidpp

modprobe handles dependency resolution using /lib/modules/$(uname -r)/modules.dep, loading prerequisite modules before the requested one.


Historical Context

The Linux driver model was designed by Patrick Mochel and merged in Linux 2.5.45 (2002). Before this, Linux had separate device enumeration for PCI (struct pci_dev lists), USB (usb_device trees), and platform devices (scattered arch-specific code). Power management was especially fragmented — there was no unified suspend/resume path.

The model drew influence from the Windows Driver Model (WDM) introduced in Windows 98/2000, which also used a bus-driver-device hierarchy. Linux's implementation added kobjects and sysfs as a side effect, replacing the earlier procfs device entries.

Device Tree support was integrated later (around 2011 for mainline ARM) following the "ARM architecture chaos" that Linus Torvalds complained about in 2011 — hundreds of ARM board-specific files with hardcoded hardware descriptions. Device Tree moved that knowledge out of the kernel.


Production Examples

NVMe drive appearing: BIOS/UEFI enumerates PCIe bus → kernel pci_scan_bus() creates struct pci_dev with vendor=0x1987 (Phison) → PCI bus match() checks all registered PCI drivers → nvme driver matches via pci_device_id table → nvme_probe() called → registers block device → udev creates /dev/nvme0 and /dev/nvme0n1.

USB keyboard: xHCI controller generates interrupt for port state change → usb_hub_irq() runs → new device enumerated → USB descriptor read (HID class, keyboard protocol) → usbhid driver probed → udev receives KOBJ_ADD for input device → udev creates /dev/input/event3 with group input.

Raspberry Pi I2C sensor: Device Tree node with compatible = "bosch,bmp280"of_platform_populate() creates platform device → I2C bus match() finds bmp280 driver → bmp280_probe() configures sensor registers → sysfs attribute /sys/bus/i2c/devices/1-0076/hwmon/hwmon1/temp1_input exposes temperature.


Debugging Notes

# Show all devices on PCI bus with driver binding status
lspci -k

# Show udev events in real time
udevadm monitor --kernel --udev --property

# Manually trigger udev rules re-evaluation for a device
udevadm trigger --action=add /sys/bus/usb/devices/1-1

# Show why a driver is not binding to a device
udevadm test /sys/bus/usb/devices/1-1

# Check which driver owns a device
ls -la /sys/bus/pci/devices/0000:02:00.0/driver

# Force-bind a driver to a specific device (override match)
echo "0000:02:00.0" > /sys/bus/pci/drivers/e1000e/bind

# Show module dependencies
modinfo e1000e | grep depends
cat /lib/modules/$(uname -r)/modules.dep | grep e1000e

Common failure: driver not probing 1. Check dmesg | grep -i probe for probe errors 2. Verify MODULE_DEVICE_TABLE matches actual hardware IDs 3. Check that module is loaded (lsmod | grep driver_name) 4. Check modalias matches: cat /sys/bus/pci/devices/ADDR/modalias vs modinfo driver.ko | grep alias


Security Implications

The driver model's auto-loading feature is an attack surface. Any unprivileged process that can create a socket of a non-existent network protocol triggers request_module("net-pf-XX"), which auto-loads a kernel module. Historically this was exploited to trigger loading of vulnerable modules. The fix in Linux 3.3 introduced kmod_filter behavior and kernel.modprobe sysctl controls.

udev rules running as root can execute arbitrary programs when devices appear. A malicious USB device that spoofs a known vendor/device ID could trigger udev rules that run privileged scripts. This is a physical access attack but relevant in shared hardware environments.

The bind/unbind sysfs interfaces are root-only, but any process with CAP_SYS_ADMIN can rebind drivers, potentially overriding security assumptions about device ownership.


Performance Implications

The driver probe path runs synchronously during boot for built-in drivers, creating a serialized boot bottleneck. Linux mitigates this with asynchronous probing (PROBE_PREFER_ASYNCHRONOUS flag) for independent subsystems, allowing parallel initialization of storage and network drivers.

sysfs attribute access involves a kobject lock and a function call per read/write. For high-frequency monitoring, reading sysfs in a tight loop is expensive. The perf subsystem and eBPF provide lower-overhead alternatives for production monitoring.


Failure Modes

  • Probe failure: probe() returns a non-zero error code. Device remains unbound. Visible in dmesg. Common causes: resource conflict, firmware not found, hardware not responding.
  • Module load failure: missing symbol, version mismatch (vermagic check fails). Linux refuses to load a module built against a different kernel version unless CONFIG_MODULE_FORCE_LOAD=y.
  • Reference count leak: failure to call put_device() after get_device(). Device object stays alive indefinitely, preventing module unload.
  • udev timeout: systemd-udevd has a per-event timeout (default 180s). If a device's probe() hangs or a udev rule's helper program hangs, subsequent events for that device class may queue up or be dropped.

Modern Usage

The driver model is stable and unchanged in principle since 2.6. Recent developments:

  • ACPI-based enumeration on ARM64: ARM servers (AWS Graviton, Ampere Altra) use ACPI instead of Device Tree, making them boot like x86 servers. The same driver works on both ARM64+ACPI and ARM64+DT.
  • fw_node (fwnode_handle): A unified abstraction over Device Tree OF nodes and ACPI nodes, allowing single-source drivers that work on both.
  • Auxiliary bus: Introduced in Linux 5.11 for splitting large monolithic drivers (like ice, the Intel 100GbE driver) into sub-drivers where each functional unit (networking, RDMA, ipsec offload) is a separate driver on an auxiliary bus.

Future Directions

The push toward Rust driver bindings in Linux (merged in 6.1) uses the driver model's C structures via safe Rust wrappers. The Registration struct in Rust implements Drop to ensure driver_unregister() is always called, fixing an entire class of missing-cleanup bugs that exist in C drivers.

eBPF for device management: BPF programs can be attached to LSM hooks that govern device access, enabling fine-grained device permission policies without modifying kernel code.


Exercises

  1. Write a minimal platform driver that registers itself against a Device Tree compatible string. Compile as a module, load it on a QEMU ARM VM with a custom .dts, and observe the probe being called.
  2. Add a sysfs attribute to your driver that returns the number of times probe() has been called.
  3. Write a udev rule that creates a stable symlink under /dev/mydevice/ when a USB device with a specific vendor/product ID is plugged in.
  4. Use udevadm monitor to capture the full uevent sequence when a USB storage device is plugged in. Identify each event's subsystem, action, and DEVPATH.
  5. Trace the match() calls on the platform bus during boot using ftrace: echo 'platform_match' > /sys/kernel/debug/tracing/set_ftrace_filter.

References

  • Linux Kernel Documentation: Documentation/driver-api/driver-model/
  • Linux Device Drivers, 3rd Edition — Corbet, Rubini, Kroah-Hartman (LDD3)
  • Patrick Mochel, "The sysfs Filesystem" — OLS 2005
  • Greg Kroah-Hartman, "The Linux Kernel Driver Interface" — kernel.org
  • Device Tree specification: https://www.devicetree.org/specifications/
  • drivers/base/ in the Linux kernel source tree
  • Documentation/ABI/testing/sysfs-bus-* for sysfs ABI documentation