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

Introduction
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.
Concept 1: Mutex Synchronization
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);
Why Use a 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.
Concept 2: Concurrency in Device Drivers
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.
Concept 3: Character Device Drivers
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.
File Operations Interface
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() |
Safe User-Space Communication
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.
Read-Once Device Behavior
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.
Concept 4: Linux Version Compatibility
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.
Important Kernel APIs
| 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 |
What This Project Demonstrates
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.
Conclusion
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.
Source Code
GitHub Repository: 👉 linux_kernel_con_c_1 Explore the complete source code, build files, and module implementation on GitHub.



