Static Device Number Allocation in Linux Character Device Drivers

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

Free
Linux Driver Course
Beginner
Friendly Tutorial
Hands-On
Live Kernel Examples

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(), and MINOR() 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_exit kernel 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

dev_t (32-bit) — Bit Layout
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>:

Function Prototype
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
⚠️ Important: On success the function returns 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.

static_alloc.c — Complete Source
#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

Makefile
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:

Command: View registered devices
cat /proc/devices

A typical output looks like this:

Sample /proc/devices output (character devices section)
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()

Static Allocation Flow — Step by Step
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?

Load the module with major_number=10
sudo insmod ./static_alloc.ko major_number=10
dmesg output
[ 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
🔴 Result: Registration FAILED The kernel rejected the request because major number 10 is already owned by the 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?

Load with major_number=311, device_name=usb
sudo insmod ./static_alloc.ko major_number=311 device_name=usb
dmesg output
[11785.653156] Major Number : 311
[11785.653162] Minor number : 0
[11785.653164] count : 1
[11785.653165] Device Name : usb
[11785.653168] device num registered
🟢 Result: Registration SUCCEEDED The kernel does not treat the name as a unique key. Two drivers can share the same name in /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.
/proc/devices after loading (excerpt)
180 usb      ← original USB driver
311 usb      ← our new module (same name, different major)
💡 Key Insight: The kernel identifies drivers by major number, not by name. Names in /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.

Load with count=5
sudo insmod ./static_alloc.ko major_number=327 minor_number=0 count=5 device_name=ept
dmesg output
[12314.660126] Major Number : 327
[12314.660133] Minor number : 0
[12314.660134] count : 5
[12314.660134] Device Name : ept
[12314.660137] device num registered
🟢 Result: Succeeded Minor numbers 0, 1, 2, 3, and 4 under major 327 are now reserved for the ept driver. This is useful when a single driver manages multiple device instances — for example, a serial driver managing /dev/ttyEP0 through /dev/ttyEP4.
What the kernel now holds for major 327
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:

2²⁰ = 1,048,576 minor numbers Valid range: 0 to 1,048,575

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.

⚠️ Overflow Risk: If 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:

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)

Load with major_number=4095
sudo insmod ./static_alloc.ko major_number=4095
dmesg output
[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
🔴 Result: FAILED The kernel message is explicit: “major requested (4095) is greater than the maximum (511)”. Even though 4095 fits inside the 12-bit field of 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.

Prototype
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.

💡 Note: 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 from module_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 devnum as a module-level global so the same value is accessible in both the init and exit functions.
  • Run cat /proc/devices before 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. Using GPL gives access to more exported kernel symbols.

Key Takeaways

dev_t = 32-bit packed number Major = 12 bits (max 511) Minor = 20 bits (max 2²⁰−1) MKDEV() packs them MAJOR() / MINOR() extract them register_chrdev_region() = static Returns 0 on success Duplicate major → failure Duplicate name → allowed Always unregister on exit Use dynamic alloc in production

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

  1. dev_t bit-layout diagram ALT: Linux dev_t structure showing major and minor number bit layout in static device number allocation
  2. register_chrdev_region() call flow screenshot ALT: Linux kernel register_chrdev_region static allocation function flow in char device driver
  3. /proc/devices terminal screenshot showing major numbers ALT: /proc/devices output showing registered char device major numbers in Linux kernel driver tutorial
  4. dmesg output showing device number registration success and failure ALT: dmesg output for Linux character device static allocation success and failure in kernel module
  5. 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 →

 

Leave a Reply

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