In Linux, everything is treated as a file — even hardware devices like keyboards, mice, and USB dongles. Hardware devices are accessed by the user through special device files.
These files are grouped under the /dev directory. System calls like open, read, write, close, lseek, and mmap are redirected by the operating system to the device driver associated with the physical device.
In Unix/Linux there are two major categories of device drivers. The division is based on speed, data volume, and how data is transferred between device and system.
🔤 Character Device Drivers
Slow devices managing small amounts of data. Access does not require frequent seeks. Data flows as a byte stream — character by character.
💾 Block Device Drivers
Large data volume devices. Data is organised in blocks. Communication is mediated by the file management subsystem and block device subsystem.
raviteja@raviteja-Inspiron-15-3511:~$ ls -l /dev/ | grep "^b" brw-rw---- 1 root disk 7, 0 May 16 12:48 loop0 brw-rw---- 1 root disk 7, 1 May 16 12:48 loop1 brw-rw---- 1 root disk 7, 10 May 16 12:48 loop10 brw-rw---- 1 root disk 7, 11 May 16 12:48 loop11 brw-rw---- 1 root disk 7, 12 May 16 12:48 loop12 brw-rw---- 1 root disk 7, 13 May 16 12:48 loop13 brw-rw---- 1 root disk 7, 14 May 16 12:48 loop14 brw-rw---- 1 root disk 7, 15 May 16 12:48 loop15
c = character device | b = block device | d = directoryThere are three essential steps to register a character device driver with the Linux kernel:
1
dev_t2
struct cdev and struct file_operations3
cdev_add()The connection between an application and a device file is based on the name of the device file. However, the connection between the device file and the device driver is based on the device number — not the name.
📌 Major Number
Identifies the device type (e.g. USB, serial). It identifies which driver handles this device.
📌 Minor Number
Identifies a specific physical device within those served by the same driver.
Static Assignment
Choose a number that does not appear to be in use already. Only useful when you know the major number in advance.
Dynamic Assignment Preferred
The kernel finds and assigns a free major number for you. Preferred to avoid conflicts with other device drivers.
raviteja@raviteja-Inspiron-15-3511:~/linux_kernel_source/linux/Documentation/admin-guide$ vi devices.txt 0 Unnamed devices (e.g. non-device mounts) 0 = reserved as null device number 1 char Memory devices 1 = /dev/mem Physical memory access 2 = /dev/kmem OBSOLETE - replaced by /proc/kcore 3 = /dev/null Null device 4 = /dev/port I/O port access 5 = /dev/zero Null byte source 6 = /dev/core OBSOLETE - replaced by /proc/kcore 7 = /dev/full Returns ENOSPC on write 8 = /dev/random Nondeterministic random number gen. 9 = /dev/urandom Faster, less secure random number gen. 10 = /dev/aio Asynchronous I/O notification interface 11 = /dev/kmsg Writes to this come out as printk's 12 = /dev/oldmem OBSOLETE - replaced by /proc/vmcore 1 block RAM disk 0 = /dev/ram0 First RAM disk 1 = /dev/ram1 Second RAM disk
dev_t Data Type┌────────────────────────────┬──────────────────────────────────────────┐
│ 12 bits │ 20 bits │
├────────────────────────────┼──────────────────────────────────────────┤
│ MAJOR NUMBER │ MINOR NUMBER │
└────────────────────────────┴──────────────────────────────────────────┘
32-bit dev_t
MAJOR(dev_t dev); MINOR(dev_t dev); MKDEV(int major, int minor);
- MAJOR(dev) — extract the major number from a
dev_t - MINOR(dev) — extract the minor number from a
dev_t - MKDEV(major, minor) — construct a
dev_tfrom major and minor numbers
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
MODULE_LICENSE("GPL");
static int hello_init(void)
{
printk("driver 1 init \n");
dev_t devicenum = 0;
printk("Major Number : %d\n", MAJOR(devicenum));
printk("Minor Number : %d\n", MINOR(devicenum));
devicenum = MKDEV(120, 30);
printk("Device Number : %u\n", devicenum);
printk("Major Number : %d\n", MAJOR(devicenum));
printk("Minor Number : %d\n", MINOR(devicenum));
return 0;
}
static void hello_exit(void)
{
printk("1 driver exit\n");
}
module_init(hello_init);
module_exit(hello_exit);
obj-m += 1_ex.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
# Monitor kernel messages in another terminal: dmesg -w & # Load the module: raviteja@raviteja-Inspiron-15-3511:~/advance_lsyspg/char_dev_drivers$ insmod ./1_ex.ko [ 1885.711252] 1_ex: loading out-of-tree module taints kernel. [ 1885.711261] 1_ex: module verification failed: signature and/or required key missing - tainting kernel [ 1885.712416] driver 1 init [ 1885.712420] Major Number : 0 [ 1885.712422] Minor Number : 0 [ 1885.712423] Device Number : 125829150 [ 1885.712425] Major Number : 120 [ 1885.712427] Minor Number : 30
devicenum = 0, both MAJOR and MINOR return 0. After MKDEV(120, 30), the encoded 32-bit value is 125829150 and MAJOR/MINOR correctly extract 120 and 30 respectively.The file /proc/devices displays all currently configured character and block devices. Output includes the major number and name of each device, split into two sections.
raviteja@raviteja-Inspiron-15-3511:~$ cat /proc/devices Character devices: 1 mem 4 /dev/vc/0 4 tty 4 ttyS 5 /dev/tty 5 /dev/console 5 /dev/ptmx 5 ttyprintk ...
There are two ways to allocate device numbers: static and dynamic.
| Method | How It Works | When to Use |
|---|---|---|
| Static | You tell the kernel exactly which major/minor numbers you want. Kernel grants them if available. | Only when you know the exact major number in advance. |
| Dynamic Preferred | You tell the kernel how many numbers you need; it finds and returns a free major number. | Preferred — avoids conflicts with other drivers. |
int register_chrdev_region(dev_t from, unsigned int count, const char *name);
Description: Register a range of device numbers.
Arguments:
from— first device number in the desired range (must include the major number)count— number of consecutive device numbers requiredname— name of the device or driver (appears in/proc/devices)
Returns: 0 on success, negative error code on failure.
void unregister_chrdev_region(dev_t from, unsigned int count);
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
raviteja@raviteja-Inspiron-15-3511:~/advance_lsyspg/char_dev_drivers/day2$ make make -C /lib/modules/6.8.0-111-generic/build \ M=/home/raviteja/advance_lsyspg/char_dev_drivers/day2 modules make[1]: Entering directory '/usr/src/linux-headers-6.8.0-111-generic' warning: the compiler differs from the one used to build the kernel The kernel was built by: x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04.3) 12.3.0 You are using: gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04.3) 12.3.0 CC [M] /home/raviteja/advance_lsyspg/char_dev_drivers/day2/static_alloc.o MODPOST /home/raviteja/advance_lsyspg/char_dev_drivers/day2/Module.symvers CC [M] /home/raviteja/advance_lsyspg/char_dev_drivers/day2/static_alloc.mod.o LD [M] /home/raviteja/advance_lsyspg/char_dev_drivers/day2/static_alloc.ko BTF [M] /home/raviteja/advance_lsyspg/char_dev_drivers/day2/static_alloc.ko Skipping BTF generation for static_alloc.ko due to unavailability of vmlinux make[1]: Leaving directory '/usr/src/linux-headers-6.8.0-111-generic'
raviteja@raviteja-Inspiron-15-3511:~/advance_lsyspg/char_dev_drivers/day2$ sudo insmod ./static_alloc.ko [ 1992.778147] Major Number : 120 [ 1992.778148] Minor Number : 0 [ 1992.778149] Count : 1 [ 1992.778150] Device num registered [ 2153.975511] Major Number : 120 [ 2153.975515] Minor Number : 0 [ 2153.975515] Count : 1 [ 2153.975516] Device Name : chardev [ 2153.975517] Device num registered
# After insmod: raviteja@raviteja-Inspiron-15-3511:~$ cat /proc/devices ... 120 chardev ... # After rmmod — entry disappears: raviteja@raviteja-Inspiron-15-3511:~$ sudo rmmod static_alloc.ko
raviteja@raviteja-Inspiron-15-3511:~/advance_lsyspg/char_dev_drivers/day2$ modinfo static_alloc.ko filename: /home/raviteja/advance_lsyspg/char_dev_drivers/day2/static_alloc.ko license: GPL srcversion: FFCAC28DA12BE03688AAB61 depends: retpoline: Y name: static_alloc vermagic: 6.8.0-111-generic SMP preempt mod_unload modversions parm: major_number:int parm: minor_number:int parm: count:int parm: device_name:charp
# Load with custom module parameters raviteja@raviteja-Inspiron-15-3511:~/advance_lsyspg/char_dev_drivers/day2$ \ sudo insmod ./static_alloc.ko major_number=123 minor_number=27 device_name=EPATH [ 2584.164914] Major Number : 123 [ 2584.164918] Minor Number : 27 [ 2584.164918] Count : 1 [ 2584.164919] Device Name : EPATH [ 2584.164920] Device num registered # Verify registered device raviteja@raviteja-Inspiron-15-3511:~$ cat /proc/devices | less ... 123 EPATH ...
✓
/dev. System calls are routed to device drivers.✓
✓
dev_t = 12-bit major + 20-bit minor. Use MAJOR(), MINOR(), MKDEV() macros from <linux/kdev_t.h>.✓
register_chrdev_region(). Dynamic is preferred to avoid conflicts.✓
unregister_chrdev_region() in the module exit function.✓
cat /proc/devices. Module parameters allow passing major/minor at load time.struct cdev, file_operations, and cdev_add()