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,以此对比验证锁的必要性。

用户态 (8线程) 线程 0 线程 1 线程 2 ... 线程 7 open() ×1000次 内核驱动 自旋锁保护 spin_lock_irqsave() counter++ spin_unlock_irqrestore() 共享资源 counter (全局) 验证 read() 读取 counter /dev/spinlock_demo 比较 expected vs actual 8000 == 8000 ? 退出码 0 = 通过 read 无锁时 counter++ 非原子 → 丢失更新;自旋锁保证互斥 irqsave/restore 变体同时保护中断上下文

常见问题

  • 模块加载失败(Operation not permitted):确认内核开启了模块加载权限,或使用sudo。
  • 测试程序退出码非0:先确认驱动已加载且设备节点权限正确(chmod 666)。若权限无误仍失败,检查是否误改了驱动代码。
  • 为什么用irqsave变体:本案例中open回调可能被中断上下文打断,使用irqsave保存并屏蔽本地中断,防止死锁。