4. 并发控制之自旋锁
任务目标
在Linux内核驱动中,使用自旋锁保护共享资源(全局计数器),防止多线程并发访问导致的数据竞争。通过编写用户态多线程测试程序,验证自旋锁确实保证了数据一致性。
环境依赖
- Linux内核源码树(用于编译内核模块)
- 内核头文件包(
linux-headers-$(uname -r)) - GCC及交叉编译工具链(若目标平台非本机)
- 测试平台:x86_64或ARM(本案例代码与架构无关)
核心实现
驱动模块定义了一个全局计数器和一个自旋锁。设备节点每次被打开时,在open回调中执行1000次临界区操作(读-改-写)。若不加锁,多线程并发打开时计数器会丢失更新;加锁后结果应精确等于打开次数 × 1000。
// spinlock_demo.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/spinlock.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define DEV_NAME "spinlock_demo"
#define LOOP_COUNT 1000
static dev_t dev_num;
static struct cdev demo_cdev;
static struct class *demo_class;
static spinlock_t counter_lock;
static unsigned long counter;
static int demo_open(struct inode *inode, struct file *filp)
{
int i;
unsigned long flags;
for (i = 0; i < LOOP_COUNT; i++) {
spin_lock_irqsave(&counter_lock, flags);
counter++; // 临界区:读-改-写
spin_unlock_irqrestore(&counter_lock, flags);
}
return 0;
}
static ssize_t demo_read(struct file *filp, char __user *buf,
size_t len, loff_t *off)
{
char kbuf[64];
int n;
n = snprintf(kbuf, sizeof(kbuf), "%lu\n", counter);
if (n < 0 || n >= sizeof(kbuf))
return -EFAULT;
if (copy_to_user(buf, kbuf, n))
return -EFAULT;
return n;
}
static struct file_operations demo_fops = {
.owner = THIS_MODULE,
.open = demo_open,
.read = demo_read,
};
static int __init demo_init(void)
{
int ret;
spin_lock_init(&counter_lock);
counter = 0;
ret = alloc_chrdev_region(&dev_num, 0, 1, DEV_NAME);
if (ret < 0)
return ret;
cdev_init(&demo_cdev, &demo_fops);
ret = cdev_add(&demo_cdev, dev_num, 1);
if (ret < 0)
goto err_cdev;
demo_class = class_create(DEV_NAME);
if (IS_ERR(demo_class)) {
ret = PTR_ERR(demo_class);
goto err_class;
}
device_create(demo_class, NULL, dev_num, NULL, DEV_NAME);
return 0;
err_class:
cdev_del(&demo_cdev);
err_cdev:
unregister_chrdev_region(dev_num, 1);
return ret;
}
static void __exit demo_exit(void)
{
device_destroy(demo_class, dev_num);
class_destroy(demo_class);
cdev_del(&demo_cdev);
unregister_chrdev_region(dev_num, 1);
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Spinlock concurrency control demo");
编译与加载
# Makefile
obj-m := spinlock_demo.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
make
sudo insmod spinlock_demo.ko
sudo chmod 666 /dev/spinlock_demo
多线程测试程序
// test_spinlock.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <fcntl.h>
#include <unistd.h>
#define THREADS 8
#define DEV_PATH "/dev/spinlock_demo"
void *worker(void *arg)
{
int fd = open(DEV_PATH, O_RDONLY);
if (fd < 0) {
perror("open");
return NULL;
}
close(fd); // open 回调中执行 LOOP_COUNT 次自旋锁保护的自增
return NULL;
}
int main(void)
{
pthread_t tids[THREADS];
char buf[64];
int fd, i;
unsigned long expected = THREADS * 1000UL;
unsigned long actual;
for (i = 0; i < THREADS; i++)
pthread_create(&tids[i], NULL, worker, NULL);
for (i = 0; i < THREADS; i++)
pthread_join(tids[i], NULL);
fd = open(DEV_PATH, O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
read(fd, buf, sizeof(buf));
close(fd);
actual = strtoul(buf, NULL, 10);
printf("expected = %lu, actual = %lu\n", expected, actual);
return (actual == expected) ? 0 : 1;
}
编译测试程序:gcc -o test_spinlock test_spinlock.c -lpthread
验证方法
运行./test_spinlock,观察输出。若输出expected = 8000, actual = 8000且程序退出码为0,说明自旋锁正确保护了共享计数器,无数据丢失。可尝试将驱动中spin_lock_irqsave/spin_unlock_irqrestore注释掉重新编译加载,再运行测试,此时actual通常会小于8000,以此对比验证锁的必要性。
常见问题
- 模块加载失败(
Operation not permitted):确认内核开启了模块加载权限,或使用sudo。 - 测试程序退出码非0:先确认驱动已加载且设备节点权限正确(
chmod 666)。若权限无误仍失败,检查是否误改了驱动代码。 - 为什么用
irqsave变体:本案例中open回调可能被中断上下文打断,使用irqsave保存并屏蔽本地中断,防止死锁。