Static Device Number Allocation in Linux Character Device Drivers
Learn how to register character device numbers statically using register_chrdev_region() — a foundational step in Linux kernel driver development
Welcome to this free Linux kernel device driver tutorial from EmbeddedPathashala. In this lecture you will learn about static device number allocation in Linux character device drivers. Static allocation is the first method a driver writer encounters when learning how the Linux kernel assigns major and minor numbers to devices. Understanding this concept completely — including its limitations and failure cases — is essential before you move on to dynamic allocation. Every experiment below was run on a real kernel so you can see exactly what the kernel says back to you.
What You Will Learn
- What a device number is and how it is composed of a major and minor number
- How to use
MKDEV(),MAJOR(), andMINOR()kernel macros - How to statically allocate a device number with
register_chrdev_region() - What happens when you try to reuse an already-taken major number
- What happens when you reuse an existing device name with a different major number
- The maximum allowed major number (
CHRDEV_MAJOR_MAX) and why exceeding 511 fails - The maximum number of minor numbers (2²⁰) and what overflow looks like
- How to unregister a device number when your module exits
- Why static allocation is not recommended for production drivers
Prerequisites
- Basic understanding of Linux kernel modules (insmod / rmmod / dmesg)
- Familiarity with writing a simple
hello_init/hello_exitkernel module - A Linux machine with kernel headers installed (tested on Linux 6.x)
- Basic C programming knowledge
What Is a Linux Device Number?
In Linux, every character device and block device is identified by a device number. The device number is a single 32-bit integer of type dev_t that encodes two pieces of information:
- Major Number — identifies which driver is responsible for the device. All devices controlled by the same driver share the same major number.
- Minor Number — identifies a specific instance managed by that driver. For example, a single UART driver might manage
/dev/ttyS0,/dev/ttyS1,/dev/ttyS2. Each of these gets a different minor number but shares the same major number.
When user space opens /dev/mydevice, the kernel reads the major number embedded in the device file’s inode, looks up the registered driver for that major number, and hands the open() call to that driver. The minor number is passed along so the driver knows which instance is being accessed.
How dev_t Packs Major and Minor Numbers
| Bits 31 – 20 MAJOR (12 bits) Values: 0 – 4095 (kernel enforces max 511) | Bits 19 – 0 MINOR (20 bits) Values: 0 – 1,048,575 (2²⁰ − 1) | ||||||||||||||||||||||||||||||
| MAJOR(dev) extracts these bits | MINOR(dev) extracts these bits | ||||||||||||||||||||||||||||||
| MKDEV(major, minor) combines both fields into a single dev_t value | |||||||||||||||||||||||||||||||
Kernel Macros: MKDEV, MAJOR, MINOR
The kernel provides three macros in <linux/kdev_t.h> to work with device numbers. You should never manipulate the bits of dev_t directly — always use these macros so your code remains portable across kernel versions.
| Macro | Header | Purpose | Example |
|---|---|---|---|
MKDEV(major, minor) |
linux/kdev_t.h |
Combine major + minor into a dev_t |
devnum = MKDEV(120, 0); |
MAJOR(dev) |
linux/kdev_t.h |
Extract the major number from dev_t |
printk("%d", MAJOR(devnum)); |
MINOR(dev) |
linux/kdev_t.h |
Extract the minor number from dev_t |
printk("%d", MINOR(devnum)); |
register_chrdev_region() — Static Allocation API
To statically allocate a character device number you call register_chrdev_region(). The prototype is declared in <linux/fs.h>:
int register_chrdev_region(dev_t first, unsigned int count, const char *name);
| Parameter | Type | Meaning |
|---|---|---|
first |
dev_t |
The starting device number (built with MKDEV) |
count |
unsigned int |
How many consecutive minor numbers to reserve |
name |
const char * |
Name that appears in /proc/devices |
| Return value | int |
0 on success, negative errno on failure |
0 (which is falsy in C). The driver code therefore checks if(!register_chrdev_region(...)) — not if(register_chrdev_region(...)). This is a common source of confusion for beginners.Complete Static Allocation Driver — Source Code Walkthrough
Below is the complete static_alloc.c driver used in this tutorial. Read through it carefully — every line is explained in the sections that follow.
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kdev_t.h> /* MKDEV, MAJOR, MINOR */
#include <linux/fs.h> /* register_chrdev_region, unregister_chrdev_region */
/* ---- Module parameters (can be overridden at insmod time) ---- */
int major_number = 120;
int minor_number = 0;
char *device_name = "chardev";
int count = 1;
dev_t devnum; /* will hold the combined device number */
module_param(major_number, int, 0);
module_param(minor_number, int, 0);
module_param(count, int, 0);
module_param(device_name, charp, 0);
MODULE_LICENSE("GPL");
static int hello_init(void)
{
/* Step 1: Build the dev_t from major + minor */
devnum = MKDEV(major_number, minor_number);
printk("Major Number : %d\n", MAJOR(devnum));
printk("Minor number : %d\n", MINOR(devnum));
printk("count : %d\n", count);
printk("Device Name : %s\n", device_name);
/* Step 2: Try to register the range with the kernel */
if (!register_chrdev_region(devnum, count, device_name))
printk("device num registered\n");
else
printk("device number reg failed\n");
return 0;
}
static void hello_exit(void)
{
/* Step 3: Always unregister on module removal */
unregister_chrdev_region(devnum, count);
}
module_init(hello_init);
module_exit(hello_exit);
Line-by-Line Explanation
| Code | Why It Is There |
|---|---|
| #include <linux/kdev_t.h> | Provides MKDEV, MAJOR, MINOR macros and the dev_t type |
| #include <linux/fs.h> | Provides register_chrdev_region() and unregister_chrdev_region() |
| dev_t devnum; | A global that stores the built device number so hello_exit() can pass the same value to unregister |
| module_param(…) | Exposes major_number, minor_number, count, device_name as insmod parameters so we can experiment without recompiling |
| devnum = MKDEV(major_number, minor_number); | Combines the two numbers into a single dev_t. The kernel stores them packed in one integer. |
| if(!register_chrdev_region(…)) | Returns 0 on success (falsy), so we negate with ! to enter the success branch |
| unregister_chrdev_region(devnum, count); | Releases the range when the module is removed. Forgetting this leaks kernel resources. |
Makefile for Building the Kernel Module
obj-m += static_alloc.o
KDIR = /lib/modules/$(shell uname -r)/build
PWD = $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
| Makefile Variable | Meaning |
|---|---|
| obj-m += static_alloc.o | Tells the Kbuild system to compile static_alloc.c as a loadable module (.ko) |
| KDIR | Path to the running kernel’s build directory (where kernel headers live) |
| make -C $(KDIR) M=$(PWD) modules | Changes into the kernel source tree and compiles modules found in our directory (PWD) |
Build the module by running make in your source directory. You will get static_alloc.ko as output.
Understanding /proc/devices
Before loading your module, always check which major numbers are already in use on your system:
cat /proc/devices
A typical output looks like this:
Character devices:
1 mem
4 /dev/vc/0
4 tty
5 /dev/tty
5 /dev/console
10 misc
13 input
180 usb
...
The number on the left is the major number. The name on the right is the device name passed to register_chrdev_region(). Notice that major number 10 is taken by misc and major number 180 is taken by usb. These facts are central to the experiments below.
What Happens When You Call register_chrdev_region()
| 1 | Your driver calls MKDEV(major, minor) Packs the chosen major and minor numbers into a single dev_t integer. |
| ↓ | |
| 2 | Your driver calls register_chrdev_region(devnum, count, name) Asks the kernel: “Please reserve major_number for count minor numbers under the name name.” |
| ↓ | |
| 3 | Kernel checks its internal chrdev table If the major number is free and within the allowed range (≤ 511), registration succeeds and the device appears in /proc/devices. |
| ↓ | |
| ✗ | If major is already taken OR exceeds 511 → returns error Your driver must handle this and log an appropriate printk message. The device number is not reserved. |
Experiment 1 — What Happens When You Reuse an Existing Major Number?
Major number 10 is already used by the misc driver in Linux. What happens if our module also tries to claim major 10?
sudo insmod ./static_alloc.ko major_number=10
[ 8601.044048] Major Number : 10
[ 8601.044050] Minor number : 0
[ 8601.044050] count : 1
[ 8601.044051] Device Name : chardev
[ 8601.044052] device number reg failed
misc driver. The kernel maintains a global table of registered character device ranges and will not allow two drivers to claim the same major number.This is the core problem with static allocation — you must know in advance which major numbers are free on the target system. A number that is free on your development machine may be taken on the production board. This is exactly why dynamic allocation (alloc_chrdev_region()) was introduced and is preferred for all production drivers.
Experiment 2 — What Happens When You Reuse an Existing Device Name?
Major number 180 is used by the usb driver. Now let us try: use a different major number (311) but give our device the same name as an existing one (“usb”). Will the kernel allow it?
sudo insmod ./static_alloc.ko major_number=311 device_name=usb
[11785.653156] Major Number : 311
[11785.653162] Minor number : 0
[11785.653164] count : 1
[11785.653165] Device Name : usb
[11785.653168] device num registered
/proc/devices as long as they have different major numbers. The name is purely informational — it is what you see in /proc/devices and has no enforcement meaning.180 usb ← original USB driver
311 usb ← our new module (same name, different major)
/proc/devices are only for human readability. Do not rely on name uniqueness.Experiment 3 — Registering Multiple Minor Numbers at Once
The count parameter of register_chrdev_region() lets you reserve a range of consecutive minor numbers in a single call. Here we register 5 minor numbers starting from minor 0 under major 327.
sudo insmod ./static_alloc.ko major_number=327 minor_number=0 count=5 device_name=ept
[12314.660126] Major Number : 327
[12314.660133] Minor number : 0
[12314.660134] count : 5
[12314.660134] Device Name : ept
[12314.660137] device num registered
ept driver. This is useful when a single driver manages multiple device instances — for example, a serial driver managing /dev/ttyEP0 through /dev/ttyEP4.| Major | Minor 0 | Minor 1 | Minor 2 | Minor 3 | Minor 4 |
|---|---|---|---|---|---|
| 327 | ✔ ept | ✔ ept | ✔ ept | ✔ ept | ✔ ept |
Maximum Minor Number — How Many Can You Have?
Recall from the dev_t bit layout that the minor number field is 20 bits wide. This gives a maximum of:
In practice you will never need anywhere near this many minor numbers for a single driver. A driver managing 256 UART ports would use minors 0 through 255 — well within the limit. However if you accidentally pass a count or a starting minor_number that causes the range to exceed 2²⁰, you will observe an overflow: the upper bits of the minor number wrap around, producing an incorrect dev_t.
minor_number + count > 2²⁰, the resulting device numbers are invalid. Always validate your range before calling register_chrdev_region().Maximum Major Number — The CHRDEV_MAJOR_MAX Limit
The major number field in dev_t is 12 bits wide, which theoretically allows values from 0 to 4095 (2¹²). However, the kernel artificially limits this to a lower value defined in include/linux/fs.h:
#define CHRDEV_MAJOR_MAX 512
This means the largest usable major number is 511. If you ask for anything from 512 upward the kernel rejects the request regardless of whether that number appears free.
Experiment 4 — Trying Major Number 4095 (Maximum Theoretical)
sudo insmod ./static_alloc.ko major_number=4095
[13087.802651] Major Number : 4095
[13087.802656] Minor number : 0
[13087.802657] count : 1
[13087.802658] Device Name : chardev
[13087.802660] CHRDEV "chardev" major requested (4095) is greater than the maximum (511)
[13087.802664] device number reg failed
dev_t, the kernel refuses to honour it because CHRDEV_MAJOR_MAX is set to 512, making 511 the effective ceiling.| Field | Bit Width | Theoretical Max | Kernel-Enforced Max |
|---|---|---|---|
| Major Number | 12 bits | 4095 | 511 (CHRDEV_MAJOR_MAX − 1) |
| Minor Number | 20 bits | 1,048,575 | 1,048,575 (no artificial cap) |
Cleaning Up — unregister_chrdev_region()
Every successful call to register_chrdev_region() must be paired with a call to unregister_chrdev_region() when the module exits. Failure to do so will leave the device number permanently reserved until the next reboot, potentially preventing any other driver from claiming that major number.
void unregister_chrdev_region(dev_t first, unsigned int count);
Pass the same dev_t and count values that were used during registration. This is why devnum is declared as a global variable — so both hello_init() and hello_exit() can access the same value.
unregister_chrdev_region() does not check whether the range was actually registered. Calling it with the wrong arguments silently does nothing or, worse, releases a range belonging to another driver. Always pass the same values used at registration.Static Allocation vs Dynamic Allocation — Comparison
Now that you understand how static allocation works and its failure modes, you are ready to appreciate why dynamic allocation exists. Here is a side-by-side comparison:
| Aspect | Static (register_chrdev_region) | Dynamic (alloc_chrdev_region) |
|---|---|---|
| Major number chosen by | You (the driver developer) | Kernel (picks a free one automatically) |
| Risk of conflict | High — chosen number may be taken | None — kernel guarantees uniqueness |
| Portability | Poor — different systems have different free numbers | Good — works on any Linux system |
| When to use | Learning, debugging, controlled lab environments | Production drivers, upstream submissions |
| Predictability | Major number is fixed and known in advance | Major number is unknown until runtime |
| Kernel function | register_chrdev_region() |
alloc_chrdev_region() |
Common Mistakes and Troubleshooting
| Mistake | What Happens | Fix |
|---|---|---|
Using if(register_chrdev_region(...)) instead of if(!...) |
Success is reported as failure because return 0 is falsy | Always negate with ! or compare == 0 |
| Choosing a major number already in use | register_chrdev_region() returns an error |
Check cat /proc/devices first; use dynamic allocation |
| Choosing major > 511 | Kernel prints “greater than the maximum (511)” | Keep major ≤ 511 |
Forgetting to call unregister_chrdev_region() in exit |
Device number leaks; stays reserved until reboot | Always add it to module_exit function |
Not including <linux/fs.h> |
Compile error: implicit declaration of register_chrdev_region |
Add #include <linux/fs.h> |
Storing devnum as a local variable in init |
hello_exit() cannot access it — undefined behaviour |
Declare devnum as a global or module-level static |
Best Practices for Character Device Number Registration
- Always check the return value of
register_chrdev_region()and return an error code frommodule_init()on failure — do not proceed with a partially-initialized driver. - Use
alloc_chrdev_region()for all production drivers. Static allocation is only acceptable for learning exercises or highly controlled embedded platforms where the major number assignment is part of a fixed BSP. - Declare
devnumas a module-level global so the same value is accessible in both the init and exit functions. - Run
cat /proc/devicesbefore picking a static major number to avoid conflicts during development. - Guard against count overflow — verify that
minor_number + count ≤ 2²⁰before calling the registration function. - Match your
MODULE_LICENSE()declaration to the license of your code. UsingGPLgives access to more exported kernel symbols.
Key Takeaways
Conclusion
In this free Linux kernel driver tutorial you have seen static device number allocation from every angle: the structure of dev_t, how the kernel macros MKDEV, MAJOR, and MINOR work, and how to use register_chrdev_region() and unregister_chrdev_region() correctly. More importantly, you ran four real-hardware experiments that exposed the failure modes — a duplicate major number, a shared name, a range of minors, and a major exceeding the enforced limit of 511.
These experiments teach you the most important lesson: static allocation is fragile because it depends on a globally agreed assignment that must be coordinated across all drivers on a system. The Linux kernel community addressed this with alloc_chrdev_region(), which lets the kernel pick a free major number automatically. In the next lecture we will implement exactly that and complete the full character device registration flow.
Suggested Images for This Article
- dev_t bit-layout diagram ALT: Linux dev_t structure showing major and minor number bit layout in static device number allocation
- register_chrdev_region() call flow screenshot ALT: Linux kernel register_chrdev_region static allocation function flow in char device driver
- /proc/devices terminal screenshot showing major numbers ALT: /proc/devices output showing registered char device major numbers in Linux kernel driver tutorial
- dmesg output showing device number registration success and failure ALT: dmesg output for Linux character device static allocation success and failure in kernel module
- Static vs Dynamic allocation comparison diagram ALT: Static vs dynamic device number allocation comparison in Linux kernel character device driver
Frequently Asked Questions (FAQ)
Q1. What is static device number allocation in Linux?
Static device number allocation means the driver developer manually chooses a major and minor number and asks the kernel to reserve that specific number using register_chrdev_region(). Unlike dynamic allocation, the kernel does not pick the number — you do. This is the simplest way to learn how device numbers work but is not recommended for production drivers because the chosen number may already be in use on the target system.
Q2. What is the difference between a major number and a minor number?
The major number identifies the driver. All devices controlled by the same driver share one major number. The minor number identifies a specific device instance managed by that driver. For example, a single UART driver might use major 4 and minor numbers 64, 65, 66 for /dev/ttyS0, /dev/ttyS1, /dev/ttyS2.
Q3. Why does register_chrdev_region() return 0 on success?
Following the POSIX and Linux kernel convention, most kernel functions return 0 to mean success and a negative errno value to mean failure. So register_chrdev_region() returning 0 means everything went fine. This is why the example code checks if(!register_chrdev_region(...)) — the ! inverts the logic so the true branch runs on success.
Q4. What is the maximum major number I can use in Linux?
The dev_t type stores 12 bits for the major number, giving a theoretical range of 0–4095. However, the kernel enforces a hard limit defined as CHRDEV_MAJOR_MAX 512 in include/linux/fs.h, meaning the highest usable major number is 511. Requesting 512 or above causes register_chrdev_region() to fail with a kernel log message saying the requested major is greater than the maximum (511).
Q5. How many minor numbers can a single driver have?
The minor number field in dev_t is 20 bits wide, allowing up to 2²⁰ = 1,048,576 distinct minor numbers per major number. In practice a driver rarely uses more than a few hundred. Pass the desired count to the count parameter of register_chrdev_region() to reserve a contiguous range.
Q6. Can two drivers have the same name in /proc/devices?
Yes. The kernel does not enforce name uniqueness. Two completely different drivers can appear under the same name in /proc/devices as long as they have different major numbers. This was demonstrated in Experiment 2 where a new driver was registered under the name “usb” alongside the existing USB driver. Names in /proc/devices are purely informational.
Q7. What happens if I forget to call unregister_chrdev_region()?
The device number remains registered in the kernel’s internal table for the lifetime of the running kernel instance (until the next reboot). No other driver can claim that major number while it is stuck registered. This is a resource leak. For production drivers this is treated as a serious bug. Always pair every successful registration with a matching unregistration in the module exit function.
Q8. When should I use static allocation vs dynamic allocation?
Use static allocation only for learning exercises or for highly controlled embedded platforms where the major number assignments are fixed by the BSP and you can guarantee no conflicts. Use dynamic allocation (alloc_chrdev_region()) for all production drivers and for any driver intended to be submitted upstream to the Linux kernel. The Linux kernel coding style guidelines and driver review process strongly prefer dynamic allocation.
Q9. Where is dev_t defined and what type is it?
dev_t is a 32-bit unsigned integer type defined in the Linux kernel headers. It is accessible by including <linux/types.h>, which is pulled in transitively by <linux/kdev_t.h>. It stores the major number in the upper 12 bits and the minor number in the lower 20 bits. Always use the MKDEV, MAJOR, and MINOR macros — never manipulate the bits directly.
Q10. How do I verify my device number was registered successfully?
After loading the module with sudo insmod ./static_alloc.ko, run cat /proc/devices and look for your major number and device name. You can also run dmesg | tail -10 to see the printk messages from your module’s init function which will confirm success or print the failure reason.
Authoritative References
Continue Learning — Free Linux Kernel Driver Course
EmbeddedPathashala offers completely free Linux device driver and kernel development courses for engineering students and working professionals.
Visit EmbeddedPathashala Next: Dynamic Allocation →
