Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/deelerdev/linux/llms.txt

Use this file to discover all available pages before exploring further.

Writing a Linux device driver starts with understanding two things: the kernel module infrastructure that loads and unloads your code, and the driver model APIs that bind your driver to a device. This guide walks you through both, starting from the simplest possible module and building up to a character device driver and a platform driver.
1

Set up a minimal kernel module

Every loadable kernel module must declare an init function and an exit function, and announce its license.
2

Allocate a character device number

Use alloc_chrdev_region() to dynamically allocate a major number and register your device range.
3

Implement file_operations

Fill in open, release, read, write, and unlocked_ioctl to handle user-space system calls.
4

Register the cdev

Initialize a struct cdev and add it to the kernel with cdev_add().
5

Create a device node

Use class_create() and device_create() so that udev automatically creates the /dev entry.
6

Test and clean up

Load the module with insmod, verify it in /proc/modules and /sys, then remove it cleanly with rmmod.

Setting up a minimal kernel module

Every kernel module begins with three pieces: an init function, an exit function, and module metadata.
/* minimal_module.c */
#include <linux/module.h>
#include <linux/init.h>

static int __init minimal_init(void)
{
	pr_info("minimal: loaded\n");
	return 0;
}

static void __exit minimal_exit(void)
{
	pr_info("minimal: unloaded\n");
}

module_init(minimal_init);
module_exit(minimal_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <you@example.com>");
MODULE_DESCRIPTION("A minimal kernel module example");
The three macros at the bottom come from <linux/module.h>:
/* From include/linux/module.h */

/* Called during do_initcalls() (built-in) or at module insertion time. */
#define module_init(x)	__initcall(x);

/* Wraps cleanup code with cleanup_module() for rmmod. No effect if built-in. */
#define module_exit(x)	__exitcall(x);

/* Embeds license info in the module ELF section. */
#define MODULE_LICENSE(_license) MODULE_FILE MODULE_INFO(license, _license)

/* Author and description metadata. */
#define MODULE_AUTHOR(_author)      MODULE_INFO(author, _author)
#define MODULE_DESCRIPTION(_desc)   MODULE_INFO(description, _desc)
Build the module with a minimal Makefile:
obj-m += minimal_module.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
make
sudo insmod minimal_module.ko
dmesg | tail -1   # should print "minimal: loaded"
sudo rmmod minimal_module

Writing a character device driver

A character device driver exposes a device file under /dev. User-space programs call standard file operations on it. The driver provides the implementations through a struct file_operations.

struct file_operations

/* From include/linux/fs.h (lines 1926–1970) */
struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	__poll_t (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
} __randomize_layout;

struct cdev

struct cdev is the kernel’s internal representation of a character device.
/* From include/linux/cdev.h */
struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
} __randomize_layout;

void cdev_init(struct cdev *, const struct file_operations *);
int  cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);

Complete character device example

/* chardev.c — minimal character device driver */
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "mychardev"
#define BUF_SIZE    256

static dev_t         mydev_num;    /* major/minor number allocated */
static struct cdev   mydev_cdev;
static struct class *mydev_class;
static char          mydev_buf[BUF_SIZE];
static size_t        mydev_buf_len;

static int mydev_open(struct inode *inode, struct file *filp)
{
	pr_info(DEVICE_NAME ": open\n");
	return 0;
}

static int mydev_release(struct inode *inode, struct file *filp)
{
	pr_info(DEVICE_NAME ": release\n");
	return 0;
}

static ssize_t mydev_read(struct file *filp, char __user *buf,
			  size_t count, loff_t *ppos)
{
	size_t to_copy = min(count, mydev_buf_len);

	if (copy_to_user(buf, mydev_buf, to_copy))
		return -EFAULT;

	*ppos += to_copy;
	return to_copy;
}

static ssize_t mydev_write(struct file *filp, const char __user *buf,
			   size_t count, loff_t *ppos)
{
	size_t to_copy = min(count, (size_t)(BUF_SIZE - 1));

	if (copy_from_user(mydev_buf, buf, to_copy))
		return -EFAULT;

	mydev_buf_len = to_copy;
	mydev_buf[to_copy] = '\0';
	return to_copy;
}

static long mydev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	switch (cmd) {
	case 0:
		memset(mydev_buf, 0, BUF_SIZE);
		mydev_buf_len = 0;
		return 0;
	default:
		return -ENOTTY;
	}
}

static const struct file_operations mydev_fops = {
	.owner          = THIS_MODULE,
	.open           = mydev_open,
	.release        = mydev_release,
	.read           = mydev_read,
	.write          = mydev_write,
	.unlocked_ioctl = mydev_ioctl,
};

static int __init chardev_init(void)
{
	int ret;
	struct device *dev;

	/* Dynamically allocate a major number for 1 minor device. */
	ret = alloc_chrdev_region(&mydev_num, 0, 1, DEVICE_NAME);
	if (ret < 0) {
		pr_err(DEVICE_NAME ": alloc_chrdev_region failed: %d\n", ret);
		return ret;
	}

	/* Initialise and add the cdev to the kernel. */
	cdev_init(&mydev_cdev, &mydev_fops);
	mydev_cdev.owner = THIS_MODULE;

	ret = cdev_add(&mydev_cdev, mydev_num, 1);
	if (ret) {
		pr_err(DEVICE_NAME ": cdev_add failed: %d\n", ret);
		goto err_unreg;
	}

	/* Create a class so udev generates /dev/mychardev. */
	mydev_class = class_create(DEVICE_NAME);
	if (IS_ERR(mydev_class)) {
		ret = PTR_ERR(mydev_class);
		goto err_cdev;
	}

	dev = device_create(mydev_class, NULL, mydev_num, NULL, DEVICE_NAME);
	if (IS_ERR(dev)) {
		ret = PTR_ERR(dev);
		goto err_class;
	}

	pr_info(DEVICE_NAME ": registered at %d:%d\n",
		MAJOR(mydev_num), MINOR(mydev_num));
	return 0;

err_class:
	class_destroy(mydev_class);
err_cdev:
	cdev_del(&mydev_cdev);
err_unreg:
	unregister_chrdev_region(mydev_num, 1);
	return ret;
}

static void __exit chardev_exit(void)
{
	device_destroy(mydev_class, mydev_num);
	class_destroy(mydev_class);
	cdev_del(&mydev_cdev);
	unregister_chrdev_region(mydev_num, 1);
	pr_info(DEVICE_NAME ": unregistered\n");
}

module_init(chardev_init);
module_exit(chardev_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Minimal character device driver example");

Registering with alloc_chrdev_region

alloc_chrdev_region() asks the kernel to assign an unused major number. You pass it the base minor and the count of minors you need:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor,
                        unsigned count, const char *name);
Pair every successful alloc_chrdev_region() with unregister_chrdev_region() in your exit path.
Always use alloc_chrdev_region() rather than hard-coding a major number. Hard-coded major numbers conflict with other drivers and are rejected upstream.

Platform driver example

Platform drivers manage SoC peripherals that appear in the Device Tree or ACPI tables. The kernel populates a struct platform_device for each node and your driver binds to it through the name or the of_match_table.

struct platform_driver

/* From Documentation/driver-api/driver-model/platform.rst */
struct platform_driver {
	int (*probe)(struct platform_device *);
	void (*remove)(struct platform_device *);
	void (*shutdown)(struct platform_device *);
	int (*suspend)(struct platform_device *, pm_message_t state);
	int (*resume)(struct platform_device *);
	struct device_driver driver;
	const struct platform_device_id *id_table;
	bool prevent_deferred_probe;
	bool driver_managed_dma;
};
Register and unregister with:
int platform_driver_register(struct platform_driver *drv);
void platform_driver_unregister(struct platform_driver *drv);

Complete platform driver example

/* myplatdrv.c — minimal platform driver */
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/io.h>

struct myplatdrv_data {
	void __iomem *base;
};

static int myplatdrv_probe(struct platform_device *pdev)
{
	struct myplatdrv_data *priv;
	struct resource *res;

	priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
	if (!priv)
		return -ENOMEM;

	/*
	 * devm_ioremap_resource() maps the first memory resource and
	 * automatically unmaps it when the driver is removed or probe fails.
	 */
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	priv->base = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(priv->base))
		return PTR_ERR(priv->base);

	platform_set_drvdata(pdev, priv);

	dev_info(&pdev->dev, "probed at %pa\n", &res->start);
	return 0;
}

static void myplatdrv_remove(struct platform_device *pdev)
{
	dev_info(&pdev->dev, "removed\n");
	/* devm resources are freed automatically */
}

static const struct of_device_id myplatdrv_of_match[] = {
	{ .compatible = "vendor,myplatdrv" },
	{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, myplatdrv_of_match);

static struct platform_driver myplatdrv_driver = {
	.probe  = myplatdrv_probe,
	.remove = myplatdrv_remove,
	.driver = {
		.name           = "myplatdrv",
		.of_match_table = myplatdrv_of_match,
	},
};

module_platform_driver(myplatdrv_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Minimal platform driver example");
module_platform_driver() is a shorthand macro defined in <linux/platform_device.h>. It expands to a module_init() that calls platform_driver_register() and a module_exit() that calls platform_driver_unregister(), saving you the boilerplate.
Use devm_* (device-managed) resource allocation functions wherever they exist. Resources acquired with devm_kzalloc(), devm_ioremap_resource(), or devm_request_irq() are freed automatically when the device is unbound, eliminating a whole class of resource-leak bugs.

Handling interrupts with request_irq

Most hardware drivers need to respond to interrupts. Call request_irq() inside probe() once the hardware is initialized, and call free_irq() inside remove().
/* From include/linux/interrupt.h (lines 172–177) */
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)
{
	return request_threaded_irq(irq, handler, NULL,
				    flags | IRQF_COND_ONESHOT, name, dev);
}
A typical interrupt handler looks like this:
static irqreturn_t mydrv_irq_handler(int irq, void *dev_id)
{
	struct mydrv_data *priv = dev_id;

	/* Read the hardware status register to confirm the interrupt. */
	u32 status = readl(priv->base + STATUS_REG);
	if (!(status & IRQ_PENDING))
		return IRQ_NONE;

	/* Acknowledge the interrupt. */
	writel(IRQ_CLEAR, priv->base + STATUS_REG);

	/* Wake up any waiting processes. */
	wake_up_interruptible(&priv->wait_queue);

	return IRQ_HANDLED;
}

static int mydrv_probe(struct platform_device *pdev)
{
	struct mydrv_data *priv;
	int irq, ret;

	/* ... allocate priv, map registers ... */

	irq = platform_get_irq(pdev, 0);
	if (irq < 0)
		return irq;

	ret = devm_request_irq(&pdev->dev, irq, mydrv_irq_handler,
			       IRQF_SHARED, "mydrv", priv);
	if (ret) {
		dev_err(&pdev->dev, "request_irq failed: %d\n", ret);
		return ret;
	}

	return 0;
}
Use devm_request_irq() instead of request_irq() when possible. It automatically calls free_irq() when the device is unbound, so you never need to pair it manually with a free_irq() call in remove().
IRQ_HANDLED tells the kernel your handler processed the interrupt. IRQ_NONE tells it the interrupt came from a different device sharing the same line. For shared interrupts (IRQF_SHARED), always check a hardware status register before returning IRQ_HANDLED.

Development workflow summary

1

Create the source file

Write your .c file with module_init, module_exit, and your driver logic.
2

Write a Kbuild Makefile

Use obj-m += yourdriver.o and point make at the kernel build tree.
3

Build the module

Run make -C /lib/modules/$(uname -r)/build M=$PWD modules.
4

Load and test

Use sudo insmod yourdriver.ko, check dmesg for your log messages, and exercise the driver.
5

Unload and fix

Use sudo rmmod yourdriver. Check dmesg for cleanup messages and verify no resources are leaked.
6

Run in-tree (optional)

Move the source into the kernel tree under drivers/, add an entry to the subsystem’s Kconfig and Makefile, and build with CONFIG_YOURDRIVER=m.

Driver model overview

Understand driver types, the probe/remove lifecycle, and key headers before writing code.

Linux device model

Learn how struct device, sysfs, power management, and device links fit together.

Build docs developers (and LLMs) love