Writing a Linux driver connects your hardware directly to the operating system’s kernel. If you’ve ever wondered how to write a linux driver, this guide breaks down the entire process into clear, actionable steps. You don’t need to be a kernel expert to start—just a solid grasp of C programming and some patience.
Drivers are the bridge between your device and the OS. They handle communication, data transfer, and control. Without them, your hardware is just a collection of chips and wires. Let’s get you writing your first driver today.
Understanding The Linux Driver Model
Before you write code, you need to understand how Linux organizes drivers. The kernel uses a modular system where drivers can be loaded and unloaded at runtime. This makes development faster and safer.
Every driver fits into one of three categories: char drivers, block drivers, or network drivers. Char drivers handle devices that stream data byte by byte—like keyboards or serial ports. Block drivers manage storage devices that read and write in blocks. Network drivers deal with network interfaces.
For beginners, char drivers are the easiest to start with. They have a simpler API and fewer edge cases. We’ll focus on char drivers in this guide.
How To Write A Linux Driver
Setting Up Your Development Environment
You need a Linux system with kernel headers installed. Most distributions let you install them with a single command. For Ubuntu or Debian, run:
sudo apt-get install linux-headers-$(uname -r)
For Fedora or CentOS, use:
sudo dnf install kernel-devel
You also need a compiler and build tools. Install build-essential on Debian-based systems or “Development Tools” group on Red Hat-based ones.
Set up a dedicated workspace directory. This keeps your driver files organized and separate from other projects.
Creating The Driver Skeleton
Every Linux driver starts with a minimal structure. Create a file called hello_driver.c. This file will contain the initialization and cleanup functions.
Here’s the basic skeleton:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void) {
printk(KERN_INFO "Hello, driver world!\n");
return 0;
}
static void __exit hello_exit(void) {
printk(KERN_INFO "Goodbye, driver world!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Linux driver");
The __init and __exit macros mark functions that run when the driver loads and unloads. printk is the kernel’s version of printf—it sends messages to the kernel log.
Writing The Makefile
You need a Makefile to compile your driver. The kernel build system handles most of the complexity. Create a file named Makefile in the same directory:
obj-m += hello_driver.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
Run make in your terminal. If everything is set up correctly, you’ll see a .ko file—that’s your kernel module.
Loading And Testing The Driver
Use insmod to load your driver into the kernel:
sudo insmod hello_driver.ko
Check the kernel log to see if your message appeared:
dmesg | tail
You should see “Hello, driver world!” in the output. To unload the driver, use:
sudo rmmod hello_driver
Then check the log again for the goodbye message.
Adding Device Registration
A driver that only prints messages isn’t very useful. You need to register a device so user-space programs can interact with it. This involves creating a device number and a file operations structure.
First, allocate a major number for your device. You can use a static number or let the kernel assign one dynamically. Dynamic allocation is safer:
static int major_number;
static int __init hello_init(void) {
major_number = register_chrdev(0, "hello_driver", &fops);
if (major_number < 0) {
printk(KERN_ALERT "Failed to register device\n");
return major_number;
}
printk(KERN_INFO "Registered with major number %d\n", major_number);
return 0;
}
The fops structure holds pointers to functions that handle open, read, write, and close operations.
Implementing File Operations
Define the file operations structure with the functions you want to support. For a simple driver, you might only need open and release:
static int hello_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "Device opened\n");
return 0;
}
static int hello_release(struct inode *inode, struct file *file) {
printk(KERN_INFO "Device closed\n");
return 0;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
};
Add these above your init function. The .owner field prevents the module from being unloaded while it’s in use.
Handling Read And Write
To make your driver actually transfer data, implement read and write functions. These copy data between kernel space and user space:
static char message[256] = "Hello from kernel!\n";
static int message_len = 18;
static ssize_t hello_read(struct file *file, char __user *buffer,
size_t length, loff_t *offset) {
if (*offset >= message_len)
return 0;
if (length > message_len - *offset)
length = message_len - *offset;
if (copy_to_user(buffer, message + *offset, length))
return -EFAULT;
*offset += length;
return length;
}
static ssize_t hello_write(struct file *file, const char __user *buffer,
size_t length, loff_t *offset) {
if (length > 255)
length = 255;
if (copy_from_user(message, buffer, length))
return -EFAULT;
message_len = length;
message[length] = '\0';
return length;
}
Add these to your fops structure. The copy_to_user and copy_from_user functions handle the safe transfer between kernel and user memory.
Creating A Device Node
After loading the driver, create a device node in /dev so user programs can access it:
sudo mknod /dev/hello_driver c 240 0
Replace 240 with your actual major number (check dmesg). The “c” means character device, and “0” is the minor number.
Now you can read from and write to the device:
cat /dev/hello_driver
echo "New message" > /dev/hello_driver
Handling Multiple Devices
Real hardware often has multiple instances. Use the minor number to distinguish between them. You can register a range of minor numbers with register_chrdev_region or use the modern cdev interface.
The cdev approach is more flexible:
static struct cdev my_cdev;
static int __init hello_init(void) {
dev_t dev_num;
alloc_chrdev_region(&dev_num, 0, 1, "hello_driver");
major_number = MAJOR(dev_num);
cdev_init(&my_cdev, &fops);
cdev_add(&my_cdev, dev_num, 1);
printk(KERN_INFO "Device registered\n");
return 0;
}
This method is preferred for new drivers. It gives you better control over device numbers and lifecycle.
Adding Device Class And Udev
Modern Linux systems use udev to automatically create device nodes. To support this, create a device class and device:
static struct class *hello_class;
static int __init hello_init(void) {
// ... previous code ...
hello_class = class_create(THIS_MODULE, "hello_class");
device_create(hello_class, NULL, dev_num, NULL, "hello_driver");
return 0;
}
Now when you load the driver, udev automatically creates /dev/hello_driver. No manual mknod needed.
Debugging Your Driver
Kernel debugging is different from user-space debugging. The primary tool is dmesg to view printk messages. Use different log levels:
KERN_INFOfor general informationKERN_DEBUGfor debug messagesKERN_ERRfor errorsKERN_ALERTfor critical issues
You can also use printk_ratelimit to avoid flooding the log. For more advanced debugging, consider using kgdb or kprobes.
Handling Hardware Interaction
Real drivers talk to actual hardware. This involves memory-mapped I/O, interrupts, and DMA. For a PCI device, you’d probe the bus and request regions:
static int my_pci_probe(struct pci_dev *dev, const struct pci_device_id *id) {
pci_enable_device(dev);
pci_request_regions(dev, "my_driver");
// Map BAR0
void __iomem *base = pci_iomap(dev, 0, 0);
// Read/write registers
iowrite32(0x01, base + REG_CONTROL);
ioread32(base + REG_STATUS);
return 0;
}
Interrupts require registering a handler:
static irqreturn_t my_interrupt_handler(int irq, void *dev_id) {
// Handle interrupt
return IRQ_HANDLED;
}
// In probe:
request_irq(dev->irq, my_interrupt_handler, IRQF_SHARED, "my_driver", dev);
Using Kernel APIs Safely
The kernel has strict rules about memory allocation and locking. Use kmalloc for small allocations and vmalloc for large ones. Always check return values for NULL.
For synchronization, use spinlocks in interrupt context and mutexes in process context:
static DEFINE_SPINLOCK(my_lock);
spin_lock(&my_lock);
// Critical section
spin_unlock(&my_lock);
static DEFINE_MUTEX(my_mutex);
mutex_lock(&my_mutex);
// Critical section
mutex_unlock(&my_mutex);
Building For Different Kernel Versions
Kernel APIs change between versions. Use conditional compilation to handle differences:
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,10,0)
// New API
#else
// Old API
#endif
This makes your driver portable across multiple kernel versions.
Testing With Real Hardware
Start with virtual hardware or emulators. QEMU can emulate various devices. Use modprobe to load your driver with parameters:
sudo modprobe hello_driver debug=1
Add module parameters to your code:
static int debug = 0;
module_param(debug, int, 0644);
Common Pitfalls And Solutions
- Kernel panic: Always check return values and use proper locking
- Memory leaks: Use
kfreein cleanup functions - Device not appearing: Check udev rules and device class creation
- Permission denied: Set proper file mode with
device_create
Submitting Your Driver Upstream
If you want your driver included in the mainline kernel, follow the Linux kernel coding style. Use checkpatch.pl to verify your code. Submit patches to the appropriate mailing list with a clear description.
Frequently Asked Questions
What programming language is used for Linux drivers?
C is the standard language for Linux kernel drivers. Some parts use assembly, but C covers 99% of what you need.
Do I need to recompile the kernel to add a driver?
No. You can compile your driver as a module and load it dynamically. Only built-in drivers require kernel recompilation.
How long does it take to write a simple driver?
A basic char driver can be written in a few hours. Complex hardware drivers may take weeks or months.
Can I write a driver in Python or Rust?
Python is not supported for kernel drivers. Rust has experimental support in recent kernels, but C remains the standard.
What’s the best resource for learning more?
The Linux Device Drivers book (free online) and the kernel documentation in /Documentation are excellent starting points.
Writing a Linux driver is a rewarding skill that gives you deep understanding of how your system works. Start with the simple skeleton we built here, then gradually add complexity as you learn. Test on virtual hardware first, and always keep a backup of your kernel config. With practice, you’ll be able to write drivers for any device you encounter.