Character Device Drivers tutorial – what are device drivers?

 

 

Character Device Drivers tutorial – what are device drivers?
Linux Kernel Programming — Lecture 1: Device Driver Fundamentals
Lec 1
First Lecture
Char
Driver Type
dev_t
Key Data Type
2
Code Examples

🔑 Key Concepts
Character Device Block Device dev_t Major Number Minor Number MKDEV / MAJOR / MINOR register_chrdev_region /dev directory /proc/devices insmod / rmmod struct cdev file_operations

📁 1. Linux: Everything is a File

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.

⚙️ 2. Two Categories of Device Files

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.

Keyboard Mouse Serial Ports Sound Card Joystick

💾 Block Device Drivers

Large data volume devices. Data is organised in blocks. Communication is mediated by the file management subsystem and block device subsystem.

Hard Drives CD-ROMs RAM Disks
Key Difference: For character devices, system calls go directly to the device driver. For block devices, calls go through the file system → intermediate layer → block device driver.
Listing Device Files
List all character device files
raviteja@raviteja-Inspiron-15-3511:~$ ls -l /dev/ | grep “^c” crw——- 1 root root 10, 261 May 16 12:48 acpi_thermal_rel crw-r–r– 1 root root 10, 235 May 16 12:48 autofs crw——- 1 root root 10, 234 May 16 12:48 btrfs-control crw–w—- 1 root tty 5, 1 May 16 12:48 console crw——- 1 root root 10, 260 May 16 12:48 cpu_dma_latency crw——- 1 root root 10, 203 May 16 12:48 cuse
List all block device files
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
File type prefix legend:  c = character device  |  b = block device  |  d = directory

🛠️ 3. How to Create a Character Device Driver

There are three essential steps to register a character device driver with the Linux kernel:

1

Allocate a Device Number — statically or dynamically using dev_t

2

Initialise the Character Device — with its file operations using struct cdev and struct file_operations

3

Register with the Kernel — using cdev_add()

🔢 4. Device Numbers — Major & Minor

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.

ttyS* devices — same major number (4), different minor numbers
crw-rw—- 1 root dialout 4, 71 May 17 12:35 /dev/ttyS7 crw-rw—- 1 root dialout 4, 72 May 17 12:35 /dev/ttyS8 crw-rw—- 1 root dialout 4, 73 May 17 12:35 /dev/ttyS9
Choosing a Device Number

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.

Statically assigned major numbers — from kernel documentation
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

📐 5. The dev_t Data Type
dev_t structure — 32 bits total
┌────────────────────────────┬──────────────────────────────────────────┐
│          12 bits           │                 20 bits                  │
├────────────────────────────┼──────────────────────────────────────────┤
│        MAJOR NUMBER        │               MINOR NUMBER               │
└────────────────────────────┴──────────────────────────────────────────┘
                           32-bit dev_t
📦 Macros — Header: <linux/kdev_t.h>

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_t from major and minor numbers
Example 1 — Using dev_t, MAJOR, MINOR, MKDEV 1_ex.c
1_ex.c — Source Code
#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);
Makefile
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
Build & Load — Terminal output
# 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
Note: When 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.

📋 6. /proc/devices

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.

cat /proc/devices — sample output
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
  ...

📌 7. Allocating Major & Minor Numbers

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.

Static Assignment API
register_chrdev_region — Header: <linux/fs.h>

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 required
  • name — name of the device or driver (appears in /proc/devices)

Returns: 0 on success, negative error code on failure.

unregister_chrdev_region — Header: <linux/fs.h>

void unregister_chrdev_region(dev_t from, unsigned int count);

Call this in your module’s exit function to release the reserved device numbers.

Example 2 — Static Device Number Allocation static_alloc.c
Makefile for static_alloc
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
make — Build output
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'
insmod & dmesg output
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
Verify in /proc/devices
# 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

Module Info & Module Parameters
modinfo 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
Loading with custom module parameters
# 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
...

✅ Lecture Summary

Linux treats all hardware as files under /dev. System calls are routed to device drivers.

Two driver categories: Character (byte stream, slow) and Block (bulk data, with filesystem mediation).

dev_t = 12-bit major + 20-bit minor. Use MAJOR(), MINOR(), MKDEV() macros from <linux/kdev_t.h>.

Static allocation: register_chrdev_region(). Dynamic is preferred to avoid conflicts.

Always call unregister_chrdev_region() in the module exit function.

Verify registered devices with cat /proc/devices. Module parameters allow passing major/minor at load time.
Topics covered: /dev directory char vs block drivers dev_t MKDEV / MAJOR / MINOR register_chrdev_region /proc/devices insmod / rmmod modinfo / module params

Next Up →
Dynamic device number allocation, struct cdev, file_operations, and cdev_add()

Lecture 2 Back to Index

Leave a Reply

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