Dynamic Allocation of Character Device Numbers in Linux Kernel

 

 

Dynamic Allocation of Character Device Numbers in Linux Kernel
Master alloc_chrdev_region(), kernel internals of dynamic major number allocation, and manual device node creation with mknod
🎯 Beginner Friendly
🐧 Linux 6.x Ready
💻 Hands-On Labs
🆓 Free Course

🔑 Keywords Covered in This Tutorial
dynamic allocation char device driver alloc_chrdev_region unregister_chrdev_region major minor number dev_t MAJOR() MINOR() find_dynamic_major mknod device node /proc/devices Linux kernel module free Linux device driver course

📚 What You Will Learn

In this free Linux device driver tutorial from EmbeddedPathashala, you will learn everything about dynamic allocation of character device numbers in the Linux kernel. This is a critical skill for anyone entering embedded systems or kernel development, and it is covered in detail in our free Linux kernel development course.

  • ✅ Why dynamic allocation is preferred over static allocation for character device numbers
  • ✅ How to use alloc_chrdev_region() to allocate major numbers at runtime
  • ✅ Understanding dev_t, MAJOR(), and MINOR() macros
  • ✅ Deep dive into the Linux kernel’s find_dynamic_major() internal function
  • ✅ Understanding dynamic major number allocation segments (standard + extended)
  • ✅ How to use module_param to pass parameters to your driver
  • ✅ How to verify device registration using /proc/devices
  • ✅ How to manually create device nodes using mknod
  • ✅ Difference between a device file and a device driver
  • ✅ Building and testing with a complete Makefile

🔧 Prerequisites

Before starting this tutorial, make sure you have completed or are familiar with:

Linux Kernel Module Basics Static Device Number Registration (register_chrdev_region) Understanding of Major & Minor Numbers dev_t and MKDEV() macro insmod / rmmod / dmesg commands Basic C programming

If you haven’t completed the static allocation tutorial yet, we strongly recommend going through Tutorial 09 – Static Allocation of Character Device Numbers first, available on EmbeddedPathashala.

1. Why Dynamic Allocation? — The Problem with Static Registration

In the previous tutorial (Tutorial 09), we used register_chrdev_region() to statically reserve a fixed major number for our character device driver. While that works for learning and controlled environments, it has a real-world problem: major number conflicts.

Every Linux system has a fixed set of character device major numbers ranging from 0 to 511. Many of these are already reserved by well-known drivers — for example, major number 1 is for mem, major 4 for tty, major 8 for sd (SCSI disks), etc. When you statically pick a major number, there is no guarantee it is free on every system. Your driver may work perfectly on your development machine but fail on a production system or another developer’s laptop because that major number is already taken.

The solution is dynamic allocation of character device numbers: instead of telling the kernel “I want major number 244”, you ask the kernel “please give me any free major number”, and the kernel picks one for you at load time. This is the recommended approach in professional Linux driver development.

Static vs Dynamic Device Number Registration — Comparison
Aspect Static Allocation (register_chrdev_region) Dynamic Allocation (alloc_chrdev_region)
Major Number Fixed, chosen by developer Assigned by kernel at runtime
Risk of Conflict High — may clash with existing drivers None — kernel guarantees a free slot
Portability Low — system-specific High — works on any Linux system
Recommended Use Lab learning only Production drivers
How to Find Major# You know it (you chose it) Read from dev_t via MAJOR() or /proc/devices

2. The alloc_chrdev_region() API — Dynamic Allocation of Character Device Numbers

The Linux kernel provides the alloc_chrdev_region() function for dynamic allocation of character device numbers. It is declared in <linux/fs.h>.

2.1 Function Prototype

int alloc_chrdev_region(dev_t *dev,
                        unsigned int baseminor,
                        unsigned int count,
                        const char *name);

2.2 Parameter Details

alloc_chrdev_region() — Parameter Breakdown
Parameter Type Direction Description
dev dev_t * Output Pointer to a dev_t variable. After the call, this holds the first assigned device number (major + base minor encoded together)
baseminor unsigned int Input The starting minor number you want. Usually 0. Minor numbers are not dynamically allocated — you choose them.
count unsigned int Input Number of consecutive minor numbers to reserve. Pass 1 if you have only one device.
name const char * Input Name string for the device. This appears in /proc/devices after registration.

2.3 Return Value

Returns 0 on success. Returns a negative error code on failure (e.g., -EBUSY if no major numbers are available, -EINVAL for invalid arguments).

⚠️ Important: Unlike register_chrdev_region() where you know the major number beforehand, here you must read it back from the dev_t output parameter using the MAJOR() macro after a successful call.

2.4 The Companion Cleanup Function

Every successful call to alloc_chrdev_region() must be paired with a call to unregister_chrdev_region() when the module is removed, to release the reserved numbers back to the kernel.

void unregister_chrdev_region(dev_t from, unsigned int count);
/* from  – the first device number (your dev_t variable)
   count – same count you passed to alloc_chrdev_region() */

3. Understanding dev_t, MAJOR(), and MINOR()

Before writing the driver, it is essential to understand how the Linux kernel packs device numbers. dev_t is a 32-bit integer type (defined in <linux/kdev_t.h>) that encodes both the major and minor numbers in a single value.

dev_t — 32-bit Bit Layout (Major + Minor packed together)

MAJOR — bits [31..20] and [7..0] (12 bits total) MINOR — bits [19..8] and … (20 bits total)
Total: 32 bits → supports major 0–4095, minor 0–1048575

The kernel uses bit-manipulation macros to extract these fields:

dev_t devnum;

// After alloc_chrdev_region():
unsigned int major = MAJOR(devnum);   // extract major from dev_t
unsigned int minor = MINOR(devnum);   // extract minor from dev_t

// To compose a dev_t manually:
dev_t d = MKDEV(major, minor);        // pack major+minor → dev_t

3.1 Required Header Files

These four headers are the minimum required for any character device driver that uses dynamic allocation:

Required Header Files and Their Purpose
Header File What It Provides
<linux/kernel.h> printk(), kernel type definitions, macros like ARRAY_SIZE()
<linux/module.h> MODULE_LICENSE, module_init(), module_exit(), module_param()
<linux/kdev_t.h> dev_t type, MAJOR(), MINOR(), MKDEV() macros
<linux/fs.h> alloc_chrdev_region(), unregister_chrdev_region(), register_chrdev_region(), file operations

4. Complete Dynamic Allocation Character Driver — Step-by-Step

Now let’s build the complete driver. We will add module_param support so we can configure the minor number, device count, and device name from the command line when inserting the module — a very useful pattern in real drivers.

4.1 Full Source Code: dynamic.c

/* dynamic.c — Dynamic allocation of character device numbers
 * Tutorial 10 | EmbeddedPathashala
 * Free Linux Device Driver Course
 */

#include <linux/kernel.h>    /* printk, KERN_INFO */
#include <linux/module.h>    /* module_init, module_exit, module_param, MODULE_LICENSE */
#include <linux/kdev_t.h>    /* dev_t, MAJOR(), MINOR() */
#include <linux/fs.h>        /* alloc_chrdev_region, unregister_chrdev_region */

/* ─── Module parameters (can be overridden at insmod time) ─── */
static int bminor = 0;                  /* base minor number  */
static char *device_name = "mychdrv";  /* name shown in /proc/devices */
static int count = 1;                  /* how many minor numbers to reserve */

module_param(bminor, int, 0);          /* insmod dynamic.ko bminor=5   */
module_param(count, int, 0);           /* insmod dynamic.ko count=3    */
module_param(device_name, charp, 0);   /* insmod dynamic.ko device_name="mydev" */

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Dynamic char device number allocation example");

/* ─── dev_t variable to hold our assigned device number ─── */
static dev_t devnum;   /* kernel will write the allocated dev_t here */

/* ─────────────────────────────────────────────────────────── */
/*  Module init                                               */
/* ─────────────────────────────────────────────────────────── */
static int __init hello_init(void)
{
    printk(KERN_INFO "dynamic: Loading module...\n");
    printk(KERN_INFO "dynamic: baseminor   = %d\n", bminor);
    printk(KERN_INFO "dynamic: count       = %d\n", count);
    printk(KERN_INFO "dynamic: device_name = %s\n", device_name);

    /*
     * alloc_chrdev_region() — ask the kernel to pick a free major number
     *
     * &devnum      — output: kernel writes assigned dev_t here
     * bminor       — starting minor number (we choose, default 0)
     * count        — how many consecutive minor numbers to reserve
     * device_name  — name that appears in /proc/devices
     *
     * Returns 0 on success, negative errno on failure.
     */
    if (alloc_chrdev_region(&devnum, bminor, count, device_name) == 0) {
        printk(KERN_INFO "dynamic: Device number registered successfully\n");
        printk(KERN_INFO "dynamic: Major number = %d\n", MAJOR(devnum));
        printk(KERN_INFO "dynamic: Minor number = %d\n", MINOR(devnum));
    } else {
        printk(KERN_ERR "dynamic: Device number registration FAILED\n");
        return -EINVAL;
    }

    return 0;  /* 0 = success; non-zero = module load fails */
}

/* ─────────────────────────────────────────────────────────── */
/*  Module exit                                               */
/* ─────────────────────────────────────────────────────────── */
static void __exit hello_exit(void)
{
    /*
     * Always release the device numbers in the exit function.
     * Pass the same dev_t and count used during allocation.
     */
    unregister_chrdev_region(devnum, count);
    printk(KERN_INFO "dynamic: Device number released. Module unloaded.\n");
}

module_init(hello_init);
module_exit(hello_exit);

4.2 Line-by-Line Explanation

Understanding module_param()

module_param(variable, type, permissions) exposes a kernel variable as a module parameter that can be set at insmod time:

  • module_param(bminor, int, 0) — allows sudo insmod dynamic.ko bminor=5
  • module_param(count, int, 0) — allows sudo insmod dynamic.ko count=3
  • module_param(device_name, charp, 0)charp means “char pointer” (string)
  • The third argument 0 means no sysfs visibility (permissions = 0). Use 0644 to expose under /sys/module/.
Why We Check Return Value with == 0 (not != 0)

Notice we used if (alloc_chrdev_region(...) == 0) for the success path. This is cleaner and more readable than if (!alloc_chrdev_region(...)). Both work — in your draft code if(!alloc_chrdev_region(...)) was used, which is valid because 0 is falsy in C. However, using == 0 makes the success condition explicit, improving readability in production code.

Why Return -EINVAL on Failure?

When alloc_chrdev_region() fails and we return a non-zero value from __init, the kernel automatically rejects the insmod operation and prints an appropriate error. Returning -EINVAL (or the actual error from the function) propagates the error to userspace — dmesg will show it.

5. The Makefile — Building the Kernel Module

# Makefile for dynamic char driver
# Tutorial 10 | EmbeddedPathashala

obj-m += dynamic.o          # Tell the kernel build system to build dynamic.ko

KDIR = /lib/modules/$(shell uname -r)/build   # Path to running kernel build tree
PWD  = $(shell pwd)                           # Current working directory

all:
	make -C $(KDIR) M=$(PWD) modules         # Invoke kernel Makefile with our dir

clean:
	make -C $(KDIR) M=$(PWD) clean           # Remove build artifacts

Build the module with:

$ make
# Successful build produces: dynamic.ko

6. Loading, Verifying, and Unloading the Driver

6.1 Insert the Module

$ sudo insmod ./dynamic.ko

6.2 Check Kernel Log with dmesg

$ dmesg | tail -10

# Expected output:
[ 1462.540674] dynamic: Loading module...
[ 1462.540675] dynamic: baseminor   = 0
[ 1462.540676] dynamic: count       = 1
[ 1462.540677] dynamic: device_name = mychdrv
[ 1462.540678] dynamic: Device number registered successfully
[ 1462.540679] dynamic: Major number = 511
[ 1462.540680] dynamic: Minor number = 0

Notice that the kernel assigned major number 511. This is not random — it reflects the kernel’s internal algorithm for picking dynamic major numbers. We will explore this algorithm in the next section.

6.3 Verify in /proc/devices

$ cat /proc/devices

# You will see your device listed under "Character devices":
Character devices:
  1 mem
  4 /dev/vc/0
  ...
511 mychdrv      <-- Your dynamically allocated driver!

6.4 Insert with Custom Parameters

# Override device name and starting minor number:
$ sudo insmod ./dynamic.ko device_name="myep" bminor=10 count=2

# dmesg will show:
[ ... ] dynamic: baseminor   = 10
[ ... ] dynamic: count       = 2
[ ... ] dynamic: device_name = myep
[ ... ] dynamic: Major number = 510   (next available)

6.5 Remove the Module

$ sudo rmmod dynamic

# After rmmod, check /proc/devices:
$ cat /proc/devices
# "mychdrv" is GONE — the major number has been released back to the kernel pool

7. Deep Dive — How the Kernel Picks Dynamic Major Numbers (find_dynamic_major())

This section takes you under the hood of the kernel. Understanding how the kernel picks major numbers will help you reason about driver behaviour, especially in embedded systems where resources are constrained.

When you call alloc_chrdev_region(), the kernel internally calls a static function called find_dynamic_major(), located in /linux/fs/char_dev.c.

7.1 The Four Key Macros

Dynamic Major Number Allocation Segments — Macro Values
Macro Value Meaning
CHRDEV_MAJOR_MAX 512 Maximum possible major number. The chrdevs[] array has 512 entries (index 0–511).
CHRDEV_MAJOR_DYN_END 234 Bottom boundary of the standard dynamic range. The standard segment goes from 511 down to 235 (descending scan).
CHRDEV_MAJOR_DYN_EXT_START 511 Start of the extended dynamic range. When the standard range is exhausted, the kernel scans from here downward (through hash-colliding slots).
CHRDEV_MAJOR_DYN_EXT_END 384 Bottom boundary of the extended range. Covers majors 385–511 via hash chaining (these majors hash to the same slots as existing majors).

7.2 Visualising the Allocation Segments

Character Device Major Number Space — Allocation Segments (0 to 511)
0 — 233Static / Well-known drivers
235 — 511Standard Dynamic Range
385 — 511Extended Dynamic Range
Reserved & Static (kernel + major vendors)
First allocation attempt (511→235)
Fallback via hash chaining (511→385)

7.3 The find_dynamic_major() Source Code

Here is the actual kernel source code from /linux/fs/char_dev.c that implements the two-stage search:

/* From: linux/fs/char_dev.c */
static int find_dynamic_major(void)
{
    int i;
    struct char_device_struct *cd;

    /* ── Stage 1: Scan the STANDARD dynamic major range ──
     * Start from slot 511 (ARRAY_SIZE(chrdevs)-1) and
     * scan downward to CHRDEV_MAJOR_DYN_END (234).
     * chrdevs[] is an array of 512 pointers.
     * If a slot is NULL, that major number is free → return it.
     */
    for (i = ARRAY_SIZE(chrdevs) - 1; i >= CHRDEV_MAJOR_DYN_END; i--) {
        if (chrdevs[i] == NULL)
            return i;   /* ← Found a free slot in standard range */
    }

    /* ── Stage 2: Scan the EXTENDED dynamic major range ──
     * When majors above 255 need to share hash slots (because
     * chrdevs[] has only 512 entries but majors can theoretically
     * go higher via 12-bit encoding), we scan from
     * CHRDEV_MAJOR_DYN_EXT_START (511) down to
     * CHRDEV_MAJOR_DYN_EXT_END (384).
     *
     * For each candidate major, walk the linked list at
     * chrdevs[major_to_index(i)]. If no node matches this
     * exact major, it's available.
     */
    for (i = CHRDEV_MAJOR_DYN_EXT_START; i > CHRDEV_MAJOR_DYN_EXT_END; i--) {
        for (cd = chrdevs[major_to_index(i)]; cd; cd = cd->next)
            if (cd->major == i)
                break;

        if (cd == NULL || cd->major != i)
            return i;   /* ← Found a free slot in extended range */
    }

    return -EBUSY;  /* ← All dynamic major numbers are exhausted */
}

7.4 Step-by-Step Walk Through the Algorithm

find_dynamic_major() — Decision Flow
alloc_chrdev_region() called
Scan chrdevs[] from index 511 down to 234 (standard range)
Is chrdevs[i] == NULL?
YES ✅
Return that index as the major number
Typical result: 511 on a fresh system
NO ❌ (all taken)
Move to Stage 2: Extended range
Scan 511 down to 385 via hash chain
Walk linked list; find unoccupied major
If both stages exhausted → return -EBUSY
“CHRDEV dynamic allocation region is full”

7.5 Experiment: Exhausting the Dynamic Major Pool

Your draft mentioned a very interesting experiment: load a module 512 times in a loop. Here is what happens and why you need a reboot to recover:

/* experiment_exhaust.c — Demonstrate dynamic major number exhaustion */
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>

#define MAX_DEVS 512

static dev_t devnums[MAX_DEVS];
static int allocated = 0;

static int __init exhaust_init(void)
{
    int i, ret;
    char name[32];

    for (i = 0; i < MAX_DEVS; i++) {
        snprintf(name, sizeof(name), "mychardev_%d", i);
        ret = alloc_chrdev_region(&devnums[i], 0, 1, name);
        if (ret < 0) {
            printk(KERN_ERR "exhaust: FAILED at iteration %d: %d\n", i, ret);
            /* You will see the error message from char_dev.c:
             * CHRDEV "mychardev_NNN" dynamic allocation region is full */
            break;
        }
        printk(KERN_INFO "exhaust: [%d] major=%d\n", i, MAJOR(devnums[i]));
        allocated++;
    }
    printk(KERN_INFO "exhaust: Total allocated: %d\n", allocated);
    return 0;
}

static void __exit exhaust_exit(void)
{
    int i;
    for (i = 0; i < allocated; i++)
        unregister_chrdev_region(devnums[i], 1);
}

module_init(exhaust_init);
module_exit(exhaust_exit);
MODULE_LICENSE("GPL");
⚠️ Why You Need a Reboot (Not Just rmmod) to Recover

When you call alloc_chrdev_region() in a loop without a matching unregister_chrdev_region(), the major numbers accumulate in chrdevs[]. Even if you rmmod your module without properly releasing them, the registrations may persist in kernel memory until reboot. That is why the draft mentioned “you should only reboot not rmmod” — if your hello_exit() doesn’t call unregister_chrdev_region(), or if the module crashes before cleanup, only a reboot clears the slate.

Lesson: Always register a proper module_exit() function that calls unregister_chrdev_region(). Never skip the cleanup path.

8. Creating Device Nodes — The mknod Command

After successfully loading the driver and registering device numbers, the kernel knows about your driver. But userspace applications cannot access it yet because there is no device file (also called a device node) in the /dev/ directory. Device files are the interface between userspace and the kernel driver.

There are two ways to create device files:

  1. Manual — using the mknod command (covered in this section)
  2. Automatic — using udev with class_create() and device_create() (covered in a later tutorial)

8.1 The mknod Syntax

$ mknod -m <permissions> <path> <type> <major> <minor>

mknod — Argument Breakdown
Argument Meaning Example
-m permissions Optional. File permission bits (like chmod). If omitted, uses umask. -m 0644
path Full path to the device file. Must include /dev/ prefix. /dev/mydevice
type c for character device, b for block device, p for named pipe (FIFO) c
major Major number of your driver (obtained from dmesg / MAJOR(devnum)) 511
minor Minor number (same as baseminor you passed to alloc_chrdev_region) 0

8.2 Complete mknod Example

# Step 1: Load the module and find the assigned major number
$ sudo insmod ./dynamic.ko
$ dmesg | grep "Major number"
[...] dynamic: Major number = 511

# Step 2: Create the device node manually
$ sudo mknod -m 0644 /dev/mydevice c 511 0
#                    ↑              ↑ ↑   ↑
#                    permissions    | |   minor
#                                   | char type
#                                   major from dmesg

# Step 3: Verify the device file was created
$ ls -l /dev/mydevice
crw-r--r-- 1 root root 511, 0 Jun 01 10:45 /dev/mydevice
#  ↑                   ↑    ↑
#  c = char device    511   0
#                  major   minor

# Step 4: Check /proc/devices (driver listed here)
$ cat /proc/devices | grep mychdrv
511 mychdrv

# NOTE: /dev/mydevice appears in ls /dev/ but NOT in /proc/devices
# /proc/devices shows DRIVERS (kernel side)
# /dev/ shows DEVICE FILES (filesystem side)

8.3 Deleting the Device Node

# Device nodes created with mknod are regular files — delete with rm
$ sudo rm /dev/mydevice

# Or to remove all files matching the pattern:
$ sudo rm /dev/mydevice*

9. Key Concept — Device File vs. Device Driver

Your draft notes asked: “What is the difference between a device file and a device driver?” This is an excellent conceptual question that trips up many beginners.

Device File vs Device Driver — Architecture

👤 Userspace App
open("/dev/mydevice", ...)
read(...) / write(...)

📄 Device File
/dev/mydevice
Lives in the filesystem
Created by mknod or udev
Contains: major + minor numbers
It is just a pointer!

🔄 Kernel VFS
Extracts major + minor
Looks up chrdevs[]
Routes to correct driver

⚙️ Device Driver
dynamic.ko
Loaded in kernel space
Has file_operations
Controls actual hardware
The device file is only a name + major/minor number stored on disk. The device driver is the actual kernel code. The kernel VFS bridges them.

Device File vs Device Driver — Key Differences
Property Device File (/dev/xxx) Device Driver (.ko module)
Where it lives Filesystem (/dev/) Kernel memory (after insmod)
Created by mknod or udev insmod / modprobe
Contains inode with file type + major + minor Compiled kernel code, file_operations
Visible in ls /dev/, stat /dev/xxx /proc/devices, lsmod
Deleted by rm /dev/xxx rmmod
Can exist without the other? Yes — device file without driver = open() fails with ENXIO Yes — driver without device file = userspace can’t reach it

10. /proc/devices vs /dev/ — Clearing the Confusion

Many beginners confuse these two. After your hands-on experiments you will notice:

  • /dev/mydevice appears in ls /dev/ — but you will NOT see it in cat /proc/devices
  • mychdrv appears in cat /proc/devices — but you need ls /dev/ to see the device file
The Golden Rule

/proc/devices lists registered kernel drivers (character and block), indexed by major number. It is a kernel-side view.

/dev/ lists device files on the filesystem. Each file encodes a major and minor number but is otherwise just an inode. It is a userspace-side view.

The kernel connects them at runtime: when userspace opens /dev/mydevice, the kernel reads its major number (511), looks it up in chrdevs[511], and dispatches the call to our dynamic.ko driver.

11. Common Mistakes and How to Avoid Them

Common Mistakes in Dynamic Allocation — Troubleshooting Guide
# Mistake Symptom Fix
1 Forgot unregister_chrdev_region() in exit function Major number stays reserved after rmmod; future loads may get a different major Always pair alloc with unregister in module_exit()
2 Hardcoding major number in mknod without checking dmesg Device file uses wrong major; open() fails with ENXIO Always read major from dmesg or MAJOR(devnum) after loading
3 Not checking return value of alloc_chrdev_region() Module loads but devnum is garbage; MAJOR(devnum) returns wrong value Always check return value and return error from init on failure
4 Using alloc_chardev_region (wrong spelling) Compile error: “implicit declaration of function” Correct spelling: alloc_chrdev_region (not chardev)
5 Forgetting that the device node must be deleted manually after rmmod Stale /dev/mydevice remains; next insmod may use different major → device file points to wrong driver Script your workflow: rmmodrm /dev/xxxinsmodmknod
6 Filling the dynamic pool and expecting rmmod to fix it “CHRDEV dynamic allocation region is full” — rmmod doesn’t help Reboot the system to clear the pool

12. Best Practices for Dynamic Character Device Registration

✅ Best Practice 1 — Always Use Dynamic Allocation in Production

Never hardcode a major number in production driver code. Use alloc_chrdev_region(). Reserve static numbers only if your driver is being submitted to the official kernel tree and assigned a number in Documentation/admin-guide/devices.txt.

✅ Best Practice 2 — Store dev_t at Module Level

Declare your dev_t devnum as a static global in your module. You need it both in module_init() (to receive the result) and in module_exit() (to unregister). Local variables won’t work for this.

✅ Best Practice 3 — Print the Assigned Major in printk

Always log the dynamically assigned major number using printk(KERN_INFO "Major = %d\n", MAJOR(devnum)). This is essential for creating device nodes correctly with mknod or for debugging udev rules.

✅ Best Practice 4 — Use __init and __exit Attributes

Mark your init function with __init and your exit function with __exit. The kernel can then free the init function’s memory after boot (for built-in drivers), saving RAM. For loadable modules it has no effect but is good practice and signals intent clearly.

✅ Best Practice 5 — Plan for Automatic Device Node Creation

While mknod is great for learning and quick tests, production drivers should use class_create() and device_create() to automatically generate device nodes via udev. This will be covered in the next tutorial: Tutorial 11 – Automatic Device Node Creation with udev.

13. Complete Hands-On Workflow — From Source to Device Node

# ─── Step 1: Create project directory ───
$ mkdir ~/ep_dynamic_driver && cd ~/ep_dynamic_driver

# ─── Step 2: Create dynamic.c and Makefile (shown in sections 4 and 5) ───

# ─── Step 3: Build the kernel module ───
$ make
# Produces: dynamic.ko

# ─── Step 4: Insert the module ───
$ sudo insmod ./dynamic.ko
# OR with custom params:
$ sudo insmod ./dynamic.ko device_name="ep_char" bminor=0 count=1

# ─── Step 5: Check the assigned major number ───
$ dmesg | tail -5
# Note the Major number (e.g., 511)

# ─── Step 6: Verify in /proc/devices ───
$ cat /proc/devices | grep -E "ep_char|mychdrv"
511 mychdrv

# ─── Step 7: Create the device node ───
$ sudo mknod -m 0666 /dev/ep_char c 511 0

# ─── Step 8: Verify device node ───
$ ls -l /dev/ep_char
crw-rw-rw- 1 root root 511, 0 Jun 01 10:50 /dev/ep_char

# ─── Step 9: Test (basic — no file_ops implemented yet) ───
$ cat /dev/ep_char
# Will fail with "Operation not supported" — expected! No read() implemented yet.

# ─── Step 10: Cleanup ───
$ sudo rm /dev/ep_char          # delete device node first
$ sudo rmmod dynamic            # then unload the module
$ dmesg | tail -3               # verify unregister message

14. Summary & Key Takeaways

🎯 What We Covered
  • 🔵 Dynamic allocation is the recommended way to get a major number — no conflicts, no manual management
  • 🔵 alloc_chrdev_region(&devnum, baseminor, count, name) is the kernel API — only the major is dynamically chosen; you pick the minor
  • 🔵 The allocated dev_t holds both major and minor, extracted via MAJOR() and MINOR()
  • 🔵 module_param() makes your driver configurable at insert time — great for reusable, flexible drivers
  • 🔵 The kernel’s find_dynamic_major() scans two ranges: standard (511→235) then extended (511→385)
  • 🔵 Filling the pool without releasing (no rmmod or missing unregister) requires a reboot to recover
  • 🔵 mknod creates device files manually; they must be deleted manually (use rm)
  • 🔵 A device file is a filesystem entry with major+minor; a device driver is kernel code — they are linked by the VFS at runtime
  • 🔵 /proc/devices shows registered drivers; /dev/ shows device files — these are different things

15. Conclusion

In this free Linux device driver tutorial from EmbeddedPathashala, you have mastered the complete concept of dynamic allocation of character device numbers. You started from understanding why dynamic allocation is necessary, learned the alloc_chrdev_region() API in depth, explored the actual kernel source code of find_dynamic_major() to understand the two allocation segments (standard: 511→235, extended: 511→385), built and tested a real kernel module with module_param support, and finally learned how to manually create device nodes using mknod.

You also clarified a conceptual distinction that confuses many engineers: the difference between a device file in /dev/ and a device driver in kernel memory. These are two separate entities that the kernel’s VFS bridges at runtime using the major number as the key.

The next logical step is to move beyond just reserving device numbers and actually implement the file_operations structure — the open, read, write, and release handlers that make your driver actually do something. That is covered in Tutorial 11 on our free Linux kernel development course at EmbeddedPathashala.

16. Frequently Asked Questions (FAQ)

Q1. What is dynamic allocation in a Linux character device driver?

Dynamic allocation means asking the Linux kernel to pick an available major number for your character device driver at module load time, rather than hardcoding a fixed number. This is done using the alloc_chrdev_region() kernel API. It is the recommended approach in professional Linux driver development because it avoids conflicts with other drivers.

Q2. What is the difference between alloc_chrdev_region() and register_chrdev_region()?

register_chrdev_region() is used when you already know the major number you want and want to reserve it statically. alloc_chrdev_region() is used when you want the kernel to assign any available major number dynamically. For production drivers, always use alloc_chrdev_region().

Q3. How do I find the major number assigned by alloc_chrdev_region()?

The function writes the assigned device number into the dev_t pointer you pass as the first argument. Extract the major number using the MAJOR(devnum) macro. You can also log it with printk and read it from dmesg, or look it up in /proc/devices.

Q4. Why does the kernel assign major number 511 on my system?

The kernel’s find_dynamic_major() function scans the chrdevs[] array starting from index 511 (the maximum) and works downward looking for a free slot. On a freshly booted system with few custom drivers, index 511 is typically free, so it is assigned first. As you load more drivers, you will see 510, 509, etc.

Q5. What are the two dynamic major number ranges in the Linux kernel?

The Linux kernel has a standard dynamic range (major 235 to 511, scanned from 511 downward) and an extended dynamic range (major 385 to 511, used when the standard range is exhausted). These are controlled by four macros: CHRDEV_MAJOR_MAX (512), CHRDEV_MAJOR_DYN_END (234), CHRDEV_MAJOR_DYN_EXT_START (511), and CHRDEV_MAJOR_DYN_EXT_END (384).

Q6. Why do I need a reboot after filling the dynamic major pool?

If you allocate major numbers in a loop without releasing them (e.g., module crashes before exit, or exit function is missing), the registrations persist in kernel memory even after rmmod. Since the pool is stored in the in-memory chrdevs[] array, only a system reboot resets it completely. This is why proper cleanup in module_exit() using unregister_chrdev_region() is critical.

Q7. What is the purpose of the mknod command in device driver development?

mknod creates a special file (device node) in the /dev/ filesystem. This device file is what userspace applications open and interact with. It contains the major and minor numbers that tell the kernel which driver to route the call to. Without a device node, userspace cannot interact with your driver even if it is loaded correctly.

Q8. Why doesn’t my mknod-created device appear in /proc/devices?

/proc/devices lists kernel-registered character and block drivers, indexed by major number. It is a kernel-side view. Device files in /dev/ are filesystem entries and are not listed in /proc/devices. They serve different purposes: /proc/devices shows drivers; /dev/ shows access points for userspace.

Q9. What does module_param(device_name, charp, 0) do?

module_param() exposes a kernel variable as a loadable module parameter. charp is the type token for “char pointer” (a string). The third argument 0 sets the sysfs permission to none (not visible under /sys/module/). This allows you to run sudo insmod dynamic.ko device_name="myep" to override the default device name without recompiling.

Q10. Is static allocation (register_chrdev_region) ever appropriate?

Yes, but only in very specific situations: (1) when your driver is being submitted to the Linux mainline kernel and officially assigned a major number in Documentation/admin-guide/devices.txt, or (2) during early learning to understand how device numbers work. For all other cases — especially in embedded systems products and custom drivers — use dynamic allocation.

📎 Authoritative References

🚀 Continue Your Free Linux Device Driver Journey

EmbeddedPathashala offers completely free courses on Linux kernel development, embedded systems, Bluetooth/BLE, and more — taught by working engineers.

📚 All Courses (Free) ▶️ Next: Auto Device Nodes with udev

Leave a Reply

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