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:
insmodloads a single.kofile;modproberesolves 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
compatiblestring 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
| Concept | Key API / Mechanism | Lifecycle Stage |
|---|---|---|
| Kernel module | module_init() / module_exit() | Load / unload |
| Bus registration | platform_driver_register() | Init registers driver with bus |
| Device matching | of_match_table with compatible strings | Bus matches driver to device tree node |
| Probe | probe() callback | Bus calls when match found |
| Char device | file_operations, cdev_add() or misc_register() | Probe creates /dev/ node |
| Remove | remove() callback | Bus calls on unbind or module unload |
| sysfs | DEVICE_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 andEXPORT_SYMBOL_GPLsymbols become unavailable.MODULE_AUTHOR()andMODULE_DESCRIPTION()-- informational, shown bymodinfo.
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:
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
#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.
Device Tree Driver Code| |v v+-----------+ +---------------+| Device | | Driver ||compatible |------->|of_match_table ||="vendor, | match |="vendor, || sensor" | | sensor" |+-----------+ +-------+-------+|vprobe() 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 Field | Userspace Call | Purpose |
|---|---|---|
.open | open("/dev/mydev", ...) | Acquire device, allocate per-file state |
.release | close(fd) | Release device, free per-file state |
.read | read(fd, buf, count) | Copy data from device to userspace |
.write | write(fd, buf, count) | Copy data from userspace to device |
.unlocked_ioctl | ioctl(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 Type | Data Model | Access Pattern | Example |
|---|---|---|---|
| Character | Byte stream | Sequential (seek optional) | Sensor, UART, GPIO |
| Block | Fixed-size blocks | Random access | SD card, eMMC, NAND |
| Network | Packets/frames | Socket 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:
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 registersdevm_request_irq()-- register an interrupt handlerdevm_kzalloc()-- allocate driver-private datadevm_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:
| Interface | Best For | Example |
|---|---|---|
| sysfs | Simple key-value configuration, status monitoring | LED brightness, sensor calibration offset |
| ioctl | Structured commands, binary data, performance-critical paths | Camera frame capture, DMA buffer setup |
/dev/ read/write | Streaming data, bulk transfer | Sensor data streams, serial communication |
Driver Types Compared
| Driver Type | Bus | Discovery | Device Tree | Example |
|---|---|---|---|---|
| Platform | platform | Device tree compatible | Yes | GPIO controller, SoC peripheral |
| I2C | i2c | Device tree + address | Yes | Sensor, EEPROM, RTC |
| SPI | spi | Device tree + chip select | Yes | Flash, ADC, display |
| USB | usb | Hot-plug (VID/PID) | No | USB dongle, webcam |
| PCI/PCIe | pci | Hot-plug (vendor/device ID) | No | Network 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_initregisters with a bus, the bus matches and callsprobe,probesets up hardware and creates/dev/nodes, userspace callsopen/read/write/ioctl,removetears 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
.kofile); "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 manyEXPORT_SYMBOL_GPLsymbols become unavailable, causing cryptic link errors. - Don't ignore
devm_functions -- using non-managed resources without meticulous cleanup in every error path ofprobe()causes kernel memory leaks. Interviewers specifically look for whether you default todevm_variants.
Follow-up Questions
- "How would you implement a custom
ioctlcommand 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
udevdecide 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.