Introduction
Welcome back! In our previous lesson, we established the theoretical foundation for kernel synchronization by comparing spinlocks, mutexes, and semaphores. We focused on their core behaviors—spinning versus sleeping—and the critical importance of execution context in choosing the right primitive. You learned why a mutex is illegal in an interrupt handler and why a spinlock is necessary.
This lesson is all about putting that theory into practice. Your goal is to implement a kernel module that correctly synchronizes access to shared data using appropriate locking primitives. We will move from conceptual understanding to hands-on coding, a crucial step for both contributing to kernel projects and mastering technical interviews.
We will:
- Write a kernel module with multiple threads that create a race condition.
- Solve the race condition first with a spinlock.
- Adapt the module to use a mutex.
- Apply your knowledge to a practical exercise: synchronizing access to a shared buffer in a character device.
Let's get started.
1. Identifying the Critical Section
Before we can apply a lock, we must identify what we need to protect. The portion of code that accesses a shared resource and must not be concurrently executed by multiple threads is called a critical section. Any failure to protect a critical section can lead to a race condition.
Consider a driver that accesses a shared global array. The period during which that array is being read from or written to constitutes the critical section.

Our first task will be to create a simple scenario with a critical section and then protect it.
2. Implementing Synchronization with a Spinlock
Spinlocks are the most basic and high-performance lock, designed for very short critical sections. As we discussed, they are essential in atomic contexts because they don't sleep. Let's build a module where two kernel threads attempt to increment a shared counter.
Spinlock API
The core spinlock API is straightforward. To learn about it, let's turn to a reference article that describes the essential functions.
The Linux Kernel Locking API and Shared Objects | by Packt - Medium
The article 'The Linux Kernel Locking API and Shared Objects' provides a clear explanation of the spinlock API. We'll focus on the functions for initialization, locking, and unlocking.
Please read the section titled 'Spinlocks'. Pay close attention to: The difference between static (DEFINE_SPINLOCK) and dynamic (spin_lock_init) initialization. The basic spin_lock() and spin_unlock() functions. The explanation of the deadlock problem with interrupts and the introduction of the spin_lock_irqsave() variant.
Hands-On Example: Protecting a Counter
Now, let's write the code. We will create two kernel threads that each loop, lock a spinlock, increment a global counter, unlock the spinlock, and then sleep for a short period.
The following code is based on the example from the Linux Kernel Programming slides we've used previously.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/spinlock.h>
// 1. Define the shared data and the spinlock
static unsigned int shared_counter = 0;
static DEFINE_SPINLOCK(my_counter_lock);
static struct task_struct *thread1;
static struct task_struct *thread2;
// 2. The function our kernel threads will execute
static int thread_function(void *data)
{
char *thread_name = (char *)data;
// Loop until the module is being removed
while (!kthread_should_stop()) {
// 3. Enter the critical section
spin_lock(&my_counter_lock);
shared_counter++;
pr_info("[%s] Incremented counter to: %u\n", thread_name, shared_counter);
// 4. Leave the critical section
spin_unlock(&my_counter_lock);
// Sleep to allow the other thread to run
msleep(500);
}
pr_info("[%s] Thread stopping.\n", thread_name);
return 0;
}
// Module initialization
static int __init sync_module_init(void)
{
pr_info("Spinlock example: Initializing module.\n");
// 5. Create and start the kernel threads
thread1 = kthread_run(thread_function, "Thread A", "thread_a");
if (IS_ERR(thread1)) {
pr_err("Failed to create thread1\n");
return PTR_ERR(thread1);
}
thread2 = kthread_run(thread_function, "Thread B", "thread_b");
if (IS_ERR(thread2)) {
pr_err("Failed to create thread2\n");
kthread_stop(thread1);
return PTR_ERR(thread2);
}
return 0;
}
// Module cleanup
static void __exit sync_module_exit(void)
{
pr_info("Spinlock example: Exiting module.\n");
// 6. Stop the threads
kthread_stop(thread1);
kthread_stop(thread2);
}
module_init(sync_module_init);
module_exit(sync_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple kernel module demonstrating spinlock usage.");
To Do:
- Save the code above as
spinlock_example.c. - Create a
Makefile:obj-m += spinlock_example.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 - Compile with
make. - Load the module:
sudo insmod spinlock_example.ko - Observe the output:
dmesg -w - Unload the module:
sudo rmmod spinlock_example
You will see output where the counter is incremented sequentially without any lost updates, demonstrating that the lock is working correctly.
3. Implementing Synchronization with a Mutex
Now, let's solve the same problem using a mutex. As a sleeping lock, a mutex is generally preferred in process context (like our kernel threads) if the critical section might be long or involve operations that could sleep.
Mutex API
The mutex API is conceptually similar to the spinlock API but is designed for sleeping locks.
The Linux Kernel Locking API and Shared Objects | by Packt - Medium
Let's return to the 'The Linux Kernel Locking API' article to review the mutex API.
Read the section 'Mutexes'. Focus on: The behavior on contention (sleeping, not spinning). Static (DEFINE_MUTEX) and dynamic (mutex_init) initialization. The locking functions, especially mutex_lock() and mutex_lock_interruptible(). The strict rules for mutex usage (e.g., only the owner can unlock).
Hands-On Video Demonstration
Watching an expert code this can be very instructive. The following video walks through the exact process of adding a mutex to a multi-threaded kernel module.
Let's code a Linux Driver - 28: Mutex exclusive access to shared resources
This video from the 'Johannes 4GNU_Linux' channel provides a practical, step-by-step guide to using a mutex in a kernel module to protect a resource between two threads.
Watch the following segments: Intro to Mutexes (00:13 - 01:38): A quick refresher on why mutexes are needed. Coding the Mutex (02:35 - 06:22): This is the core of the video. Observe how the struct mutex is declared and initialized, and how mutex_lock() and mutex_unlock() are used to wrap the critical section. Testing and Analysis (06:22 - 08:15): The presenter loads the module and analyzes the kernel log (dmesg) to show how the mutex ensures sequential access. Notice how one thread has to wait for the other to leave the critical section.
Adapting Our Code for Mutexes
Inspired by the video, you can now adapt our previous spinlock_example.c. The changes are minimal:
- Include
<linux/mutex.h>. - Replace
DEFINE_SPINLOCK(my_counter_lock);withstatic DEFINE_MUTEX(my_counter_mutex);. - Replace
spin_lock(&my_counter_lock);withmutex_lock(&my_counter_mutex);. - Replace
spin_unlock(&my_counter_lock);withmutex_unlock(&my_counter_mutex);.
The rest of the logic remains the same. The functional outcome will be identical—the counter is protected—but the mechanism is different. If the lock is contended, the thread will sleep instead of spin, which is more efficient for the system as a whole.
4. Practical Exercise: Synchronizing a Character Device
This is your main task for this lesson. You will create a simple character device that acts as a shared log buffer.
Scenario:
- The driver will have a fixed-size global buffer (e.g.,
char shared_buffer[256];). - When a user writes to the device (
write(fd, ...)), the data is appended toshared_buffer. - When a user reads from the device (
read(fd, ...)), the content ofshared_bufferis returned, and the buffer is cleared.
The Problem:
If one process is writing to the buffer while another is reading from it, a race condition can occur. The reading process might get a partially written message, or the writer might corrupt the buffer while it's being read.
Your Task:
- Implement a character device with
readandwritefile operations. - Identify the critical sections in your
readandwritefunctions. - Choose the appropriate locking primitive to protect the
shared_buffer. - Implement the locking to make your driver safe for concurrent access.
Hints:
- Since
readandwritesystem calls operate in process context and can potentially sleep (due tocopy_from_user/copy_to_user), a mutex is the appropriate choice. - You will need to define a mutex, initialize it in your driver's
initfunction, and usemutex_lock/mutex_unlockin yourreadandwritefunctions.
Click here for skeleton code to get started.
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/mutex.h>
#define DEVICE_NAME "syncdemo"
#define BUF_LEN 256
// --- TODO: Define your mutex here ---
static char shared_buffer[BUF_LEN];
static int major_number;
static struct cdev my_cdev;
static struct class *my_class;
static ssize_t dev_read(struct file *filep, char *buffer, size_t len, loff_t *offset) {
// --- TODO: Acquire the lock ---
// Critical Section
int bytes_read = simple_read_from_buffer(buffer, len, offset, shared_buffer, strlen(shared_buffer));
if (bytes_read > 0) {
memset(shared_buffer, 0, BUF_LEN); // Clear buffer after read
}
// --- TODO: Release the lock ---
return bytes_read;
}
static ssize_t dev_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) {
// --- TODO: Acquire the lock ---
// Critical Section
if (len >= BUF_LEN) {
pr_warn("Data too long for buffer\n");
// --- TODO: Release the lock before returning ---
return -EINVAL;
}
if (copy_from_user(shared_buffer, buffer, len)) {
// --- TODO: Release the lock before returning ---
return -EFAULT;
}
shared_buffer[len] = '\0'; // Null-terminate
// --- TODO: Release the lock ---
return len;
}
static const struct file_operations fops = {
.read = dev_read,
.write = dev_write,
};
static int __init sync_char_init(void) {
alloc_chrdev_region(&major_number, 0, 1, DEVICE_NAME);
cdev_init(&my_cdev, &fops);
cdev_add(&my_cdev, major_number, 1);
my_class = class_create(THIS_MODULE, DEVICE_NAME);
device_create(my_class, NULL, major_number, NULL, DEVICE_NAME);
// --- TODO: Initialize your mutex here ---
pr_info("Sync Char Device Initialized\n");
return 0;
}
static void __exit sync_char_exit(void) {
device_destroy(my_class, major_number);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(major_number, 1);
pr_info("Sync Char Device Exited\n");
}
module_init(sync_char_init);
module_exit(sync_char_exit);
MODULE_LICENSE("GPL");
Click here for the complete solution.
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/mutex.h>
#define DEVICE_NAME "syncdemo"
#define BUF_LEN 256
// 1. Define the mutex
static DEFINE_MUTEX(buffer_mutex);
static char shared_buffer[BUF_LEN];
static int major_number;
static struct cdev my_cdev;
static struct class *my_class;
static ssize_t dev_read(struct file *filep, char *buffer, size_t len, loff_t *offset) {
int bytes_read;
// 2. Acquire the lock
if (mutex_lock_interruptible(&buffer_mutex)) {
return -ERESTARTSYS;
}
// Critical Section
bytes_read = simple_read_from_buffer(buffer, len, offset, shared_buffer, strlen(shared_buffer));
if (bytes_read > 0) {
memset(shared_buffer, 0, BUF_LEN); // Clear buffer after read
}
// 3. Release the lock
mutex_unlock(&buffer_mutex);
return bytes_read;
}
static ssize_t dev_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) {
// 2. Acquire the lock
if (mutex_lock_interruptible(&buffer_mutex)) {
return -ERESTARTSYS;
}
// Critical Section
if (len >= BUF_LEN) {
pr_warn("Data too long for buffer\n");
mutex_unlock(&buffer_mutex); // Release before returning
return -EINVAL;
}
if (copy_from_user(shared_buffer, buffer, len)) {
mutex_unlock(&buffer_mutex); // Release before returning
return -EFAULT;
}
shared_buffer[len] = '\0'; // Null-terminate
// 3. Release the lock
mutex_unlock(&buffer_mutex);
return len;
}
static const struct file_operations fops = {
.read = dev_read,
.write = dev_write,
};
static int __init sync_char_init(void) {
// No need to initialize a statically defined mutex.
// If we had a `struct mutex my_mutex;` pointer in a struct,
// we would call `mutex_init(&my_struct->my_mutex);` here.
alloc_chrdev_region(&major_number, 0, 1, DEVICE_NAME);
cdev_init(&my_cdev, &fops);
cdev_add(&my_cdev, major_number, 1);
my_class = class_create(THIS_MODULE, DEVICE_NAME);
device_create(my_class, NULL, major_number, NULL, DEVICE_NAME);
pr_info("Sync Char Device Initialized\n");
return 0;
}
static void __exit sync_char_exit(void) {
device_destroy(my_class, major_number);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(major_number, 1);
pr_info("Sync Char Device Exited\n");
}
module_init(sync_char_init);
module_exit(sync_char_exit);
MODULE_LICENSE("GPL");
Conclusion
In this lesson, you have successfully bridged the gap between theory and practice. You've seen how to use the core APIs for both spinlocks and mutexes and applied them to solve a real-world concurrency problem in a character device driver.
Key Takeaways:
- The spinlock API revolves around
DEFINE_SPINLOCK,spin_lock(), andspin_unlock(). They are used for high-contention, short critical sections, especially in atomic contexts. - The mutex API uses
DEFINE_MUTEX,mutex_lock(), andmutex_unlock(). They are the default choice for synchronization in process context, as they yield the CPU instead of wasting cycles spinning. - Protecting a shared resource involves three steps: identifying the critical section, choosing the correct lock based on the execution context, and correctly placing the lock/unlock calls around the critical section.
Having learned how to use locks, a critical follow-up question is: what happens when you use them incorrectly? Misusing locks can lead to one of the most difficult bugs to diagnose: a deadlock. In our next lesson, we will explore this problem and learn how to diagnose a deadlock scenario in kernel code using the lockdep tool.