This article is a continuation of the Series on Linux Device Drivers and carries the discussion on Linux device drivers and their implementation. The aim of this series is to provide easy and practical examples that anyone can understand. This is the Spinlock in Linux Kernel Part 1 – Linux Device Driver Tutorial Part 23.
You can also read mutex, read-write spinlock, seqlock, interrupts, Procfs, Workqueue, Completion, Softirq, and threaded IRQ in the Linux device driver.
Prerequisites
In the example section, I used Kthread to explain Spinlock. If you don’t know what is Kthread and How to use it, then I would recommend you to explore that by using the below link.
Introduction
In our previous tutorial, we have understood the use of Mutex and its Implementation. If you have understood Mutex then Spinlock is also similar. Both are used to protect a shared resource from being modified by two or more processes simultaneously.
SpinLock
In the Mutex concept, when the thread is trying to lock or acquire the Mutex which is not available then that thread will go to sleep until that Mutex is available. Whereas in Spinlock it is different. The spinlock is a very simple single-holder lock. If a process attempts to acquire a spinlock and it is unavailable, the process will keep trying (spinning) until it can acquire the lock. This simplicity creates a small and fast lock.
Like Mutex, there are two possible states in Spinlock: Locked or Unlocked.
SpinLock in Linux Kernel Device Driver
If the kernel is running on a uniprocessor and CONFIG_SMP
, CONFIG_PREEMPT
aren’t enabled while compiling the kernel then spinlock will not be available. Because there is no reason to have a lock when no one else can run at the same time.
But if you have disabled CONFIG_SMP
and enabled CONFIG_PREEMPT
then spinlock will simply disable preemption, which is sufficient to prevent any races.
Initialize
We can initialize Spinlock in Linux kernel in two ways.
- Static Method
- Dynamic Method
Static Method
You can statically initialize a Spinlock using the macro given below.
DEFINE_SPINLOCK(etx_spinlock);
The macro given above will create a spinlock_t
variable in the name of etx_spinlock
and initialize to UNLOCKED STATE. Take a look at the expansion of DEFINE_SPINLOCK
below.
#define DEFINE_SPINLOCK(x) spinlock_t x = __SPIN_LOCK_UNLOCKED(x)
Dynamic Method
If you want to initialize dynamically you can use the method as given below.
spinlock_t etx_spinlock; spin_lock_init(&etx_spinlock);
You can use any one of the methods.
After initializing the spinlock, there are several ways to use spinlock to lock or unlock, based on where the spinlock is used; either in user context or interrupt context. Let’s look at the approaches to these situations.
Approach 1 (Locking between User context)
If you share data with user context (between Kernel Threads), then you can use this approach.
Lock:
spin_lock(spinlock_t *lock)
This will take the lock if it is free, otherwise, it’ll spin until that lock is free (Keep trying).
Try Lock:
spin_trylock(spinlock_t *lock)
Locks the spinlock if it is not already locked. If unable to obtain the lock it exits with an error and does not spin. It returns non-zero if it obtains the lock otherwise returns zero.
Unlock:
spin_unlock(spinlock_t *lock)
It does the reverse of the lock. It will unlock which is locked by the above call.
Checking Lock:
spin_is_locked(spinlock_t *lock)
This is used to check whether the lock is available or not. It returns non-zero if the lock is currently acquired. otherwise returns zero.
Example
//Thread 1 int thread_function1(void *pv) { while(!kthread_should_stop()) { spin_lock(&etx_spinlock); etx_global_variable++; printk(KERN_INFO "In EmbeTronicX Thread Function1 %lu\n", etx_global_variable); spin_unlock(&etx_spinlock); msleep(1000); } return 0; } //Thread 2 int thread_function2(void *pv) { while(!kthread_should_stop()) { spin_lock(&etx_spinlock); etx_global_variable++; printk(KERN_INFO "In EmbeTronicX Thread Function2 %lu\n", etx_global_variable); spin_unlock(&etx_spinlock); msleep(1000); } return 0; }
Approach 2 (Locking between Bottom Halves)
If you want to share data between two different Bottom halves or the same bottom halves, then you can use Approach 1.
Approach 3 (Locking between User context and Bottom Halves)
If you share data with a bottom half and user context (like Kernel Thread), then this approach will be useful.
Lock:
spin_lock_bh(spinlock_t *lock)
It disables soft interrupts on that CPU, then grabs the lock. This has the effect of preventing softirqs, tasklets, and bottom halves from running on the local CPU. Here the suffix ‘_bh
‘ refers to “Bottom Halves“.
Unlock:
spin_unlock_bh(spinlock_t *lock)
It will release the lock and re-enables the soft interrupts which are disabled by the above call.
Example
//Thread int thread_function(void *pv) { while(!kthread_should_stop()) { spin_lock_bh(&etx_spinlock); etx_global_variable++; printk(KERN_INFO "In EmbeTronicX Thread Function %lu\n", etx_global_variable); spin_unlock_bh(&etx_spinlock); msleep(1000); } return 0; } /*Tasklet Function*/ void tasklet_fn(unsigned long arg) { spin_lock_bh(&etx_spinlock); etx_global_variable++; printk(KERN_INFO "Executing Tasklet Function : %lu\n", etx_global_variable); spin_unlock_bh(&etx_spinlock); }
Approach 4 (Locking between Hard IRQ and Bottom Halves)
If you share data between Hardware ISR and Bottom halves then you have to disable the IRQ before locking. Because the bottom halves processing can be interrupted by a hardware interrupt. So this will be used in that scenario.
Lock:
spin_lock_irq(spinlock_t *lock)
This will disable interrupts on that CPU, then grab the lock.
Unlock:
spin_unlock_irq(spinlock_t *lock)
It will release the lock and re-enables the interrupts which are disabled by the above call.
Example
/*Tasklet Function*/ void tasklet_fn(unsigned long arg) { spin_lock_irq(&etx_spinlock); etx_global_variable++; printk(KERN_INFO "Executing Tasklet Function : %lu\n", etx_global_variable); spin_unlock_irq(&etx_spinlock); } //Interrupt handler for IRQ 11. static irqreturn_t irq_handler(int irq,void *dev_id) { spin_lock_irq(&etx_spinlock); etx_global_variable++; printk(KERN_INFO "Executing ISR Function : %lu\n", etx_global_variable); spin_unlock_irq(&etx_spinlock); /*Scheduling Task to Tasklet*/ tasklet_schedule(tasklet); return IRQ_HANDLED; }
Approach 5 (Alternative way of Approach 4)
If you want to use a different variant rather than using spin_lock_irq()
and spin_unlock_irq()
then you can use this approach.
Lock:
spin_lock_irqsave( spinlock_t *lock, unsigned long flags );
This will save whether interrupts were on or off in a flags
word and grab the lock.
Unlock:
spin_unlock_irqrestore( spinlock_t *lock, unsigned long flags );
This will release the spinlock and restores the interrupts using the flags
argument.
Approach 6 (Locking between Hard IRQs)
If you want to share data between two different IRQs, then you should use Approach 5.
Spinlock in Linux kernel example programming
This code snippet explains how to create two threads that access a global variable (etx_gloabl_variable
). So before accessing the variable, it should lock the spinlock. After that, it will release the spinlock. This example is using Approach 1.
Driver Source Code
[Get the source from GitHub]
/****************************************************************************//** * \file driver.c * * \details Simple linux driver (Spinlock) * * \author EmbeTronicX * * \Tested with Linux raspberrypi 5.10.27-v7l-embetronicx-custom+ * *******************************************************************************/ #include <linux/kernel.h> #include <linux/init.h> #include <linux/module.h> #include <linux/kdev_t.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/device.h> #include<linux/slab.h> //kmalloc() #include<linux/uaccess.h> //copy_to/from_user() #include <linux/kthread.h> //kernel threads #include <linux/sched.h> //task_struct #include <linux/delay.h> #include <linux/err.h> DEFINE_SPINLOCK(etx_spinlock); //spinlock_t etx_spinlock; unsigned long etx_global_variable = 0; dev_t dev = 0; static struct class *dev_class; static struct cdev etx_cdev; static int __init etx_driver_init(void); static void __exit etx_driver_exit(void); static struct task_struct *etx_thread1; static struct task_struct *etx_thread2; /*************** Driver functions **********************/ static int etx_open(struct inode *inode, struct file *file); static int etx_release(struct inode *inode, struct file *file); static ssize_t etx_read(struct file *filp, char __user *buf, size_t len,loff_t * off); static ssize_t etx_write(struct file *filp, const char *buf, size_t len, loff_t * off); /******************************************************/ int thread_function1(void *pv); int thread_function2(void *pv); /* ** thread function 1 */ int thread_function1(void *pv) { while(!kthread_should_stop()) { if(!spin_is_locked(&etx_spinlock)) { pr_info("Spinlock is not locked in Thread Function1\n"); } spin_lock(&etx_spinlock); if(spin_is_locked(&etx_spinlock)) { pr_info("Spinlock is locked in Thread Function1\n"); } etx_global_variable++; pr_info("In EmbeTronicX Thread Function1 %lu\n", etx_global_variable); spin_unlock(&etx_spinlock); msleep(1000); } return 0; } /* ** thread function 2 */ int thread_function2(void *pv) { while(!kthread_should_stop()) { spin_lock(&etx_spinlock); etx_global_variable++; pr_info("In EmbeTronicX Thread Function2 %lu\n", etx_global_variable); spin_unlock(&etx_spinlock); msleep(1000); } return 0; } //File operation structure static struct file_operations fops = { .owner = THIS_MODULE, .read = etx_read, .write = etx_write, .open = etx_open, .release = etx_release, }; /* ** This function will be called when we open the Device file */ static int etx_open(struct inode *inode, struct file *file) { pr_info("Device File Opened...!!!\n"); return 0; } /* ** This function will be called when we close the Device file */ static int etx_release(struct inode *inode, struct file *file) { pr_info("Device File Closed...!!!\n"); return 0; } /* ** This function will be called when we read the Device file */ static ssize_t etx_read(struct file *filp, char __user *buf, size_t len, loff_t *off) { pr_info("Read function\n"); return 0; } /* ** This function will be called when we write the Device file */ static ssize_t etx_write(struct file *filp, const char __user *buf, size_t len, loff_t *off) { pr_info("Write Function\n"); return len; } /* ** Module Init function */ static int __init etx_driver_init(void) { /*Allocating Major number*/ if((alloc_chrdev_region(&dev, 0, 1, "etx_Dev")) <0){ pr_info("Cannot allocate major number\n"); return -1; } pr_info("Major = %d Minor = %d \n",MAJOR(dev), MINOR(dev)); /*Creating cdev structure*/ cdev_init(&etx_cdev,&fops); /*Adding character device to the system*/ if((cdev_add(&etx_cdev,dev,1)) < 0){ pr_info("Cannot add the device to the system\n"); goto r_class; } /*Creating struct class*/ if(IS_ERR(dev_class = class_create(THIS_MODULE,"etx_class"))){ pr_info("Cannot create the struct class\n"); goto r_class; } /*Creating device*/ if(IS_ERR(device_create(dev_class,NULL,dev,NULL,"etx_device"))){ pr_info("Cannot create the Device \n"); goto r_device; } /* Creating Thread 1 */ etx_thread1 = kthread_run(thread_function1,NULL,"eTx Thread1"); if(etx_thread1) { pr_err("Kthread1 Created Successfully...\n"); } else { pr_err("Cannot create kthread1\n"); goto r_device; } /* Creating Thread 2 */ etx_thread2 = kthread_run(thread_function2,NULL,"eTx Thread2"); if(etx_thread2) { pr_err("Kthread2 Created Successfully...\n"); } else { pr_err("Cannot create kthread2\n"); goto r_device; } //spin_lock_init(&etx_spinlock); pr_info("Device Driver Insert...Done!!!\n"); return 0; r_device: class_destroy(dev_class); r_class: unregister_chrdev_region(dev,1); cdev_del(&etx_cdev); return -1; } /* ** Module exit function */ static void __exit etx_driver_exit(void) { kthread_stop(etx_thread1); kthread_stop(etx_thread2); device_destroy(dev_class,dev); class_destroy(dev_class); cdev_del(&etx_cdev); unregister_chrdev_region(dev, 1); pr_info("Device Driver Remove...Done!!\n"); } module_init(etx_driver_init); module_exit(etx_driver_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("EmbeTronicX <[email protected]>"); MODULE_DESCRIPTION("A simple device driver - Spinlock"); MODULE_VERSION("1.18");
MakeFile
obj-m += driver.o KDIR = /lib/modules/$(shell uname -r)/build all: make -C $(KDIR) M=$(shell pwd) modules clean: make -C $(KDIR) M=$(shell pwd) clean
In this example, thread_function1
and thread_function2
are synchronized using the spinlock.
In our next part of the tutorial, we will see another variant of the spinlock (Reader/writer spinlocks).
Please find the other Linux device driver tutorials here.
You can also read the below tutorials.
Embedded Software | Firmware | Linux Devic Deriver | RTOS
Hi, I’m SLR. I am a tech blogger and an Embedded Engineer. I am always eager to learn and explore tech-related stuff! also, I wanted to deliver you the same as much in a more straightforward way with more informative content. I generally appreciate learning by doing, rather than only learning. If you want to help support me on my journey, consider sharing my articles, or Buy me a Coffee! Thank you for reading my blog! Happy learning!