Linux Drivers: A Simple Character Device
Published: September 14, 2025 | Tags: #c #linux #kernel #systems
Diving into kernel development can seem intimidating, but creating a simple driver is a fantastic way to understand the architecture of a modern operating system. In this guide, we’ll start with some essential theory before writing our very own “character device” driver from scratch.
Theory First: What Are We Building?
Before we touch any code, let’s understand a few key concepts.
Kernel Space vs. User Space
A core principle of Linux is the separation between Kernel Space and User Space.
- User Space is where your applications run (your browser, terminal, text editor). These programs have limited access to hardware and system resources.
- Kernel Space is the protected area where the kernel, the core of the OS, runs. It has direct access to hardware and manages the system. The boss.
A driver is a piece of code that runs in kernel space and acts as a bridge
This allows the user space applications to securely communicate with a piece of hardware or a kernel feature.
What is a Device File?
In Linux, almost everything is treated as a file. This includes hardware devices.
When you interact with a webcam, printer, or your hard drive, you’re often doing so through a “weird” file located in the /dev
directory.
So, you can use standard commands and functions like cat
, echo
, open()
, read()
, and write()
to interact with hardware (quite a powerful abstraction).
We are going to create a character device. This is a type of device file that handles data as a continuous stream of bytes, like a keyboard.
The Core Components
Our driver will have three main parts:
- An
init
function: This is the entry point. It’s responsible for “registering” the driver with the system so the kernel knows it exists. - An
exit
function: This is the cleanup function that runs when you unload the module. - A
file_operations
struct: A structure that links system calls made on our device file to the real functions inside our driver that will handle those calls. It is “the driver’s API”.
Building the Driver
Now that we have the theory, let’s write the code.
Prerequisites
- A Linux machine (a VM is fine too).
- Kernel headers installed (
sudo apt install linux-headers-$(uname -r)
on Debian/Ubuntu). - Basic knowledge of C and the command line. I already assume this, but just in case.
The Driver Code (mydevice.c
)
This C code implements the concepts we just discussed.
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zuhaitz");
MODULE_DESCRIPTION("A simple character device driver.");
#define DEVICE_NAME "mydevice" // No creativity, I know.
#define MSG_BUFFER_LEN 150
static int major_num;
static char msg_buffer[MSG_BUFFER_LEN] = "Hello from the kernel! :3\n";
static char *msg_ptr;
static int device_open(struct inode *, struct file *);
static int device_release(struct inode *, struct file *);
static ssize_t device_read(struct file *, char *, size_t, loff_t *);
static ssize_t device_write(struct file *, const char *, size_t, loff_t *);
static struct file_operations fops = {
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release,
};
static int __init driver_init(void)
{
printk(KERN_INFO "mydevice: Initializing the module.\n");
// We pass 0 to let the kernel assign a major number.
major_num = register_chrdev(0, DEVICE_NAME, &fops);
if (major_num < 0)
{
printk(KERN_ALERT "mydevice: failed to register a major number\n");
return major_num;
}
printk(KERN_INFO "mydevice: registered correctly with major number %d\n", major_num);
return 0;
}
static void __exit driver_exit(void)
{
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO "mydevice: Goodbye from the module!\n");
}
// For when a process opens our device file.
static int device_open(struct inode *inode, struct file *file)
{
msg_ptr = msg_buffer; // Reset the message pointer for reading
return 0;
}
// For closing our device file.
static int device_release(struct inode *inode, struct file *file)
{
return 0;
}
// When the process reads from our device file.
static ssize_t device_read(struct file *filp, char *buffer, size_t length, loff_t *offset)
{
int bytes_read = 0;
if (0 == *msg_ptr) // Did we send the whole thing?
{
return 0; // Signal of EOF
}
// Copy our kernel data to the user's buffer
while (length && *msg_ptr)
{
put_user(*(msg_ptr++), buffer++);
length--;
bytes_read++;
}
return bytes_read;
}
// For when a process writes to our device file.
static ssize_t device_write(struct file *filp, const char *buff, size_t len, loff_t *off)
{
printk(KERN_INFO "mydevice: Received %zu characters from the user\n", len);
// We don't store the data, just log.
return len;
}
module_init(driver_init);
module_exit(driver_exit);
The Makefile
To compile this into a kernel module (.ko file), we need this specific Makefile.
obj-m += mydevice.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
Compiling and Using the Driver
- Save the files.
- Compile.
make
- Load the driver.
sudo insmod mydevice.ko
- Check that it’s loaded.
dmesg | tail
- Create the device file. Use the major number you just saw. If it was
243
, for example, run:
# mknod <name> <type> <major> <minor>
sudo mknod /dev/mydevice c 243 0
- Time for testing!
echo "Hello, driver" | sudo tee /dev/mydevice
cat /dev/mydevice
- Check the kernel logs again.
dmesg | tail
- Unload the driver.
sudo rmmod mydevice
sudo rm /dev/mydevice
We’ve successfully built a Linux kernel module based on the core principles of communication between user space and the Linux kernel. Yipee!