Linux Device Drivers
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).
- › 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
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
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
- › 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 entrykset: 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