L1- Linux Character Device Driver Basics- Free Linux Device Drivers Course

 

Linux Character Device Driver Basics

Part 1 of the Character Device Drivers chapter — free Linux kernel development course by EmbeddedPathashala

Kernel 6.x Ready
Beginner Friendly
Hands-On Demo

free embedded systems course
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

What a character device driver actually does
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

Basic C programming
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 your struct cdev with your file_operations table
  • cdev_add() — registers the device with the kernel so user space can open it
  • cdev_del() — removes the device when your module unloads
How struct cdev Connects a Device File to Your Driver
User opens
/dev/mychardev
Kernel matches
major, minor
struct cdev
found
Your driver’s
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/ttyUSB0 and /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() with cdev_del() in the module exit path.
  • Keep your file_operations table const — it should never change at runtime.
  • Use pr_info()/pr_err() instead of raw printk() for consistent log formatting.

Suggested Images for This Article

  • Screenshot of ls -l /dev output — ALT text: “linux character device driver ls -l /dev output showing major and minor numbers”
  • Screenshot of dmesg output 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.

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *