In Linux kernel development, spinlocks are essential synchronization primitives for protecting shared resources in interrupt contexts. This article demonstrates how to implement interrupt-safe spinlocks in a character device driver for controlling GPIO-based LEDs. We'll explore the use of spinlock functions that save and restore interrupt states to insure proper synchronization.
Interrupt-Safe Spinlock APIs
When working with spinlocks in kernel space, it's crucial to handle interrupts properly. The Linux kernel provides specific APIs for spinlock operations that manage interrupt states:
| Function | Description |
|---|---|
| void spin_lock_irqsave(spinlock_t *lock, unsigned long flags) | Saves current interrupt state, disables local interrupts, and acquires the spinlock |
| void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) | Restores interrupt state, re-enables local interrupts, and releases the spinlock |
Driver Implementation
Below is a complete character device driver implementation thatt demonstrates the use of interrupt-safe spinlocks for managing exclusive access to an LED device:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/atomic.h>
#define LED_STATE_OFF 0
#define LED_STATE_ON 1
#define DEV_NAME "led_ctrl"
#define DEV_COUNT 1
struct led_device {
dev_t device_id;
int major_num;
int minor_num;
struct cdev char_dev;
struct class* dev_class;
struct device* device_ptr;
struct device_node* dt_node;
int gpio_pin;
atomic_t usage_count;
spinlock_t access_lock;
};
static struct led_device led_dev;
static int led_driver_open(struct inode *inode, struct file *file_ptr)
{
unsigned long interrupt_state;
spin_lock_irqsave(&led_dev.access_lock, interrupt_state);
if (atomic_read(&led_dev.usage_count) > 0) {
spin_unlock_irqrestore(&led_dev.access_lock, interrupt_state);
return -EBUSY;
}
atomic_inc(&led_dev.usage_count);
spin_unlock_irqrestore(&led_dev.access_lock, interrupt_state);
file_ptr->private_data = &led_dev;
return 0;
}
static ssize_t led_driver_write(struct file *file_ptr, const char __user *user_buf,
size_t count, loff_t *position)
{
int result;
char command_buffer[2] = {0};
result = copy_from_user(command_buffer, user_buf, count);
if (result != 0) {
pr_err("Failed to copy data from user space\n");
return -EFAULT;
}
if (command_buffer[0] == LED_STATE_ON) {
gpio_set_value(led_dev.gpio_pin, 0);
} else if (command_buffer[0] == LED_STATE_OFF) {
gpio_set_value(led_dev.gpio_pin, 1);
}
return count;
}
static int led_driver_release(struct inode *inode, struct file *file_ptr)
{
unsigned long interrupt_state;
struct led_device *dev = file_ptr->private_data;
spin_lock_irqsave(&dev->access_lock, interrupt_state);
if (atomic_read(&dev->usage_count) > 0) {
atomic_dec(&dev->usage_count);
}
spin_unlock_irqrestore(&dev->access_lock, interrupt_state);
return 0;
}
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_driver_open,
.release = led_driver_release,
.write = led_driver_write,
};
static int __init led_driver_init(void)
{
int ret = 0;
atomic_set(&led_dev.usage_count, 0);
spin_lock_init(&led_dev.access_lock);
led_dev.major_num = 0;
if (led_dev.major_num) {
led_dev.device_id = MKDEV(led_dev.major_num, 0);
ret = register_chrdev_region(led_dev.device_id, DEV_COUNT, DEV_NAME);
} else {
ret = alloc_chrdev_region(&led_dev.device_id, 0, DEV_COUNT, DEV_NAME);
led_dev.major_num = MAJOR(led_dev.device_id);
led_dev.minor_num = MINOR(led_dev.device_id);
}
if (ret < 0) {
pr_err("Failed to register device number\n");
return ret;
}
pr_info("LED driver registered with major number %d\n", led_dev.major_num);
led_dev.char_dev.owner = THIS_MODULE;
cdev_init(&led_dev.char_dev, &led_fops);
ret = cdev_add(&led_dev.char_dev, led_dev.device_id, DEV_COUNT);
if (ret < 0) {
pr_err("Failed to add character device\n");
goto err_cdev;
}
led_dev.dev_class = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(led_dev.dev_class)) {
ret = PTR_ERR(led_dev.dev_class);
goto err_class;
}
led_dev.device_ptr = device_create(led_dev.dev_class, NULL,
led_dev.device_id, NULL, DEV_NAME);
if (IS_ERR(led_dev.device_ptr)) {
ret = PTR_ERR(led_dev.device_ptr);
goto err_device;
}
led_dev.dt_node = of_find_node_by_path("/led-controller");
if (!led_dev.dt_node) {
pr_err("Device tree node not found\n");
ret = -ENODEV;
goto err_node;
}
led_dev.gpio_pin = of_get_named_gpio(led_dev.dt_node, "led-gpio", 0);
if (led_dev.gpio_pin < 0) {
pr_err("Failed to get GPIO number\n");
ret = led_dev.gpio_pin;
goto err_gpio;
}
ret = gpio_request(led_dev.gpio_pin, DEV_NAME);
if (ret) {
pr_err("Failed to request GPIO\n");
goto err_gpio;
}
ret = gpio_direction_output(led_dev.gpio_pin, 1);
if (ret) {
pr_err("Failed to set GPIO direction\n");
goto err_direction;
}
return 0;
err_direction:
gpio_free(led_dev.gpio_pin);
err_gpio:
err_node:
device_destroy(led_dev.dev_class, led_dev.device_id);
err_device:
class_destroy(led_dev.dev_class);
err_class:
cdev_del(&led_dev.char_dev);
err_cdev:
unregister_chrdev_region(led_dev.device_id, DEV_COUNT);
return ret;
}
static void __exit led_driver_exit(void)
{
gpio_set_value(led_dev.gpio_pin, 1);
gpio_free(led_dev.gpio_pin);
device_destroy(led_dev.dev_class, led_dev.device_id);
class_destroy(led_dev.dev_class);
cdev_del(&led_dev.char_dev);
unregister_chrdev_region(led_dev.device_id, DEV_COUNT);
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Developer");
MODULE_DESCRIPTION("LED driver with interrupt-safe spinlock implementation");
Compilation and Testing
To compile the driver, use the kernel build system:
$ make
make -C /path/to/kernel/source M=/path/to/driver modules
CC [M] /path/to/driver/led_driver.o
Building modules, stage 2.
MODPOST 1 modules
CC /path/to/driver/led_driver.mod.o
LD [M] /path/to/driver/led_driver.ko
After compilation, load the module and test the exclusive access functionality. The spinlock implementation ensures that only one process can access the LED device at a time, preventing race conditions in interrupt contexts.
The test procedure involves loading the module, attempting to open the device from multiple processes simultaneously, and verifying that only one process succeeds while others receive -EBUSY error.