Understanding Mutexes, Concurrency, and Character Device Drivers in Linux Kernel Modules

Search for a command to run...

No comments yet. Be the first to comment.
Modern software systems often separate control logic from high-performance execution logic. This design is common in networking, distributed systems, operating systems, storage engines, and embedded s

Introduction Engineering software often combines multiple programming languages to leverage their individual strengths. A common approach is to implement computational algorithms in native C or C++ wh

Deadlocks are one of the most common synchronization problems encountered in operating systems and concurrent programming. Although the concept is frequently introduced in textbooks, observing it insi

Memory and resource allocation are fundamental operations inside the Linux kernel. Whether assigning device IDs, managing CPU masks, allocating interrupt vectors, or tracking hardware resources, the k

The Linux scheduler is one of the most important components of the operating system. Every running program, background service, and kernel thread eventually interacts with the scheduler. In this artic

Synchronization bugs are among the most difficult issues to debug in kernel-space software. A simple character device driver can become unsafe when multiple processes access shared resources concurrently.
In this article, I build and analyze a Linux character device driver that demonstrates four important kernel concepts:
Mutex synchronization
Concurrency control
Character device drivers
Linux version compatibility
The driver creates a device node named:
/dev/mydevice
and returns a simple message to userspace when read.
A mutex ensures that only one execution context accesses a critical section at a time.
The driver declares a mutex using:
static DEFINE_MUTEX(dev_mutex);
Before accessing shared state, the driver acquires the lock:
if (mutex_lock_interruptible(&dev_mutex))
return -ERESTARTSYS;
and releases it when finished:
mutex_unlock(&dev_mutex);
Without synchronization, multiple processes could simultaneously execute the read handler and modify shared data.
Benefits of mutexes include:
Preventing race conditions
Protecting shared kernel data
Simplifying synchronization logic
Allowing sleeping operations safely
Unlike spinlocks, mutexes can safely surround operations such as:
copy_to_user(...)
which may sleep.
Concurrency occurs when multiple execution contexts attempt to access the same resource at the same time.
Consider several terminals executing:
cat /dev/mydevice
simultaneously.
Without synchronization:
Multiple threads may enter the read handler together
Shared state can become inconsistent
Race conditions become possible
With mutex protection:
Access becomes serialized
Shared state remains consistent
Device behavior becomes predictable
Kernel developers must always assume that a driver may be accessed concurrently.
A character device transfers data as a stream of bytes between user space and kernel space.
Common examples include:
/dev/null
/dev/random
/dev/tty
Our module registers a character device using:
major_num = register_chrdev(
0,
"mydevice",
&fops
);
Passing zero requests a dynamically allocated major number.
The kernel communicates with the driver through a file operations table:
static const struct file_operations fops = {
.open = dev_open,
.read = dev_read,
.release = dev_release,
};
This maps standard system calls to driver callbacks.
| User Action | Driver Function |
|---|---|
| open() | dev_open() |
| read() | dev_read() |
| close() | dev_release() |
Kernel memory cannot be accessed directly from user space.
Instead, Linux provides helper APIs such as:
copy_to_user(buf, msg_data, len);
This function:
Validates user-space addresses
Handles page faults safely
Prevents invalid memory access
It is the standard mechanism for transferring data from kernel space to user space.
The driver implements a simple read-once mechanism.
The key logic is:
if (*ppos > 0)
return 0;
After the first successful read:
File position advances
Subsequent reads return EOF
Behavior mimics many virtual kernel files
This pattern is commonly used in educational and demonstration drivers.
Kernel APIs evolve over time.
A driver that compiles on one kernel version may fail on another if APIs change.
To support multiple kernels, the module uses:
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6,4,0)
For newer kernels:
class_create("mydevice_class");
For older kernels:
class_create(
THIS_MODULE,
"mydevice_class"
);
This allows a single codebase to compile across multiple Linux releases.
| API | Purpose |
|---|---|
| register_chrdev() | Register character device |
| unregister_chrdev() | Remove character device |
| class_create() | Create device class |
| device_create() | Create device node |
| copy_to_user() | Transfer data to user space |
| DEFINE_MUTEX() | Declare mutex |
| mutex_lock_interruptible() | Acquire mutex |
| mutex_unlock() | Release mutex |
This small module covers several important kernel-development concepts:
Character device registration
Dynamic device creation
User-space interaction
Mutex-based synchronization
Concurrency protection
Read-once semantics
Cross-version kernel compatibility
Although simple, these concepts appear repeatedly in production Linux drivers.
Character device drivers provide an excellent introduction to Linux kernel development. Even a small driver can expose important topics such as synchronization, concurrency, memory safety, and API compatibility.
By combining mutex protection with a clean character-device interface, this example demonstrates how Linux drivers safely interact with user-space applications while maintaining correctness under concurrent access.
GitHub Repository: 👉 linux_kernel_con_c_1 Explore the complete source code, build files, and module implementation on GitHub.