alloc_chrdev_region(), kernel internals of dynamic major number allocation, and manual device node creation with mknod📚 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(), andMINOR()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_paramto 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
Before starting this tutorial, make sure you have completed or are familiar with:
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.
| 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
| 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.
| 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:
| 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
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)— allowssudo insmod dynamic.ko bminor=5module_param(count, int, 0)— allowssudo insmod dynamic.ko count=3module_param(device_name, charp, 0)—charpmeans “char pointer” (string)- The third argument
0means no sysfs visibility (permissions = 0). Use0644to expose under/sys/module/.
== 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.
-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
| 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
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
Is chrdevs[i] == NULL?
Typical result: 511 on a fresh system
Scan 511 down to 385 via hash chain
Walk linked list; find unoccupied major
-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");
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:
- Manual — using the
mknodcommand (covered in this section) - Automatic — using
udevwithclass_create()anddevice_create()(covered in a later tutorial)
8.1 The mknod Syntax
$ mknod -m <permissions> <path> <type> <major> <minor>
| 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.
open("/dev/mydevice", ...)read(...) / write(...)/dev/mydeviceLives in the filesystem
Created by
mknod or udevContains: major + minor numbers
It is just a pointer!
Looks up
chrdevs[]Routes to correct driver
dynamic.koLoaded in kernel space
Has
file_operationsControls actual hardware
| 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/mydeviceappears inls /dev/— but you will NOT see it incat /proc/devicesmychdrvappears incat /proc/devices— but you needls /dev/to see the device file
/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
| # | 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: rmmod → rm /dev/xxx → insmod → mknod |
| 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
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.
dev_t at Module LevelDeclare 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.
printkAlways 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.
__init and __exit AttributesMark 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.
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
- 🔵 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_tholds both major and minor, extracted viaMAJOR()andMINOR() - 🔵
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
- 🔵
mknodcreates device files manually; they must be deleted manually (userm) - 🔵 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/devicesshows 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)
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.
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().
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.
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.
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).
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.
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.
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.
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.
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.
- 🔗 Linux Kernel Source — fs/char_dev.c (Bootlin Elixir) — The actual source of
alloc_chrdev_region()andfind_dynamic_major() - 🔗 Linux Kernel Device List — kernel.org — Official registry of statically assigned major/minor numbers
- 🔗 Linux Driver Model — kernel.org Driver API
- 🔗 mknod(2) — Linux man page
EmbeddedPathashala offers completely free courses on Linux kernel development, embedded systems, Bluetooth/BLE, and more — taught by working engineers.
