Hello! Welcome to Module 9 of the Embedded Interview Preparation Bootcamp.
In our last lesson, we mastered how to control the when and where of task execution using real-time scheduling policies and CPU affinity. Now, we'll dive deeper into the kernel to build the very bridge that connects user applications to the hardware they control: the device driver. This is a core competency for any engineer working at a semiconductor company.
This lesson will guide you through building the fundamental skeleton of a Linux device driver. You'll learn how a driver introduces itself to the kernel and makes itself available to user-space applications.
Your learning outcome for this lesson is to: Implement a basic character device driver, including major/minor number allocation and file_operations registration.
1. The "Everything is a File" Philosophy
In Linux and other UNIX-like systems, a core design principle is to represent system resources and devices as files in the filesystem. A character device driver is a kernel component that provides this abstraction for character-stream-oriented hardware, such as serial ports, audio devices, or simple custom peripherals.
From a user-space application's perspective, interacting with the hardware is as simple as performing standard file I/O operations (open, read, write, close) on a special file in the /dev directory. The kernel's Virtual File System (VFS) intercepts these system calls and directs them to the appropriate functions within your driver.
To start, let's get a high-level overview of what a character driver is and its role in the system.
Understanding the Structure of a Linux Kernel Device Driver - Sergio Prado, Toradex
This video from The Linux Foundation provides an excellent introduction to the concept of device drivers as abstractions, focusing specifically on character drivers and the role of device files.
Watch from 02:13 to 07:35. Pay attention to the explanation of: Drivers as abstractions. Character vs. block devices. The three special attributes of a device file: type, major number, and minor number.
This flow, from a user-space read() call down to the driver, is visualized below.

2. Key Identifiers: Major and Minor Numbers
As you just saw, device files are identified by major and minor numbers, not by name. This pair is how the VFS connects a file in /dev to a specific driver.
- Major Number: Identifies the driver responsible for this class of devices. All devices managed by the same driver will share the same major number. You can see a list of currently registered major numbers in
/proc/devices. - Minor Number: Identifies a specific instance of a device managed by that driver. The driver uses the minor number to distinguish between multiple physical devices it might be controlling (e.g.,
/dev/ttyS0vs./dev/ttyS1).

3. Core Kernel Structures for Character Drivers
To implement a character driver, you need to be familiar with two fundamental structures:
Character device drivers — The Linux Kernel documentation
The official Linux Kernel documentation provides the most precise description of the essential data structures. We will focus on struct file_operations and struct cdev.
Please read the section titled 'Data structures for a character device'. Focus on understanding the roles of: struct file_operations: The table of function pointers that connects system calls like open and read to your driver's code. struct inode and struct file: Kernel representations of a file. Notice that the inode gives you access to the device numbers (i_cdev), and the file structure has a private_data field you can use to store your driver's state. struct cdev: The kernel's internal representation of a character device.
In summary:
struct file_operations: This is the heart of your driver's interface. You create a static instance of this struct and fill it with pointers to your functions (my_open,my_read, etc.).struct cdev: This structure represents your character device within the kernel. You will embed this inside a larger, driver-specific struct to hold your device's state.
4. The Modern Registration Process: A Step-by-Step Guide
Registering a character device is a well-defined sequence of steps performed within your kernel module's init function. The cleanup is done in reverse order in the exit function.
Step 1: Reserve a Range of Device Numbers
First, you must ask the kernel for a major number. The modern and recommended way is to request one dynamically to avoid conflicts with other drivers.
- Function:
alloc_chrdev_region(dev_t *dev, unsigned int baseminor, unsigned int count, const char *name) - Cleanup:
unregister_chrdev_region(dev_t from, unsigned int count)
This call will allocate count device numbers, assign a free major number, and return the first device number (major + baseminor) in the dev parameter.
Step 2: Initialize and Add the Character Device (cdev)
Once you have device numbers, you must initialize your cdev structure and add it to the system.
- Function:
cdev_init(struct cdev *cdev, const struct file_operations *fops) - Function:
cdev_add(struct cdev *p, dev_t dev, unsigned count) - Cleanup:
cdev_del(struct cdev *p)
cdev_init links your cdev structure to your file_operations, and cdev_add makes the device "live" and active in the kernel.
Here is a conceptual skeleton of how these steps fit together in a module's init and exit functions:
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/module.h>
static dev_t my_device_nr;
static struct cdev my_cdev;
// Define your file operations (stubs for now)
static int my_open(struct inode *inode, struct file *file) {
pr_info("my_driver: open() called\n");
return 0;
}
static int my_release(struct inode *inode, struct file *file) {
pr_info("my_driver: release() called\n");
return 0;
}
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
// .read and .write will be added in the next lesson
};
static int __init my_module_init(void) {
// 1. Allocate device numbers
if (alloc_chrdev_region(&my_device_nr, 0, 1, "my_driver") < 0) {
pr_err("my_driver: device number allocation failed\n");
return -1;
}
pr_info("my_driver: Allocated Major %d, Minor %d\n", MAJOR(my_device_nr), MINOR(my_device_nr));
// 2. Initialize the cdev structure and file_operations
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. Add the character device to the system
if (cdev_add(&my_cdev, my_device_nr, 1) < 0) {
pr_err("my_driver: cdev_add failed\n");
unregister_chrdev_region(my_device_nr, 1);
return -1;
}
pr_info("my_driver: Module loaded successfully.\n");
return 0;
}
static void __exit my_module_exit(void) {
// Cleanup in reverse order
cdev_del(&my_cdev);
unregister_chrdev_region(my_device_nr, 1);
pr_info("my_driver: Module unloaded.\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A basic character device driver");
5. Hands-on Walkthrough: Bringing the Driver to Life
Now let's see these steps in a practical demonstration. The following video walks through creating a simple character driver, compiling it, loading it, and interacting with it from the command line.
Understanding the Structure of a Linux Kernel Device Driver - Sergio Prado, Toradex
This video segment demonstrates the entire lifecycle of a basic character driver. It perfectly matches the concepts we've just discussed and shows you the expected output at each stage.
Watch from 09:18 to 19:09. Follow the presenter's actions closely as they: Show the C code for a simple LED driver with read and write stubs. Compile and load the module (insmod). Check /proc/devices to find the dynamically allocated major number. Create the device node manually using mknod. Test the driver by reading from and writing to the /dev/led file and observing the kernel messages (dmesg).
After loading the module, you must create the device file manually for testing. If the kernel assigned your driver major number 243, the command would be:mknod /dev/mydevice c 243 0
While mknod is fine for development, production drivers use class_create() and device_create() in their init function to have the udev daemon automatically create and remove the /dev node. We will explore this in a later lesson.
6. Interview Insight: Old vs. New Registration APIs
In an interview, you might be asked about different ways to register a character device. Knowing the history and trade-offs demonstrates deeper knowledge. Before the alloc_chrdev_region and cdev_* APIs became standard, a simpler but less flexible function was used.
Different approaches in character device creation
This blog post clearly contrasts the older, deprecated method of creating character devices with the modern approach we've just learned. Understanding this difference is a great way to stand out.
Read through the first few sections of this article, focusing on the descriptions of: register_chrdev(): The old way. Note its limitations: it registers for all 256 minor numbers and is now discouraged. alloc_chrdev_region(): The new way for number allocation, which we are using. The role of cdev_init(), cdev_alloc(), and cdev_add() in the new method.
Key takeaway: The modern approach separates number allocation (alloc_chrdev_region) from device registration (cdev_add). This is more robust and flexible than the old register_chrdev function, which did both and forced a driver to claim an entire major number.
Conclusion
Congratulations! You've just walked through the essential steps to create the foundation of any character device driver. You now understand how to reserve device numbers, link them to a set of file operations, and register everything with the kernel so it's ready to receive calls from user space.
Key Takeaways:
- Character drivers expose hardware to user space through the standard file I/O interface.
- Major/minor numbers are the kernel's mechanism for linking a
/devfile to the correct driver. - The modern registration process involves
alloc_chrdev_region()to get device numbers, followed bycdev_init()andcdev_add()to make the device live. - The
struct file_operationsis the core of the driver, containing pointers to the functions that handle I/O requests. - Cleanup in the module's
exitfunction must happen in the reverse order of initialization:cdev_del()thenunregister_chrdev_region().
Preview of the Next Lesson:
With the driver registered, our device file is ready to be opened and closed. But what happens when a user tries to read or write? Right now, nothing. In our next lesson, you will implement read, write, and ioctl handlers, using copy_from_user/copy_to_user for safe data transfer. This will make our driver truly functional.