Linux Character Device Driver Basics
Part 1 of the Character Device Drivers chapter — free Linux kernel development course by EmbeddedPathashala
free linux development course
free linux device drivers course
free linux kernel development course
linux character device driver
If you are starting out with a linux character device driver tutorial, this lecture is the right place to begin. A character device driver is the simplest kind of Linux driver, and understanding it properly gives you the foundation you need for every other driver type — block devices, network devices, and platform drivers included. In this lecture we build the concept from the ground up using a real, working kernel 6.x example, no ASCII diagrams and no textbook copy-paste — just plain explanations you can try on your own machine.
What You Will Learn
Why Linux says “everything is a file”
The struct cdev object
Major and minor numbers
MAJOR(), MINOR(), MKDEV() macros
A minimal working char driver on kernel 6.x
Prerequisites
Comfortable with Linux terminal commands
A Linux VM or machine with kernel headers installed
Basic idea of what a kernel module is (insmod/rmmod)
What Is a Character Device Driver?
A character device transfers data one character at a time, in a continuous stream, the same way a serial port or a keyboard does. There is no seeking to a random block like you would on a hard disk — data simply flows in order. A linux character device driver is the piece of kernel code that exposes this stream-like behaviour to user space through a special file, usually created under /dev.
This is the practical meaning of the famous Unix philosophy “everything is a file.” Your keyboard, your serial console, your random number generator, and countless other pieces of hardware are all represented as ordinary-looking files that any application can open, read from, and write to using standard system calls.
Everything Is a File: How /dev Works
Run ls -l /dev on any Linux machine and you will see a long list of entries. The very first character of each line tells you the file type:
| Character | Meaning |
|---|---|
| c | Character device file |
| b | Block device file |
| l | Symbolic link |
| d | Directory |
| s | Socket |
| p | Named pipe (FIFO) |
Try it yourself:
$ ls -l /dev/random /dev/sda
crw-rw-rw- 1 root root 1, 8 Jul 20 09:00 /dev/random
brw-rw---- 1 root disk 8, 0 Jul 20 09:00 /dev/sda
Notice the c at the start of the /dev/random line and the b at the start of /dev/sda. The two numbers separated by a comma — 1, 8 and 8, 0 — are exactly what we discuss next: the major and minor numbers.
struct cdev: The Kernel’s Character Device Object
Every character device that a driver registers with the kernel is represented internally by an instance of struct cdev. This structure is how the kernel keeps track of which file operations belong to which device file.
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
You rarely touch every field of this structure directly. In modern kernel 6.x drivers you mostly interact with it through three helper calls:
cdev_init()— links yourstruct cdevwith yourfile_operationstablecdev_add()— registers the device with the kernel so user space can open itcdev_del()— removes the device when your module unloads
/dev/mychardev
major, minor
found
file_operations run
Major and Minor Numbers Explained
Every device file is identified by a pair of numbers: the major number and the minor number.
- The major number tells the kernel which driver handles this device.
- The minor number tells the driver which specific device instance is being addressed — useful when one driver manages several physical or logical devices, for example
/dev/ttyUSB0and/dev/ttyUSB1.
Internally, the kernel packs both numbers into a single 32-bit value of type dev_t: 12 bits for the major number and 20 bits for the minor number. You almost never build this value by hand — the kernel gives you three macros in include/linux/kdev_t.h:
MAJOR(dev_t dev); /* extract the major number */
MINOR(dev_t dev); /* extract the minor number */
MKDEV(int ma, int mi); /* build a dev_t from ma + mi */
| Macro | Purpose | Typical Use |
|---|---|---|
| MAJOR(dev) | Extracts the major number from a dev_t | Debug prints, matching driver to device |
| MINOR(dev) | Extracts the minor number from a dev_t | Selecting which sub-device to operate on |
| MKDEV(ma, mi) | Combines major and minor into one dev_t | Passing a device number into cdev_add() |
Hands-On: A Minimal Character Device on Kernel 6.x
Let’s put the theory into practice with a small, original driver. It registers one character device dynamically, wires it to struct cdev, and prints its major and minor number to the kernel log — nothing more, so you can focus purely on the concept.
// ep_chardev_intro.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kernel.h>
#define EP_DEVICE_NAME "ep_chardev_intro"
static dev_t ep_devnum;
static struct cdev ep_cdev;
static int ep_open(struct inode *inode, struct file *filp)
{
pr_info("%s: device opened\n", EP_DEVICE_NAME);
return 0;
}
static int ep_release(struct inode *inode, struct file *filp)
{
pr_info("%s: device closed\n", EP_DEVICE_NAME);
return 0;
}
static const struct file_operations ep_fops = {
.owner = THIS_MODULE,
.open = ep_open,
.release = ep_release,
};
static int __init ep_chardev_init(void)
{
int ret;
ret = alloc_chrdev_region(&ep_devnum, 0, 1, EP_DEVICE_NAME);
if (ret < 0) {
pr_err("%s: failed to allocate device number\n", EP_DEVICE_NAME);
return ret;
}
cdev_init(&ep_cdev, &ep_fops);
ep_cdev.owner = THIS_MODULE;
ret = cdev_add(&ep_cdev, ep_devnum, 1);
if (ret < 0) {
unregister_chrdev_region(ep_devnum, 1);
pr_err("%s: cdev_add failed\n", EP_DEVICE_NAME);
return ret;
}
pr_info("%s: registered with major=%d minor=%d\n",
EP_DEVICE_NAME, MAJOR(ep_devnum), MINOR(ep_devnum));
return 0;
}
static void __exit ep_chardev_exit(void)
{
cdev_del(&ep_cdev);
unregister_chrdev_region(ep_devnum, 1);
pr_info("%s: unloaded\n", EP_DEVICE_NAME);
}
module_init(ep_chardev_init);
module_exit(ep_chardev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Minimal character device driver demonstrating struct cdev and major/minor numbers");
Building and Loading the Module
$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
$ sudo insmod ep_chardev_intro.ko
$ dmesg | tail -n 3
Expected output in dmesg:
[ 1234.567890] ep_chardev_intro: registered with major=238 minor=0
The exact major number will differ on your machine since we used alloc_chrdev_region(), which lets the kernel pick a free major number for us — this is the recommended approach, and we cover exactly why in Part 2 of this chapter. Confirm the registration:
$ cat /proc/devices | grep ep_chardev_intro
238 ep_chardev_intro
To actually see the device file appear under /dev you need either mknod with the printed major/minor, or a udev rule — also covered in the next lecture alongside class_create() and device_create().
Common Mistakes and Troubleshooting
| Mistake | Why It Fails | Fix |
|---|---|---|
Forgetting unregister_chrdev_region() on exit |
Device number stays reserved after rmmod | Always mirror every allocation with a matching free call in the exit path |
Calling cdev_add() before cdev_init() |
The cdev has no file_operations attached yet | Always init before add |
| Ignoring return values | Silent failures are hard to debug later | Check every return code, as shown in the example |
Best Practices
- Prefer dynamic device number allocation over hardcoding a major number.
- Always pair
cdev_add()withcdev_del()in the module exit path. - Keep your
file_operationstableconst— it should never change at runtime. - Use
pr_info()/pr_err()instead of rawprintk()for consistent log formatting.
Suggested Images for This Article
- Screenshot of
ls -l /devoutput — ALT text: “linux character device driver ls -l /dev output showing major and minor numbers” - Screenshot of
dmesgoutput after loading the demo module — ALT text: “linux character device driver kernel log showing major and minor number registration” - Diagram of user space to kernel space device file flow — ALT text: “linux character device driver architecture diagram user space to kernel space”
Frequently Asked Questions
What is the difference between a character device and a block device?
A character device transfers data as a continuous stream with no random access, like a serial port. A block device transfers data in fixed-size blocks and supports seeking, like a hard disk.
Why does struct cdev matter in a linux character device driver?
struct cdev is the kernel’s internal representation that ties a registered device number to your driver’s file_operations table, so the kernel knows which functions to call when a user opens your device file.
Can one major number serve multiple devices?
Yes. The major number identifies the driver, while the minor number identifies which specific device instance within that driver is being accessed.
Is alloc_chrdev_region() better than register_chrdev_region()?
Generally yes for production drivers, since the kernel picks a free major number automatically, avoiding conflicts. We cover the full comparison in the next lecture.
Do I need to create the /dev file manually?
Yes, unless you use class_create() and device_create() to have udev create it automatically — that is covered in Part 2.
Which kernel version does this example target?
This example is written and tested against the modern Linux kernel 6.x API, using cdev_init(), cdev_add(), and alloc_chrdev_region().
Continue the Free Linux Kernel Development Course
Part 2 covers device number allocation strategies and the file_operations structure with a complete working driver.
