Chapter 2: Character Device Drivers
Table of Contents
- Introduction to Character Devices
- Device Numbers: Major and Minor
- The cdev Structure
- Registering Character Devices
- Basic File Operations
- Complete Character Driver Example
- Device Classes and udev
- Multiple Devices
Introduction to Character Devices
What are Character Devices?
Character devices are accessed as a stream of bytes, one at a time (like a serial port or terminal). Contrast with block devices (like disks) that are accessed in fixed-size blocks.
Types of Devices in Linux
Device Types:
├── Character Devices (char)
│ ├── Serial ports (/dev/ttyS*)
│ ├── Terminals (/dev/tty*)
│ ├── Input devices (/dev/input/*)
│ ├── Sound devices (/dev/snd/*)
│ └── Custom devices
│
├── Block Devices (block)
│ ├── Hard disks (/dev/sda*)
│ ├── Optical drives (/dev/sr*)
│ └── Loop devices (/dev/loop*)
│
└── Network Devices (net)
├── Ethernet (eth*, ens*)
├── Wireless (wlan*)
└── Loopback (lo)
Character Device Architecture
┌─────────────────────────┐
│ User Application │
│ (open, read, write) │
└───────────┬─────────────┘
│ System Call
↓
┌─────────────────────────┐
│ Virtual File System │
│ (VFS Layer) │
└───────────┬─────────────┘
│
↓
┌─────────────────────────┐
│ Character Device │
│ Driver (cdev) │
│ ┌─────────────────┐ │
│ │ file_operations │ │
│ │ - open() │ │
│ │ - read() │ │
│ │ - write() │ │
│ │ - release() │ │
│ └─────────────────┘ │
└───────────┬─────────────┘
│
↓
┌─────────────────────────┐
│ Hardware │
└─────────────────────────┘
Device Numbers: Major and Minor
Theory
Every device in Linux is identified by:
- Major Number: Identifies the driver (e.g., all SCSI disks share major 8)
- Minor Number: Identifies specific device instance
Device Number = (Major << 20) | Minor
Example: /dev/sda1
Major: 8 (SCSI disk driver)
Minor: 1 (first partition of first disk)
Device Number Types
#include <linux/kdev_t.h> /* For device number macros */
#include <linux/fs.h> /* For register_chrdev_region */
/*
* dev_t - Device number type (32-bit)
* - Upper 12 bits: Major number (0-4095)
* - Lower 20 bits: Minor number (0-1048575)
*/
dev_t dev_num;
/* Extract major and minor from dev_t */
unsigned int major = MAJOR(dev_num);
unsigned int minor = MINOR(dev_num);
/* Create dev_t from major and minor */
dev_t dev = MKDEV(major, minor);
Allocating Device Numbers
Three methods to obtain device numbers:
1. Static Allocation (Fixed Major)
/*
* Use when you want a specific major number
*
* Not recommended: Can conflict with other drivers
* Only use for testing or proprietary hardware
*/
int register_chrdev_region(dev_t first, /* Starting device number */
unsigned count, /* Number of consecutive minors */
const char *name) /* Device name (appears in /proc/devices) */
{
dev_t dev;
int ret;
int major = 240; /* Choose unused major (check /proc/devices) */
dev = MKDEV(major, 0);
ret = register_chrdev_region(dev, 1, "mydevice");
if (ret < 0) {
pr_err("Failed to register device number\n");
return ret;
}
pr_info("Registered device with major %d\n", major);
return 0;
}
2. Dynamic Allocation (Recommended)
/*
* Kernel automatically assigns an unused major number
*
* Recommended: Avoids conflicts, portable across systems
*/
int alloc_chrdev_region(dev_t *dev, /* Output: Assigned device number */
unsigned baseminor, /* First minor (usually 0) */
unsigned count, /* Number of consecutive minors */
const char *name) /* Device name */
{
dev_t dev_num;
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, "mydevice");
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
pr_info("Allocated device: major=%d, minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
return 0;
}
3. Old API (Deprecated)
/*
* Legacy API - Do not use in new code
*
* Combines device number registration and cdev registration
*/
int register_chrdev(unsigned int major, /* 0 for dynamic allocation */
const char *name,
const struct file_operations *fops);
Releasing Device Numbers
/*
* Always release device numbers in cleanup function
*/
void unregister_chrdev_region(dev_t first, unsigned count);
/* Example */
static void __exit cleanup(void)
{
unregister_chrdev_region(dev_num, 1);
pr_info("Device number released\n");
}
The cdev Structure
Theory: cdev
The cdev structure represents a character device in the kernel:
struct cdev {
struct kobject kobj; /* Embedded kernel object */
struct module *owner; /* Module that owns this cdev */
const struct file_operations *ops; /* File operation methods */
struct list_head list; /* List of cdevs */
dev_t dev; /* Device number */
unsigned int count; /* Number of minor numbers */
};
Working with cdev
#include <linux/cdev.h>
/*
* Method 1: Static allocation
*/
static struct cdev my_cdev;
static int init_cdev_static(void)
{
/* Initialize cdev structure */
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
/* Add cdev to the system */
int ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
pr_err("Failed to add cdev\n");
return ret;
}
return 0;
}
static void cleanup_cdev_static(void)
{
/* Remove cdev from system */
cdev_del(&my_cdev);
}
/*
* Method 2: Dynamic allocation (Preferred)
*/
static struct cdev *my_cdev;
static int init_cdev_dynamic(void)
{
int ret;
/* Allocate cdev structure */
my_cdev = cdev_alloc();
if (!my_cdev) {
pr_err("Failed to allocate cdev\n");
return -ENOMEM;
}
/* Initialize fields */
my_cdev->ops = &fops;
my_cdev->owner = THIS_MODULE;
/* Add to system */
ret = cdev_add(my_cdev, dev_num, 1);
if (ret < 0) {
pr_err("Failed to add cdev\n");
kobject_put(&my_cdev->kobj); /* Free allocated cdev */
return ret;
}
return 0;
}
static void cleanup_cdev_dynamic(void)
{
/* cdev_del also frees dynamically allocated cdev */
cdev_del(my_cdev);
}
Registering Character Devices
Complete Registration Process
/*
* Character device registration involves three steps:
*
* 1. Allocate device number (major/minor)
* 2. Initialize and add cdev
* 3. Create device node (optional, for auto device file creation)
*/
static dev_t dev_num; /* Device number */
static struct cdev *my_cdev; /* Character device structure */
static int __init chardev_init(void)
{
int ret;
/*
* Step 1: Allocate device number
*/
ret = alloc_chrdev_region(&dev_num, 0, 1, "mychardev");
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
pr_info("Allocated device number: major=%d, minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
/*
* Step 2: Initialize cdev structure
*/
my_cdev = cdev_alloc();
if (!my_cdev) {
pr_err("Failed to allocate cdev\n");
ret = -ENOMEM;
goto err_alloc_cdev;
}
cdev_init(my_cdev, &fops);
my_cdev->owner = THIS_MODULE;
/*
* Step 3: Add cdev to kernel
*/
ret = cdev_add(my_cdev, dev_num, 1);
if (ret < 0) {
pr_err("Failed to add cdev\n");
goto err_cdev_add;
}
pr_info("Character device registered successfully\n");
return 0;
err_cdev_add:
kobject_put(&my_cdev->kobj);
err_alloc_cdev:
unregister_chrdev_region(dev_num, 1);
return ret;
}
static void __exit chardev_exit(void)
{
/* Cleanup in reverse order */
cdev_del(my_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("Character device unregistered\n");
}
Creating Device Nodes Manually
# After loading module, check assigned major number
cat /proc/devices | grep mychardev
# Create device node manually (requires root)
# mknod /dev/device_name c MAJOR MINOR
sudo mknod /dev/mychardev c 240 0
# Set permissions
sudo chmod 666 /dev/mychardev
# Test device
echo "Hello" > /dev/mychardev
cat /dev/mychardev
# Remove device node
sudo rm /dev/mychardev
Basic File Operations
The file_operations Structure
#include <linux/fs.h>
/*
* file_operations - Defines callbacks for device operations
*
* User space operations are translated to these callbacks:
* open() → .open
* read() → .read
* write() → .write
* close() → .release
* ioctl() → .unlocked_ioctl
*/
struct file_operations {
struct module *owner;
/* Position management */
loff_t (*llseek) (struct file *, loff_t, int);
/* Data transfer */
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
/* Async I/O */
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
/* Multiplexing */
__poll_t (*poll) (struct file *, struct poll_table_struct *);
/* Device control */
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
/* Memory mapping */
int (*mmap) (struct file *, struct vm_area_struct *);
/* Open and release */
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
/* Synchronization */
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
/* And many more... */
};
Implementing Basic Operations
/*
* Open operation - Called when device is opened
*
* @inode: Inode object (contains device number, permissions)
* @filp: File object (contains flags, position, private_data)
*
* Return: 0 on success, negative error code on failure
*/
static int my_open(struct inode *inode, struct file *filp)
{
pr_info("Device opened\n");
/*
* inode contains device information
*/
unsigned int major = imajor(inode);
unsigned int minor = iminor(inode);
pr_info("Device: major=%u, minor=%u\n", major, minor);
/*
* filp->f_flags contains open flags
* O_RDONLY, O_WRONLY, O_RDWR, O_NONBLOCK, etc.
*/
if ((filp->f_flags & O_ACCMODE) == O_RDONLY) {
pr_info("Opened read-only\n");
} else if ((filp->f_flags & O_ACCMODE) == O_WRONLY) {
pr_info("Opened write-only\n");
} else {
pr_info("Opened read-write\n");
}
/*
* filp->private_data can store device-specific data
* Useful for maintaining per-open-file state
*/
filp->private_data = NULL; /* Will demonstrate later */
/*
* Can refuse open based on conditions
*/
if (device_busy) {
return -EBUSY;
}
return 0; /* Success */
}
/*
* Release operation - Called when device is closed
*
* Called when last reference to file is closed
* (file descriptor closed and no other processes have it open)
*
* Return: 0 on success, negative error code on failure
*/
static int my_release(struct inode *inode, struct file *filp)
{
pr_info("Device closed\n");
/*
* Clean up any resources allocated in open
*/
if (filp->private_data) {
kfree(filp->private_data);
filp->private_data = NULL;
}
return 0;
}
/*
* Read operation - Called when userspace reads from device
*
* @filp: File object
* @buf: User space buffer (cannot directly access!)
* @count: Number of bytes to read
* @f_pos: Position in file (updated by driver)
*
* Return: Number of bytes read, or negative error code
*/
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
pr_info("Read: count=%zu, pos=%lld\n", count, *f_pos);
/*
* Example: Return error (no data available)
*/
return -EAGAIN; /* Try again later */
/*
* Example: Return EOF
*/
return 0;
/*
* Example: Copy data to userspace (shown in complete example)
*/
}
/*
* Write operation - Called when userspace writes to device
*
* @filp: File object
* @buf: User space buffer (cannot directly access!)
* @count: Number of bytes to write
* @f_pos: Position in file (updated by driver)
*
* Return: Number of bytes written, or negative error code
*/
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
pr_info("Write: count=%zu, pos=%lld\n", count, *f_pos);
/*
* Example: Refuse write (read-only device)
*/
return -EINVAL;
/*
* Example: Copy data from userspace (shown in complete example)
*/
}
/*
* Define file_operations structure
*/
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
User-Kernel Data Transfer
#include <linux/uaccess.h> /* For copy_to_user, copy_from_user */
/*
* copy_to_user - Copy data from kernel space to user space
*
* @to: User space buffer
* @from: Kernel space buffer
* @n: Number of bytes to copy
*
* Return: 0 on success, number of bytes NOT copied on failure
*/
/*
* copy_from_user - Copy data from user space to kernel space
*
* @to: Kernel space buffer
* @from: User space buffer
* @n: Number of bytes to copy
*
* Return: 0 on success, number of bytes NOT copied on failure
*/
/* Example: Safe read implementation */
static ssize_t safe_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
char kernel_buf[256];
size_t to_read;
/* Limit read to buffer size */
to_read = min(count, sizeof(kernel_buf));
/* Prepare data in kernel buffer */
snprintf(kernel_buf, sizeof(kernel_buf),
"Hello from kernel at position %lld\n", *f_pos);
/* Copy to userspace */
if (copy_to_user(buf, kernel_buf, to_read)) {
return -EFAULT; /* Bad address */
}
/* Update file position */
*f_pos += to_read;
return to_read; /* Return bytes read */
}
/* Example: Safe write implementation */
static ssize_t safe_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
char kernel_buf[256];
size_t to_write;
/* Limit write to buffer size */
to_write = min(count, sizeof(kernel_buf) - 1);
/* Copy from userspace */
if (copy_from_user(kernel_buf, buf, to_write)) {
return -EFAULT; /* Bad address */
}
/* Null-terminate */
kernel_buf[to_write] = '\0';
/* Process data */
pr_info("Received: %s\n", kernel_buf);
/* Update file position */
*f_pos += to_write;
return to_write; /* Return bytes written */
}
Complete Character Driver Example
simple_chardev.c - Full Working Driver
/*
* simple_chardev.c - Complete character device driver example
*
* Implements a simple character device that:
* - Stores data in a kernel buffer
* - Supports read/write operations
* - Handles open/close properly
* - Uses dynamic device number allocation
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define DEVICE_NAME "simple_char"
#define BUFFER_SIZE 1024
/* Device structure */
struct simple_char_dev {
struct cdev cdev; /* Character device structure */
char *buffer; /* Data buffer */
size_t buffer_size; /* Size of buffer */
size_t data_size; /* Amount of data in buffer */
struct mutex lock; /* Mutual exclusion lock */
};
static struct simple_char_dev *simple_dev;
static dev_t dev_num; /* Device number */
static int major; /* Major number */
/*
* Device open
*/
static int simple_open(struct inode *inode, struct file *filp)
{
struct simple_char_dev *dev;
pr_info("simple_char: Device opened\n");
/*
* container_of - Get pointer to struct containing cdev
*
* inode->i_cdev points to the cdev structure
* We need the containing simple_char_dev structure
*/
dev = container_of(inode->i_cdev, struct simple_char_dev, cdev);
/* Store in file's private_data for easy access */
filp->private_data = dev;
return 0;
}
/*
* Device release (close)
*/
static int simple_release(struct inode *inode, struct file *filp)
{
pr_info("simple_char: Device closed\n");
return 0;
}
/*
* Device read
*/
static ssize_t simple_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
struct simple_char_dev *dev = filp->private_data;
ssize_t retval = 0;
pr_info("simple_char: Read requested: count=%zu, pos=%lld\n",
count, *f_pos);
/* Lock device during access */
if (mutex_lock_interruptible(&dev->lock))
return -ERESTARTSYS;
/* Check if position is beyond data */
if (*f_pos >= dev->data_size)
goto out; /* EOF */
/* Adjust count if reading beyond end of data */
if (*f_pos + count > dev->data_size)
count = dev->data_size - *f_pos;
/* Copy data to user space */
if (copy_to_user(buf, dev->buffer + *f_pos, count)) {
retval = -EFAULT;
goto out;
}
/* Update file position */
*f_pos += count;
retval = count;
pr_info("simple_char: Read %zu bytes\n", count);
out:
mutex_unlock(&dev->lock);
return retval;
}
/*
* Device write
*/
static ssize_t simple_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
struct simple_char_dev *dev = filp->private_data;
ssize_t retval = 0;
pr_info("simple_char: Write requested: count=%zu, pos=%lld\n",
count, *f_pos);
/* Lock device during access */
if (mutex_lock_interruptible(&dev->lock))
return -ERESTARTSYS;
/* Check if write would overflow buffer */
if (*f_pos >= dev->buffer_size) {
retval = -ENOSPC; /* No space */
goto out;
}
/* Adjust count if writing beyond buffer */
if (*f_pos + count > dev->buffer_size)
count = dev->buffer_size - *f_pos;
/* Copy data from user space */
if (copy_from_user(dev->buffer + *f_pos, buf, count)) {
retval = -EFAULT;
goto out;
}
/* Update file position */
*f_pos += count;
/* Update data size if we extended it */
if (*f_pos > dev->data_size)
dev->data_size = *f_pos;
retval = count;
pr_info("simple_char: Wrote %zu bytes\n", count);
out:
mutex_unlock(&dev->lock);
return retval;
}
/*
* File operations structure
*/
static struct file_operations simple_fops = {
.owner = THIS_MODULE,
.open = simple_open,
.release = simple_release,
.read = simple_read,
.write = simple_write,
};
/*
* Module initialization
*/
static int __init simple_char_init(void)
{
int ret;
pr_info("simple_char: Initializing module\n");
/*
* Step 1: Allocate device structure
*/
simple_dev = kzalloc(sizeof(struct simple_char_dev), GFP_KERNEL);
if (!simple_dev) {
pr_err("simple_char: Failed to allocate device structure\n");
return -ENOMEM;
}
/*
* Step 2: Allocate buffer
*/
simple_dev->buffer = kzalloc(BUFFER_SIZE, GFP_KERNEL);
if (!simple_dev->buffer) {
pr_err("simple_char: Failed to allocate buffer\n");
ret = -ENOMEM;
goto err_alloc_buffer;
}
simple_dev->buffer_size = BUFFER_SIZE;
simple_dev->data_size = 0;
/*
* Step 3: Initialize mutex
*/
mutex_init(&simple_dev->lock);
/*
* Step 4: Allocate device number
*/
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("simple_char: Failed to allocate device number\n");
goto err_alloc_chrdev;
}
major = MAJOR(dev_num);
pr_info("simple_char: Allocated major number %d\n", major);
/*
* Step 5: Initialize and add cdev
*/
cdev_init(&simple_dev->cdev, &simple_fops);
simple_dev->cdev.owner = THIS_MODULE;
ret = cdev_add(&simple_dev->cdev, dev_num, 1);
if (ret < 0) {
pr_err("simple_char: Failed to add cdev\n");
goto err_cdev_add;
}
pr_info("simple_char: Device registered successfully\n");
pr_info("simple_char: Create device with: mknod /dev/%s c %d 0\n",
DEVICE_NAME, major);
return 0;
err_cdev_add:
unregister_chrdev_region(dev_num, 1);
err_alloc_chrdev:
kfree(simple_dev->buffer);
err_alloc_buffer:
kfree(simple_dev);
return ret;
}
/*
* Module cleanup
*/
static void __exit simple_char_exit(void)
{
pr_info("simple_char: Cleaning up module\n");
/* Remove cdev */
cdev_del(&simple_dev->cdev);
/* Release device number */
unregister_chrdev_region(dev_num, 1);
/* Free buffer */
kfree(simple_dev->buffer);
/* Free device structure */
kfree(simple_dev);
pr_info("simple_char: Module unloaded\n");
}
module_init(simple_char_init);
module_exit(simple_char_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tutorial Author");
MODULE_DESCRIPTION("Simple character device driver");
MODULE_VERSION("1.0");
Testing the Driver
# Build the driver
make
# Load the module
sudo insmod simple_chardev.ko
# Check dmesg for major number
dmesg | tail
# Create device node (replace 240 with actual major from dmesg)
sudo mknod /dev/simple_char c 240 0
sudo chmod 666 /dev/simple_char
# Test writing
echo "Hello from userspace!" > /dev/simple_char
# Test reading
cat /dev/simple_char
# More write/read tests
echo "Line 1" > /dev/simple_char
echo "Line 2" >> /dev/simple_char
cat /dev/simple_char
# Unload module
sudo rmmod simple_chardev
sudo rm /dev/simple_char
Device Classes and udev
Theory: Automatic Device Node Creation
Modern Linux uses udev to automatically create device nodes in /dev. We use the device class API to notify udev.
Implementing Automatic Device Creation
/*
* auto_chardev.c - Character device with automatic device node creation
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h> /* For class_create, device_create */
#include <linux/uaccess.h>
#define DEVICE_NAME "auto_char"
#define CLASS_NAME "auto_class"
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *dev_class; /* Device class */
static struct device *dev_device; /* Device */
/* File operations (simplified) */
static int auto_open(struct inode *inode, struct file *filp)
{
pr_info("auto_char: Device opened\n");
return 0;
}
static int auto_release(struct inode *inode, struct file *filp)
{
pr_info("auto_char: Device closed\n");
return 0;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = auto_open,
.release = auto_release,
};
static int __init auto_char_init(void)
{
int ret;
/*
* Step 1: Allocate device number
*/
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
pr_info("Allocated device number: %d:%d\n", MAJOR(dev_num), MINOR(dev_num));
/*
* Step 2: Create device class
*
* Creates /sys/class/CLASS_NAME/
* This makes udev aware of the device class
*/
dev_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(dev_class)) {
pr_err("Failed to create device class\n");
ret = PTR_ERR(dev_class);
goto err_class;
}
pr_info("Device class created\n");
/*
* Step 3: Initialize and add cdev
*/
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
pr_err("Failed to add cdev\n");
goto err_cdev;
}
/*
* Step 4: Create device
*
* This creates:
* - /sys/class/CLASS_NAME/DEVICE_NAME
* - /dev/DEVICE_NAME (automatically via udev)
*
* Parameters:
* class: The device class
* parent: Parent device (NULL for no parent)
* devt: Device number
* drvdata: Driver private data (NULL if not needed)
* fmt: Device name format string
*/
dev_device = device_create(dev_class, NULL, dev_num, NULL, DEVICE_NAME);
if (IS_ERR(dev_device)) {
pr_err("Failed to create device\n");
ret = PTR_ERR(dev_device);
goto err_device;
}
pr_info("Device created automatically at /dev/%s\n", DEVICE_NAME);
return 0;
err_device:
cdev_del(&my_cdev);
err_cdev:
class_destroy(dev_class);
err_class:
unregister_chrdev_region(dev_num, 1);
return ret;
}
static void __exit auto_char_exit(void)
{
/*
* Cleanup in reverse order
*/
device_destroy(dev_class, dev_num); /* Remove /dev/DEVICE_NAME */
cdev_del(&my_cdev);
class_destroy(dev_class); /* Remove /sys/class/CLASS_NAME */
unregister_chrdev_region(dev_num, 1);
pr_info("Device removed\n");
}
module_init(auto_char_init);
module_exit(auto_char_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tutorial Author");
MODULE_DESCRIPTION("Character device with automatic device node creation");
MODULE_VERSION("1.0");
Testing Automatic Creation
# Load module
sudo insmod auto_chardev.ko
# Device is automatically created!
ls -l /dev/auto_char
# Check sysfs
ls -l /sys/class/auto_class/auto_char/
# Test device
echo "test" > /dev/auto_char
# Unload module (device automatically removed)
sudo rmmod auto_chardev
# Device is gone
ls -l /dev/auto_char # Should fail
Multiple Devices
Managing Multiple Device Instances
/*
* multi_chardev.c - Multiple character devices
*
* Creates multiple device instances from single driver
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/slab.h>
#define DEVICE_NAME "multi_char"
#define CLASS_NAME "multi_class"
#define NUM_DEVICES 4 /* Create 4 device instances */
/* Per-device data structure */
struct multi_device {
struct cdev cdev;
int id; /* Device instance ID */
char data[256]; /* Device-specific data */
};
static dev_t dev_num; /* First device number */
static struct class *dev_class;
static struct multi_device *devices[NUM_DEVICES];
static int multi_open(struct inode *inode, struct file *filp)
{
struct multi_device *dev;
/* Get device instance from inode */
dev = container_of(inode->i_cdev, struct multi_device, cdev);
filp->private_data = dev;
pr_info("multi_char%d: Device opened\n", dev->id);
return 0;
}
static int multi_release(struct inode *inode, struct file *filp)
{
struct multi_device *dev = filp->private_data;
pr_info("multi_char%d: Device closed\n", dev->id);
return 0;
}
static ssize_t multi_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
struct multi_device *dev = filp->private_data;
pr_info("multi_char%d: Read operation\n", dev->id);
/* Implement read... */
return 0;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = multi_open,
.release = multi_release,
.read = multi_read,
};
static int __init multi_char_init(void)
{
int ret, i;
dev_t curr_dev;
/*
* Allocate device numbers for all devices
* Request NUM_DEVICES consecutive minor numbers
*/
ret = alloc_chrdev_region(&dev_num, 0, NUM_DEVICES, DEVICE_NAME);
if (ret < 0) {
pr_err("Failed to allocate device numbers\n");
return ret;
}
pr_info("Allocated device numbers: %d:%d-%d\n",
MAJOR(dev_num), MINOR(dev_num), MINOR(dev_num) + NUM_DEVICES - 1);
/* Create device class */
dev_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(dev_class)) {
ret = PTR_ERR(dev_class);
goto err_class;
}
/*
* Create each device instance
*/
for (i = 0; i < NUM_DEVICES; i++) {
/* Allocate device structure */
devices[i] = kzalloc(sizeof(struct multi_device), GFP_KERNEL);
if (!devices[i]) {
ret = -ENOMEM;
goto err_devices;
}
devices[i]->id = i;
curr_dev = MKDEV(MAJOR(dev_num), MINOR(dev_num) + i);
/* Initialize cdev */
cdev_init(&devices[i]->cdev, &fops);
devices[i]->cdev.owner = THIS_MODULE;
ret = cdev_add(&devices[i]->cdev, curr_dev, 1);
if (ret < 0) {
kfree(devices[i]);
goto err_devices;
}
/* Create device node */
if (IS_ERR(device_create(dev_class, NULL, curr_dev, NULL,
DEVICE_NAME "%d", i))) {
cdev_del(&devices[i]->cdev);
kfree(devices[i]);
goto err_devices;
}
pr_info("Created /dev/%s%d\n", DEVICE_NAME, i);
}
return 0;
err_devices:
/* Cleanup created devices */
for (i = i - 1; i >= 0; i--) {
device_destroy(dev_class, MKDEV(MAJOR(dev_num), MINOR(dev_num) + i));
cdev_del(&devices[i]->cdev);
kfree(devices[i]);
}
class_destroy(dev_class);
err_class:
unregister_chrdev_region(dev_num, NUM_DEVICES);
return ret;
}
static void __exit multi_char_exit(void)
{
int i;
/* Remove all devices */
for (i = 0; i < NUM_DEVICES; i++) {
device_destroy(dev_class, MKDEV(MAJOR(dev_num), MINOR(dev_num) + i));
cdev_del(&devices[i]->cdev);
kfree(devices[i]);
}
class_destroy(dev_class);
unregister_chrdev_region(dev_num, NUM_DEVICES);
pr_info("All devices removed\n");
}
module_init(multi_char_init);
module_exit(multi_char_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tutorial Author");
MODULE_DESCRIPTION("Multiple character device instances");
MODULE_VERSION("1.0");
Summary
In this chapter, you learned:
✅ Character Device Basics: Stream-oriented device access
✅ Device Numbers: Major/minor number allocation and management
✅ cdev Structure: Representing character devices in kernel
✅ File Operations: Implementing open, read, write, release
✅ User-Kernel Transfer: Safe data copying with copy_to/from_user
✅ Device Classes: Automatic device node creation with udev
✅ Multiple Devices: Managing multiple device instances
Key Takeaways
- Always use dynamic device number allocation (alloc_chrdev_region)
- Use copy_to_user/copy_from_user for safe data transfer
- Implement proper error handling and cleanup
- Use device classes for automatic /dev node creation
- Protect shared data with mutexes (covered more in Chapter 5)
Next Steps
Proceed to 03-file-operations.md to learn about advanced file operations including ioctl, poll, and mmap.
Quick Reference
Essential Headers
#include <linux/fs.h> /* file_operations, inode */
#include <linux/cdev.h> /* cdev structure and functions */
#include <linux/device.h> /* class_create, device_create */
#include <linux/uaccess.h> /* copy_to/from_user */
Common Functions
/* Device number management */
alloc_chrdev_region(&dev, minor, count, name);
unregister_chrdev_region(dev, count);
MAJOR(dev), MINOR(dev), MKDEV(major, minor);
/* cdev management */
cdev_init(&cdev, &fops);
cdev_add(&cdev, dev, count);
cdev_del(&cdev);
/* Device class/node creation */
class_create(owner, name);
device_create(class, parent, dev, drvdata, fmt, ...);
device_destroy(class, dev);
class_destroy(class);
/* User-kernel transfer */
copy_to_user(to, from, n);
copy_from_user(to, from, n);