Understanding Linux Device Drivers
A free Linux kernel development course lecture: how device drivers bridge hardware and user space in a modern Linux system.
What You Will Learn
- Why device drivers exist in a Linux system
- The “no policy in the kernel” design principle
- Character, block, network, and pseudo-filesystem drivers
- How to explore driver types on a running system
- When you should (and shouldn’t) write your own driver
Prerequisites
This lecture assumes you’re comfortable with basic Linux command-line usage and have completed the earlier chapters of this free embedded Linux course covering toolchains, bootloaders, kernel porting, and the root filesystem. No prior driver-writing experience is required — this is the conceptual foundation the rest of the device driver series builds on.
Why Device Drivers Exist
Every embedded Linux board is built around a specific set of hardware: a UART for a debug console, an I2C sensor, an SPI flash chip, GPIO lines driving an LED. None of that hardware speaks a common language on its own. The Linux kernel’s job is to take all of these different, incompatible hardware interfaces and expose them to user space through a small, predictable set of system calls — open(), read(), write(), close(), and ioctl(). The piece of kernel code that does this translation for one particular class of hardware is what we call a device driver.
A device driver is not always something you write yourself. Modern Linux kernels ship with drivers for the vast majority of common embedded peripherals already in tree — GPIO controllers, standard UARTs, I2C and SPI controllers, MMC/SD hosts, and more. Very often, the real engineering work in a bring-up project for the latest stable kernel is not writing a driver from scratch, it’s writing a devicetree node that tells an existing driver where your hardware lives and how it’s wired. Understanding driver architecture, however, is still essential — you need to know which driver framework your hardware falls under, what its user-space interface looks like, and how to debug it when something goes wrong.
Kernel Privilege and the “No Policy” Principle
Device driver code runs in kernel space, at the highest privilege level the processor offers. That gives it full access to physical memory, hardware registers, interrupt controllers, and DMA engines — but it also means a bug in a driver isn’t contained the way a bug in a user-space application is. A NULL pointer dereference in a driver’s interrupt handler can crash or hang the entire board, not just one process.
Because the blast radius of a driver bug is so large, the Linux kernel community follows a long-standing design principle often summarized as “no policy in the kernel.” A driver’s job is to expose the hardware’s capabilities faithfully and get out of the way — not to decide how those capabilities should be used. Decisions like “should this LED blink at 2Hz to indicate low battery” belong in user space. The driver’s only job is to let user space turn the LED on and off.
The Four Types of Linux Device Driver
Linux organizes drivers into three primary types, plus a fourth pattern that has become extremely common in modern kernels.
| Type | Access pattern | Typical use |
|---|---|---|
| Character | Unbuffered, byte or stream oriented, thin kernel-to-application layer | UARTs, sensors, custom hardware, first choice for new drivers |
| Block | Buffered, works in fixed-size blocks, backed by the page cache | eMMC, SD cards, NVMe/SSD storage |
| Network | Packet oriented, no device node, driven through the networking stack | Ethernet, Wi-Fi, CAN interfaces |
| Pseudo-filesystem | Exposed as a group of plain files, usually under sysfs or a driver-specific filesystem | GPIO via /sys/class/gpio, LEDs via /sys/class/leds, hwmon sensors |
Character devices remain the default starting point for anything that doesn’t fit neatly into block storage or networking. They offer a thin, unbuffered path straight from your read()/write() call into driver code, which makes them predictable and easy to reason about.
Block devices exist specifically for mass storage. The kernel inserts a heavy buffering and I/O-scheduling layer in front of block drivers so that reads and writes can be reordered, merged, and cached efficiently. That buffering is exactly what makes block devices a poor fit for anything that isn’t bulk storage — you don’t want your GPIO toggle silently delayed by a write-back cache.
Network devices follow a completely different model again: there’s no /dev node at all. Instead, the driver registers a network interface (like eth0 or wlan0) with the kernel’s networking subsystem, and user space interacts with it through sockets rather than file I/O.
Pseudo-filesystem drivers aren’t a distinct driver type in the kernel’s internal model so much as a pattern: a subsystem (GPIO, LEDs, hwmon, IIO) exposes its state as a directory of small files under sysfs, so you can control hardware with nothing more than cat and echo. This has become the preferred way to expose simple, low-frequency control interfaces on modern kernels.
Exploring Driver Types on a Real System
Before writing a single line of driver code, it’s worth getting comfortable inspecting what’s already registered on your board. On the latest stable kernel, /proc/devices lists every character and block driver currently registered along with its major number:
$ cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
5 /dev/tty
5 /dev/console
90 mtd
153 spi
226 drm
Block devices:
259 blkext
8 sd
179 mmc
254 device-mapper
You can also list every currently instantiated device node with its major and minor number pair:
$ ls -l /dev/i2c-* /dev/spidev* /dev/mmcblk0*
crw-rw---- 1 root i2c 89, 1 Jan 1 1970 /dev/i2c-1
crw-rw---- 1 root spi 153, 0 Jan 1 1970 /dev/spidev0.0
brw-rw---- 1 root disk 179, 0 Jan 1 1970 /dev/mmcblk0
brw-rw---- 1 root disk 179, 1 Jan 1 1970 /dev/mmcblk0p1
Notice the leading letter in the permissions column: c for character devices, b for block devices. That single character tells you immediately which of the two node types you’re looking at, before you even check the major number.
Real-World Use Cases
- Character: a custom I2C environmental sensor exposing temperature readings through
read()on a dedicated device node. - Block: the eMMC chip holding your root filesystem, accessed through the standard block layer and mounted as
/. - Network: a CAN bus interface on an automotive board, configured with
ip linkand used through raw CAN sockets. - Pseudo-filesystem: toggling a status LED from a shell script by writing to
/sys/class/leds/status/brightness, with no compiled program at all.
Common Mistakes and Troubleshooting
- Writing a driver when one already exists. Always check
drivers/in the kernel source tree and search for your chip’s compatible string before assuming you need custom code — many “missing” drivers are actually just missing devicetree entries. - Choosing block semantics for non-storage hardware. The page-cache buffering that makes block devices fast for storage will silently delay or reorder operations on anything else — stick to character devices for control-style hardware.
- Assuming a fixed major number. Relying on a hardcoded major number instead of requesting a dynamic one (covered in the next lecture) makes your driver fragile and prone to collisions.
- Putting policy in kernel code. If you find yourself hardcoding thresholds, timing decisions, or business logic into a driver, that logic almost always belongs in a user-space daemon instead.
Best Practices
- Keep drivers as small and mechanical as possible — expose capability, don’t implement policy.
- Prefer existing kernel frameworks (GPIO descriptor API, regmap, IIO, hwmon) over hand-rolled register access.
- Validate every value that crosses the user/kernel boundary — a malicious or buggy user-space program can pass anything through
ioctl()orwrite(). - Always check return values from allocation and registration functions; a driver that ignores kernel API failures can leave the system in an inconsistent state.
Summary and Key Takeaways
Device drivers are the kernel’s translation layer between generic system calls and specific hardware. They run with full kernel privilege, which is powerful but demands discipline — the “no policy in the kernel” principle keeps drivers simple, predictable, and easy to review. Linux organizes drivers into character, block, and network types, plus the increasingly common sysfs-based pseudo-filesystem pattern. In the next lecture in this free Linux device drivers course, we’ll go deep on character devices specifically: how major and minor numbers work, how device nodes get created on a modern kernel, and how to write a first user-space program that talks to one.
FAQ
What is the difference between a device driver and a kernel module?
A kernel module is a packaging mechanism — compiled code that can be loaded into or removed from a running kernel with insmod/rmmod. A device driver is the functionality inside that module (or built directly into the kernel image) that manages a specific piece of hardware. Most, but not all, drivers are built as modules.
Do I always need to write a driver for new hardware?
No. For a huge range of standard peripherals — I2C sensors, SPI flash, GPIO expanders — the mainline kernel already has a driver. Your job is usually to write a correct devicetree binding, not new driver code.
Why can’t I just use fread() and fwrite() on a device node?
The C standard library’s stream functions add user-space buffering on top of the raw system calls, which can delay or reorder what actually reaches the driver. Device access should generally go through the raw open(), read(), write(), and close() system calls instead.
What does the “no policy in the kernel” principle actually mean?
It means a driver should only expose the hardware’s raw capability to user space, not make decisions about how that capability is used. Timing, thresholds, and application logic belong in user-space programs, not compiled into kernel code.
Is a network interface a device driver?
Yes, but it’s a distinct type. Network drivers don’t create a /dev node; instead they register an interface with the kernel networking stack, and applications talk to it through sockets rather than file I/O.
What’s the fastest way to see what drivers are loaded on my board?
cat /proc/devices lists every registered character and block driver with its major number, and lsmod lists every loaded kernel module.
Continue This Free Linux Kernel Development Course
Next up: character devices, major and minor numbers, and your first device-driver-facing program.
Next Lecture Back to Course Index
4 Comments