driver-know-hows

device driver related stuff

View on GitHub

Chapter 2: Character Device Drivers

Table of Contents

  1. Introduction to Character Devices
  2. Device Numbers: Major and Minor
  3. The cdev Structure
  4. Registering Character Devices
  5. Basic File Operations
  6. Complete Character Driver Example
  7. Device Classes and udev
  8. 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:

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;
}
/*
 * 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

  1. Always use dynamic device number allocation (alloc_chrdev_region)
  2. Use copy_to_user/copy_from_user for safe data transfer
  3. Implement proper error handling and cleanup
  4. Use device classes for automatic /dev node creation
  5. 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);