Embedded Linux Essentials
intermediate
Weight: 4/10

Linux drivers and kernel modules

Understand Linux kernel modules, the driver model, character devices, platform drivers, and the probe/remove lifecycle for embedded Linux driver development.

embedded-linux
drivers
kernel-modules
char-device
platform-driver
sysfs
udev

Quick Cap

Linux drivers are kernel modules that bind to hardware through the driver model. In embedded Linux interviews, you need to understand the flow from module_init() through the bus-driver-device matching (via compatible strings) to probe(), where the driver sets up file_operations so userspace can interact with hardware through /dev/ nodes. This page covers the full journey from a bare kernel module to a production-ready platform driver.

Key Facts:

  • Every driver is a kernel module: module_init() registers, module_exit() unregisters
  • insmod vs modprobe: insmod loads a single .ko file; modprobe resolves dependencies automatically
  • GPL licensing required: Most kernel APIs require MODULE_LICENSE("GPL"); non-GPL modules are "tainted"
  • Platform driver matching: Platform drivers match to devices via the compatible string in the device tree
  • Character devices: Expose hardware to userspace through file_operations (open, read, write, ioctl)
  • Managed resources: devm_ functions automatically free resources on driver removal -- always prefer them

Deep Dive

At a Glance

ConceptKey API / MechanismLifecycle Stage
Kernel modulemodule_init() / module_exit()Load / unload
Bus registrationplatform_driver_register()Init registers driver with bus
Device matchingof_match_table with compatible stringsBus matches driver to device tree node
Probeprobe() callbackBus calls when match found
Char devicefile_operations, cdev_add() or misc_register()Probe creates /dev/ node
Removeremove() callbackBus calls on unbind or module unload
sysfsDEVICE_ATTR(), /sys/class/Runtime parameter exposure

Kernel Module Basics

Every piece of dynamically loaded kernel code is a module. The two essential entry points are module_init() (called when the module loads) and module_exit() (called when the module unloads). Functions marked __init are placed in a special memory section that the kernel frees after initialization completes, reclaiming RAM. Similarly, __exit functions are discarded entirely when the module is built into the kernel (not loadable).

Module metadata macros serve both documentation and licensing enforcement:

  • MODULE_LICENSE("GPL") -- required to access most kernel symbols. Without it, the kernel logs a "tainted" warning and EXPORT_SYMBOL_GPL symbols become unavailable.
  • MODULE_AUTHOR() and MODULE_DESCRIPTION() -- informational, shown by modinfo.

Loading modules: insmod mydriver.ko loads the exact .ko file you specify. If the module depends on symbols from another module, the load fails. modprobe mydriver reads /lib/modules/$(uname -r)/modules.dep (generated by depmod) and automatically loads all dependencies first, making it the preferred choice for production systems.

Module parameters allow runtime configuration at load time:

px-2 py-1 rounded text-sm font-mono border
module_param(baudrate, int, 0644);

The parameter can be set via insmod mydriver.ko baudrate=115200 or via modprobe, and is readable at runtime through /sys/module/mydriver/parameters/baudrate.

Building out-of-tree modules requires the kernel headers package and a minimal Makefile with obj-m += mydriver.o. The build invokes the kernel's build system: make -C /lib/modules/$(uname -r)/build M=$(pwd) modules.

Minimal Module Skeleton

c
#include <linux/module.h>
#include <linux/init.h>
static int __init hello_init(void) {
pr_info("hello: module loaded\n");
return 0;
}
static void __exit hello_exit(void) {
pr_info("hello: module unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

Load with sudo insmod hello.ko, verify with dmesg | tail, unload with sudo rmmod hello.

The Linux Driver Model

The kernel's driver model separates what hardware exists (devices) from how to operate it (drivers). A bus subsystem (platform, I2C, SPI, PCI, USB) sits between them and handles matching.

px-2 py-1 rounded text-sm font-mono border
Device Tree Driver Code
| |
v v
+-----------+ +---------------+
| Device | | Driver |
|compatible |------->|of_match_table |
|="vendor, | match |="vendor, |
| sensor" | | sensor" |
+-----------+ +-------+-------+
|
v
probe() called
+-- Map registers (devm_ioremap)
+-- Request IRQ (devm_request_irq)
+-- Create /dev node (misc_register)
+-- Ready for userspace

When the bus finds a device whose compatible property matches an entry in a driver's of_match_table, it calls that driver's probe() function, passing a reference to the device. The driver then claims hardware resources and creates userspace interfaces. On module unload or device removal, the bus calls remove() for orderly teardown.

This architecture means a driver never searches for its hardware -- the bus delivers matched devices to it. This is the fundamental difference between the old "driver polls for hardware" model and the modern Linux driver model.

Character Devices

Character devices are the most common driver type in embedded Linux. They expose hardware as a file in /dev/ that userspace processes interact with using standard system calls.

Major and minor numbers identify character devices. The major number identifies the driver (or driver class), while the minor number identifies a specific device instance within that driver. For example, /dev/ttyS0 and /dev/ttyS1 share the same major number but have different minor numbers.

The file_operations struct is the heart of a character device driver -- it maps system calls to driver functions:

file_operations FieldUserspace CallPurpose
.openopen("/dev/mydev", ...)Acquire device, allocate per-file state
.releaseclose(fd)Release device, free per-file state
.readread(fd, buf, count)Copy data from device to userspace
.writewrite(fd, buf, count)Copy data from userspace to device
.unlocked_ioctlioctl(fd, cmd, arg)Device-specific control commands

Creating /dev/ nodes: The old approach was register_chrdev() followed by manually running mknod to create the device file. The modern approach is misc_register(), which automatically creates a /dev/ node via udev with a dynamically assigned minor number. For drivers that need multiple device instances, the cdev_add() + class_create() + device_create() pattern gives full control over major/minor allocation.

Device TypeData ModelAccess PatternExample
CharacterByte streamSequential (seek optional)Sensor, UART, GPIO
BlockFixed-size blocksRandom accessSD card, eMMC, NAND
NetworkPackets/framesSocket API (no /dev/ node)Ethernet, WiFi, CAN

Platform Drivers

Most embedded drivers are platform drivers. Platform devices are peripherals that live on the SoC or are connected to memory-mapped buses -- they are not on a discoverable bus like USB or PCI, so the device tree describes them to the kernel.

The driver declares which devices it supports through of_match_table, an array of of_device_id structs containing compatible strings:

c
static const struct of_device_id mydrv_of_match[] = {
{ .compatible = "vendor,temp-sensor" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mydrv_of_match);
static struct platform_driver mydrv = {
.probe = mydrv_probe,
.remove = mydrv_remove,
.driver = {
.name = "mydrv",
.of_match_table = mydrv_of_match,
},
};
module_platform_driver(mydrv);

The module_platform_driver() macro is a convenience that generates the boilerplate module_init / module_exit functions to register and unregister the platform driver.

Inside probe(), always use managed (devm_) resource functions:

  • devm_ioremap_resource() -- map device registers
  • devm_request_irq() -- register an interrupt handler
  • devm_kzalloc() -- allocate driver-private data
  • devm_clk_get() -- obtain a clock reference

Managed resources are automatically freed when the device is removed or the driver is unbound, eliminating the entire class of "forgot to free in the error path" bugs. If you see a driver calling the non-devm_ variants, it must manually undo every allocation in remove() and in every error branch of probe().

sysfs Interface

Every device in the Linux driver model gets a directory under /sys/. The /sys/class/ hierarchy groups devices by function (e.g., /sys/class/gpio/, /sys/class/leds/), while /sys/devices/ shows the physical bus topology.

Drivers can expose custom attributes to userspace using DEVICE_ATTR() or DEVICE_ATTR_RW(). This creates readable (and optionally writable) files under the device's sysfs directory, allowing userspace tools and scripts to inspect or configure the driver without writing custom ioctl handlers.

When to use which interface:

InterfaceBest ForExample
sysfsSimple key-value configuration, status monitoringLED brightness, sensor calibration offset
ioctlStructured commands, binary data, performance-critical pathsCamera frame capture, DMA buffer setup
/dev/ read/writeStreaming data, bulk transferSensor data streams, serial communication

Driver Types Compared

Driver TypeBusDiscoveryDevice TreeExample
PlatformplatformDevice tree compatibleYesGPIO controller, SoC peripheral
I2Ci2cDevice tree + addressYesSensor, EEPROM, RTC
SPIspiDevice tree + chip selectYesFlash, ADC, display
USBusbHot-plug (VID/PID)NoUSB dongle, webcam
PCI/PCIepciHot-plug (vendor/device ID)NoNetwork card, GPU

Platform, I2C, and SPI drivers all follow the same probe/remove pattern and use device tree for hardware description. USB and PCI devices are self-describing through enumeration, so they do not need device tree entries.

Debugging Story

A team wrote a platform driver with compatible = "mycompany,temp-sensor" but the device tree node had compatible = "mycompany,temperature-sensor". The driver loaded successfully (lsmod showed it), but probe() never ran. The sensor remained inaccessible.

Diagnosis: cat /sys/bus/platform/drivers/ showed the driver was registered on the platform bus. ls /sys/bus/platform/devices/ showed the device existed. But there was no binding between them because the compatible strings did not match exactly. Fixing the device tree node to use "mycompany,temp-sensor" (or vice versa) resolved the issue immediately.

Lesson: Compatible string mismatches are the number one cause of "driver loaded but probe never called." Always copy the compatible string from the device tree binding documentation rather than typing it from memory. Use cat /sys/bus/platform/drivers/mydriver/uevent and inspect device uevent files to compare strings side by side.

What Interviewers Want to Hear

  • You understand that every driver IS a module -- "module" is the packaging, "driver" is the behavior
  • You can trace the full lifecycle: module_init registers with a bus, the bus matches and calls probe, probe sets up hardware and creates /dev/ nodes, userspace calls open/read/write/ioctl, remove tears down on unbind
  • You default to devm_ functions and can explain why (automatic cleanup, no error-path leaks)
  • You know that compatible string matching is exact and case-sensitive
  • You can debug a failed probe by checking sysfs, dmesg, and device tree status
  • You understand when to use sysfs vs ioctl vs /dev/ read/write for userspace interfaces

Interview Focus

Classic Interview Questions

Q1: "What is the lifecycle of a Linux kernel module?"

Model Answer Starter: "module_init() runs when the module loads -- for a driver, this typically calls platform_driver_register() to register with a bus. The bus scans for matching devices and calls probe() for each match. Inside probe(), the driver maps registers, requests IRQs, and creates /dev/ nodes via file_operations. Userspace interacts through open/read/write/ioctl. On unload, the bus calls remove(), then module_exit() unregisters the driver."

Q2: "Explain the difference between insmod and modprobe."

Model Answer Starter: "insmod loads the exact .ko file you specify and fails if dependencies are unmet. modprobe reads modules.dep (generated by depmod) to resolve and load all dependencies first, then loads the target module. In production, always use modprobe. modprobe -r also handles reverse dependency checking when removing modules."

Q3: "How does a platform driver bind to a device?"

Model Answer Starter: "The driver declares an of_match_table containing compatible strings. The device tree node for the hardware has a compatible property. When the platform bus sees a match, it calls the driver's probe() function with a pointer to the platform_device. Matching is exact and case-sensitive -- even a single character difference prevents binding."

Q4: "What is file_operations and how does userspace interact with a driver?"

Model Answer Starter: "file_operations is a struct of function pointers that maps system calls to driver code. When userspace calls open(\"/dev/mydev\", ...), the VFS routes it to the driver's .open callback. Similarly, read(), write(), and ioctl() map to .read, .write, and .unlocked_ioctl. The driver uses copy_to_user() and copy_from_user() to safely transfer data across the kernel-userspace boundary."

Q5: "Your driver's probe() is never called. How do you debug it?"

Model Answer Starter: "First, check that the module actually loaded: lsmod | grep mydriver. Then verify the compatible strings match exactly between the driver's of_match_table and the device tree node. Check that the device tree node has status = \"okay\" (or no status property, which defaults to enabled). Look at dmesg for errors during module load. Check /sys/bus/platform/drivers/ to confirm the driver registered on the correct bus, and /sys/bus/platform/devices/ to confirm the device exists."

Trap Alerts

  • Don't say "drivers and modules are different things" -- every driver IS a kernel module. "Module" is the container (loadable .ko file); "driver" is the role it plays (binding to hardware and serving userspace).
  • Don't forget GPL licensing -- most kernel APIs require MODULE_LICENSE("GPL"). Without it, the module loads but is "tainted" and many EXPORT_SYMBOL_GPL symbols become unavailable, causing cryptic link errors.
  • Don't ignore devm_ functions -- using non-managed resources without meticulous cleanup in every error path of probe() causes kernel memory leaks. Interviewers specifically look for whether you default to devm_ variants.

Follow-up Questions

  • "How would you implement a custom ioctl command for your driver?"
  • "What happens if two drivers have the same compatible string?"
  • "How does the kernel handle module dependencies at boot?"
  • "When would you build a driver into the kernel vs. making it a loadable module?"
  • "How does udev decide what permissions and name to assign to a /dev/ node?"

Practice

What happens if a kernel module is loaded without MODULE_LICENSE('GPL')?

What triggers the probe() function in a platform driver?

What is the main advantage of devm_ (managed) resource functions over their non-managed counterparts?

Which command resolves and loads module dependencies automatically?

A platform driver's probe() is never called even though lsmod shows the module is loaded. What is the most likely cause?

Real-World Tie-In

Custom Sensor Driver for Industrial IoT -- A factory monitoring system needed a driver for a proprietary vibration sensor connected via SPI. The team wrote a platform driver with device tree bindings so the same driver worked across three different gateway boards. Using devm_spi_register() and misc_register(), the probe function was under 40 lines. Userspace daemons read vibration samples from /dev/vibsensor0 and forwarded them to the cloud via MQTT. When a new board revision changed the SPI chip-select pin, only the device tree overlay needed updating -- zero driver code changes.

LED Controller Driver for Automotive Dashboard -- An automotive cluster display used an I2C LED controller for indicator lights. The driver exposed each LED channel as a sysfs attribute under /sys/class/leds/, following the standard Linux LED subsystem. This allowed the application layer to control LEDs with simple file writes (echo 255 > /sys/class/leds/turn-signal/brightness) while the driver handled I2C transactions, PWM dimming, and fault detection. When the LED controller IC was swapped for a different vendor's part in the next hardware revision, only the compatible string and a few register offsets changed in the driver.