Linux Device Drivers

Linux Device Drivers, 3rd Edition · Jonathan Corbet, Alessandro Rubini & Greg Kroah-Hartman ·600 pages

The definitive Linux driver development guide — char drivers (file_operations, ioctl, blocking I/O, poll), kernel synchronization (mutex/spinlock/completions), interrupt handling (top/bottom half), DMA (streaming/coherent), block drivers, network drivers (sk_buff), and the Linux device model (kobject/sysfs).

Capabilities (10)
  • Write a complete loadable kernel module with init/exit, parameters, and Makefile
  • Implement a character driver: registration, file_operations, read/write with copy_to_user
  • Use correct synchronization: mutex for sleeping, spinlock for IRQ context, completions for signaling
  • Implement ioctl commands with _IO/_IOR/_IOW macros
  • Implement blocking I/O with wait queues and wake_up
  • Implement poll/select support in a driver
  • Register and handle interrupts with request_irq, top/bottom half split
  • Perform DMA: streaming (dma_map_single) and coherent (dma_alloc_coherent)
  • Write a network driver: net_device, ndo_start_xmit, sk_buff allocation, netif_rx
  • Register PCI/USB devices with bus-specific probe/remove model
How to use

Install this skill and Claude can write complete loadable kernel modules and character drivers in C, design correct interrupt handling with top/bottom half splits, identify synchronization bugs in existing driver code, implement DMA buffer management, and wire up minimal network drivers with sk_buff handling

Why it matters

Bugs at the software/hardware boundary — wrong synchronization, improper DMA, missed interrupt acks — cause crashes and data corruption that are nearly impossible to debug without understanding the kernel's execution model; this skill enables correct driver development from scratch

Example use cases
  • Generate a complete char driver skeleton for a custom FPGA-attached device that exposes a read/write interface and supports poll() for blocking clients
  • Diagnose why a driver's interrupt handler is causing system hangs by identifying that it calls kmalloc(GFP_KERNEL) inside the ISR where sleeping is forbidden
  • Audit a driver's DMA handling to find where dma_map_single mappings are not being unmapped on the error path, causing IOMMU resource exhaustion

Linux Device Drivers Skill

Module Basics (Chapter 2)

Module Skeleton

#include <linux/module.h>
#include <linux/init.h>

static int __init mydriver_init(void) {
    printk(KERN_INFO "mydriver: loaded\n");
    return 0;
}

static void __exit mydriver_exit(void) {
    printk(KERN_INFO "mydriver: unloaded\n");
}

module_init(mydriver_init);
module_exit(mydriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Author");
MODULE_DESCRIPTION("Description");

Module Parameters

static int debug = 0;
module_param(debug, int, S_IRUGO);
MODULE_PARM_DESC(debug, "Enable debug output (0/1)");

Building

obj-m := mydriver.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
	make -C $(KDIR) M=$(PWD) modules

Load: insmod mydriver.ko / modprobe mydriver; unload: rmmod mydriver.


Character Drivers (Chapter 3)

Major and Minor Numbers

  • Major: identifies the driver
  • Minor: identifies specific device instance
  • MKDEV(major, minor), MAJOR(dev), MINOR(dev)

Registration

static int major;
static struct cdev my_cdev;

static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
    .llseek  = my_llseek,
    .unlocked_ioctl = my_ioctl,
};

static int __init init(void) {
    dev_t dev;
    alloc_chrdev_region(&dev, 0, 1, "mydev");
    major = MAJOR(dev);
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;
    cdev_add(&my_cdev, dev, 1);
    return 0;
}

read/write Operations

ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
    // copy_to_user returns number of bytes NOT copied (0 on success)
    if (copy_to_user(buf, kernel_data + *f_pos, count))
        return -EFAULT;
    *f_pos += count;
    return count;
}

ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
    if (copy_from_user(kernel_data + *f_pos, buf, count))
        return -EFAULT;
    return count;
}

Rule: always use copy_to_user/copy_from_user — never dereference user pointers directly.


Concurrency in Drivers (Chapter 5)

Semaphore (can sleep)

#include <linux/semaphore.h>
struct semaphore sem;
sema_init(&sem, 1);

down(&sem);        // acquire (sleeps if unavailable)
down_interruptible(&sem);  // returns -EINTR if signal received
// critical section
up(&sem);          // release

Mutex (preferred over semaphore)

#include <linux/mutex.h>
DEFINE_MUTEX(my_mutex);

mutex_lock(&my_mutex);
// critical section
mutex_unlock(&my_mutex);

Spinlock (cannot sleep)

#include <linux/spinlock.h>
spinlock_t my_lock = SPIN_LOCK_UNLOCKED;

unsigned long flags;
spin_lock_irqsave(&my_lock, flags);  // save interrupt state + lock
// critical section (cannot sleep, cannot call schedule())
spin_unlock_irqrestore(&my_lock, flags);

Completions (for signaling between code paths)

#include <linux/completion.h>
DECLARE_COMPLETION(my_completion);

// Thread A:
wait_for_completion(&my_completion);

// Thread B (or interrupt):
complete(&my_completion);

Advanced Char Operations (Chapter 6)

ioctl

#define MY_MAGIC 'k'
#define MY_RESET  _IO(MY_MAGIC, 0)
#define MY_GET    _IOR(MY_MAGIC, 1, int)
#define MY_SET    _IOW(MY_MAGIC, 2, int)

long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    switch (cmd) {
    case MY_RESET:
        // reset device
        break;
    case MY_GET:
        return put_user(my_value, (int __user *)arg);
    case MY_SET:
        return get_user(my_value, (int __user *)arg);
    default:
        return -ENOTTY;
    }
    return 0;
}

Blocking I/O (Wait Queues)

#include <linux/wait.h>
DECLARE_WAIT_QUEUE_HEAD(read_queue);

// In read: block until data available
wait_event_interruptible(read_queue, condition_true);
// Returns -ERESTARTSYS if signal received

// When data becomes available (e.g., in interrupt handler):
wake_up_interruptible(&read_queue);

poll/select Support

unsigned int my_poll(struct file *filp, poll_table *wait) {
    poll_wait(filp, &read_queue, wait);
    poll_wait(filp, &write_queue, wait);
    unsigned int mask = 0;
    if (data_available) mask |= POLLIN | POLLRDNORM;
    if (space_available) mask |= POLLOUT | POLLWRNORM;
    return mask;
}

Time and Deferred Work (Chapter 7)

Kernel Timers

#include <linux/timer.h>
struct timer_list my_timer;

void my_callback(struct timer_list *t) {
    // runs in softirq context — cannot sleep
    mod_timer(&my_timer, jiffies + HZ); // reschedule every 1 second
}

timer_setup(&my_timer, my_callback, 0);
mod_timer(&my_timer, jiffies + HZ);
del_timer_sync(&my_timer);  // cancel

Tasklets

void my_tasklet_func(struct tasklet_struct *t) { /* ... */ }
DECLARE_TASKLET(my_tasklet, my_tasklet_func);

// Schedule to run soon (softirq context):
tasklet_schedule(&my_tasklet);

Work Queues (can sleep)

#include <linux/workqueue.h>
DECLARE_WORK(my_work, my_work_func);

// Queue work to kernel thread (can sleep):
schedule_work(&my_work);

Memory Allocation (Chapter 8)

// Small allocations (< 128KB), physically contiguous:
void *p = kmalloc(size, GFP_KERNEL);  // may sleep
void *p = kmalloc(size, GFP_ATOMIC); // won't sleep (interrupt context)
kfree(p);

// Object cache (frequently allocated):
struct kmem_cache *cache = kmem_cache_create("name", size, align, flags, NULL);
p = kmem_cache_alloc(cache, GFP_KERNEL);
kmem_cache_free(cache, p);

// Large, virtually contiguous (can sleep):
void *p = vmalloc(size);
vfree(p);

// Per-CPU variable:
DEFINE_PER_CPU(int, my_counter);
get_cpu_var(my_counter)++;
put_cpu_var(my_counter);

Interrupt Handling (Chapter 10)

Registering an Interrupt

#include <linux/interrupt.h>

// Request IRQ:
int ret = request_irq(irq, my_handler, IRQF_SHARED, "mydev", dev_id);
// Returns 0 on success; negative on error

// Release:
free_irq(irq, dev_id);

Handler Structure

irqreturn_t my_handler(int irq, void *dev_id) {
    struct my_dev *dev = dev_id;
    // Check if this is our interrupt:
    if (!our_interrupt(dev)) return IRQ_NONE;
    // Handle hardware (fast — no sleeping!):
    // acknowledge interrupt, read status, schedule bottom half
    tasklet_schedule(&dev->tasklet);
    return IRQ_HANDLED;
}

Top half (handler): runs with interrupts disabled, must be fast, no sleeping, no blocking. Bottom half (tasklet/workqueue): deferred, can do more work, workqueue can sleep.


DMA (Chapter 15)

Streaming DMA (most common for data transfers)

#include <linux/dma-mapping.h>

// Map buffer for DMA:
dma_addr_t dma_handle = dma_map_single(
    dev, cpu_addr, size, DMA_TO_DEVICE);  // or DMA_FROM_DEVICE

// After DMA completes, unmap:
dma_unmap_single(dev, dma_handle, size, DMA_TO_DEVICE);

Consistent DMA (for control structures shared with device)

void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
dma_free_coherent(dev, size, cpu_addr, dma_handle);

mmap for DMA: use remap_pfn_range() to map device memory or DMA buffer into user space.


Block Drivers (Chapter 16)

#include <linux/blkdev.h>

static struct gendisk *my_disk;
static struct request_queue *my_queue;

// Process request queue:
static void my_request(struct request_queue *q) {
    struct request *req;
    while ((req = blk_fetch_request(q)) != NULL) {
        // transfer data between bio and device memory
        // ...
        __blk_end_request_all(req, 0);  // 0 = success
    }
}

Network Drivers (Chapter 17)

net_device Structure

struct net_device *dev = alloc_etherdev(sizeof(struct my_priv));
// Set fields:
dev->netdev_ops = &my_ops;
dev->irq = MY_IRQ;
register_netdev(dev);

static const struct net_device_ops my_ops = {
    .ndo_open         = my_open,
    .ndo_stop         = my_stop,
    .ndo_start_xmit   = my_tx,
    .ndo_get_stats    = my_stats,
};

Sending Packets

int my_tx(struct sk_buff *skb, struct net_device *dev) {
    // skb->data = packet data, skb->len = length
    // Copy to hardware TX buffer, start DMA
    dev_kfree_skb(skb);  // free when done (or after DMA completion)
    return NETDEV_TX_OK;
}

Receiving Packets

// In interrupt handler:
struct sk_buff *skb = dev_alloc_skb(pkt_len + 2);
skb_reserve(skb, 2);  // align IP header
// Copy from hardware into skb:
memcpy(skb_put(skb, pkt_len), rx_buffer, pkt_len);
skb->protocol = eth_type_trans(skb, dev);
netif_rx(skb);  // hand to network stack

Linux Device Model (Chapter 14)

Kobject / Kset / Subsystem

  • kobject: base object with reference counting, sysfs entry
  • kset: collection of kobjects
  • Each kobject appears as a directory in /sys/

Bus, Device, Driver Registration

// Driver registers with its bus type:
static struct pci_driver my_pci_driver = {
    .name     = "mydriver",
    .id_table = my_pci_ids,
    .probe    = my_probe,
    .remove   = my_remove,
};
pci_register_driver(&my_pci_driver);

// probe() called by kernel when matching device found:
int my_probe(struct pci_dev *pdev, const struct pci_device_id *id) {
    pci_enable_device(pdev);
    // allocate resources, init hardware
}

Sysfs Attributes

static DEVICE_ATTR(my_attr, S_IRUGO | S_IWUSR, show_fn, store_fn);
device_create_file(dev, &dev_attr_my_attr);
// Appears as: /sys/bus/platform/devices/mydev/my_attr