【Linux驱动开发】并发与竞争详解——自旋锁实验

前言

在Linux驱动开发中,并发与竞争是一个绕不开的核心话题。当多个进程、多个线程甚至多个CPU同时访问同一个共享资源(如全局变量、设备寄存器、硬件IO等)时,就会产生竞态条件(Race Condition),导致数据不一致、设备状态混乱等严重问题。

Linux内核提供了多种并发控制机制,包括:

  • 原子操作(Atomic Operations)

  • 自旋锁(Spinlock)

  • 信号量(Semaphore)

  • 互斥锁(Mutex)

  • 读写锁(RW Lock)

本文将通过一个GPIO LED驱动 的实际案例,详细讲解**自旋锁(Spinlock)**的使用方法,以及如何通过自旋锁保护设备的共享状态,防止多个应用程序同时打开设备导致的竞争问题。


一、设备树修改(简略)

设备树的修改与普通GPIO LED驱动基本一致,只需在设备树根节点下添加一个gpioled节点,指定LED对应的GPIO引脚即可。

💡 说明 :设备树的详细修改步骤可参考,本文重点在于自旋锁的实现与验证,故设备树部分不做赘述。 【正点原子I.MX6ULL】GPIO LED字符设备驱动开发实战_基于正点原子imx6ull点亮led csdn-CSDN博客

设备树编译后生成.dtb文件,通过U-Boot加载到内核中即可使用。


二、驱动代码编写详解

2.1 整体框架

我们的驱动采用Linux标准字符设备驱动框架,主要包含以下部分:

  1. 设备结构体定义

  2. 文件操作集(open/release/write)

  3. 驱动入口与出口函数

  4. 自旋锁保护机制(核心重点)

2.2 设备结构体
复制代码
/* gpioled设备结构体 */
struct gpioled_dev {
    dev_t devid;                // 设备号
    int major;                  // 主设备号
    int minor;                  // 次设备号
    struct cdev cdev;           // 字符设备
    struct class *class;        // 设备类
    struct device *device;      // 设备
    struct device_node *nd;     // 设备节点
    int led_gpio;               // LED对应的GPIO编号
    int dev_status;             // 设备状态:0表示可用,大于0表示正在使用
    spinlock_t lock;            // 自旋锁:保护dev_status
};

struct gpioled_dev gpioled;     // gpioled设备实例

关键点说明

  • dev_status:这是一个共享资源,用于标记设备是否被占用。0表示设备空闲,大于0表示设备正在被使用。

  • spinlock_t lock:自旋锁,用于保护dev_status这个共享变量的并发访问。

2.3 自旋锁初始化

在驱动入口函数led_init中,首先初始化自旋锁:

复制代码
/* 初始化自旋锁 */
spin_lock_init(&gpioled.lock);
gpioled.dev_status = 0;  // 标记驱动可以使用

spin_lock_init()函数用于动态初始化自旋锁,将锁的状态设置为"未持有"(解锁状态)。

2.4 open函数------自旋锁的核心应用
复制代码
static int gpioled_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &gpioled;

    /* 加锁保护状态 */
    spin_lock(&gpioled.lock);

    if (gpioled.dev_status) {        // 大于0,驱动正在使用
        spin_unlock(&gpioled.lock);  // 先解锁后返回
        return -EBUSY;               // 返回设备忙错误
    }

    gpioled.dev_status++;   // 标记被使用

    /* 解锁 */
    spin_unlock(&gpioled.lock);

    return 0;
}

🔍 自旋锁工作原理详解

  1. spin_lock(&gpioled.lock):获取自旋锁。

    1. 如果锁当前未被持有,则立即获取成功,继续执行。

    2. 如果锁已被其他执行路径持有,则原地自旋等待(忙等待),直到锁被释放。

  2. 临界区(Critical Section)

    1. if (gpioled.dev_status) { ... }gpioled.dev_status++ 这部分代码是临界区

    2. 临界区内的代码访问了共享资源dev_status,必须保证原子性,即同一时刻只能有一个执行路径进入。

  3. 为什么需要自旋锁?

    1. 假设没有自旋锁,两个应用程序A和B几乎同时调用open

      • A读取dev_status = 0

      • 此时发生调度,B也读取dev_status = 0

      • A和B都认为设备空闲,都执行dev_status++

      • 结果:两个应用都成功打开了设备,设备状态混乱!

    2. 加上自旋锁后,A先获取锁,B只能自旋等待;A完成状态检查和修改并释放锁后,B才能进入临界区,此时dev_status已经是1,B会返回-EBUSY

  4. spin_unlock(&gpioled.lock):释放自旋锁,允许其他等待的执行路径获取锁。

⚠️ 重要注意事项

  • 自旋锁持有期间不能休眠 (不能调用可能导致休眠的函数,如copy_from_userkmalloc(GFP_KERNEL)、msleep等)。

  • 自旋锁的临界区要尽可能短,因为自旋等待会浪费CPU资源。

  • 获取锁后必须在所有可能的退出路径上都释放锁,否则会造成死锁。

2.5 release函数------释放设备
复制代码
static int gpioled_release(struct inode *inode, struct file *filp)
{   
    struct gpioled_dev *dev = filp->private_data;

    /* 加锁保护状态 */
    spin_lock(&gpioled.lock);

    if (gpioled.dev_status) {        // 大于0,驱动已使用
        gpioled.dev_status--;        // 释放,标记驱动可以使用
    }

    /* 解锁 */
    spin_unlock(&gpioled.lock);

    return 0;
}

release函数在应用程序close()设备文件时被调用,负责将设备状态重置为可用。同样,对dev_status的修改也需要用自旋锁保护。

2.6 write函数------控制LED亮灭
复制代码
static ssize_t gpioled_write(struct file *filp, const char __user *buf, 
                             size_t count, loff_t *ppos)
{
    int ret = 0;
    unsigned char databuf[1];
    struct gpioled_dev *dev = filp->private_data;

    ret = copy_from_user(databuf, buf, count);
    if (ret != 0) {
        return -EINVAL;
    }

    if (databuf[0] == LEDON) {
        gpio_set_value(dev->led_gpio, 0);  // 低电平点亮
    } else {
        gpio_set_value(dev->led_gpio, 1);  // 高电平熄灭
    }

    return 0;
}

write函数接收用户空间传来的数据,根据值控制LED的亮灭。由于我们在open时已经保证了只有一个应用能打开设备,因此write操作本身是安全的。

2.7 文件操作集
复制代码
static const struct file_operations gpio_fops = {
    .owner = THIS_MODULE,
    .write = gpioled_write,
    .open = gpioled_open,
    .release = gpioled_release,
};
2.8 驱动入口函数
复制代码
static int __init led_init(void)
{
    int ret = 0;

    /* 初始化自旋锁 */
    spin_lock_init(&gpioled.lock);
    gpioled.dev_status = 0;

    /* 注册设备号 */
    gpioled.major = 0;
    if (gpioled.major) {
        gpioled.devid = MKDEV(gpioled.major, 0);
        ret = register_chrdev_region(gpioled.devid, GPIOLED_COUNT, GPIOLED_NAME);
        if (ret < 0) goto err_regchrdev;
    } else {
        ret = alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_COUNT, GPIOLED_NAME);
        if (ret < 0) goto err_allocchrdev;
        gpioled.major = MAJOR(gpioled.devid);
        gpioled.minor = MINOR(gpioled.devid);
    }
    printk("[OK] GPIOLED register_chrdev_region success.\r\n");
    printk("[INFO] GPIOLED major = %d, minor = %d\r\n", gpioled.major, gpioled.minor);

    /* 初始化cdev */
    cdev_init(&gpioled.cdev, &gpio_fops);
    gpioled.cdev.owner = THIS_MODULE;
    ret = cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_COUNT);
    if (ret < 0) goto err_cdev_add;
    printk("[OK] GPIOLED cdev_add success.\r\n");

    /* 注册设备类 */
    gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
    if (IS_ERR(gpioled.class)) goto err_class_create;
    printk("[OK] GPIOLED class_create success.\r\n");

    /* 注册设备 */
    gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);
    if (IS_ERR(gpioled.device)) goto err_device_create;
    printk("[OK] GPIOLED device_create success.\r\n");

    /* 获取设备节点 */
    gpioled.nd = of_find_node_by_path("/gpioled");
    if (gpioled.nd == NULL) goto err_find_node;
    printk("[OK] Find gpioled node success.\r\n");

    /* 获取GPIO编号 */
    gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio_new", 0);
    if (gpioled.led_gpio < 0) goto err_find_gpio;
    printk("[OK] Get GPIOLED gpio num success.\r\n");
    printk("[INFO] GPIOLED gpio num = %d\r\n", gpioled.led_gpio);

    /* 申请GPIO */
    ret = gpio_request(gpioled.led_gpio, "LED-GPIO");
    if (ret) goto err_request_gpio;
    printk("[OK] Request GPIOLED gpio success.\r\n");

    /* 设置GPIO为输出,默认点亮LED */
    ret = gpio_direction_output(gpioled.led_gpio, 1);
    if (ret) goto err_gpio_output;
    gpio_set_value(gpioled.led_gpio, 0);

    return 0;

    /* 错误处理(省略,详见完整代码) */
    ...
}
2.9 驱动出口函数
复制代码
static void __exit led_exit(void)
{
    device_destroy(gpioled.class, gpioled.devid);
    class_destroy(gpioled.class);
    cdev_del(&gpioled.cdev);
    unregister_chrdev_region(gpioled.devid, GPIOLED_COUNT);
    gpio_free(gpioled.led_gpio);
    printk("[OK] GPIOLED exit success.\r\n");
}

三、测试APP验证思路

3.1 验证目标

我们需要验证自旋锁是否真的起到了保护作用,即:

  • 当一个应用程序打开设备后,另一个应用程序是否无法再打开?

  • 当第一个应用程序关闭设备后,第二个应用程序是否又能正常打开?

3.2 测试APP设计
复制代码
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>

#define LED_ON  0
#define LED_OFF 1

int main(int argc, char *argv[])
{   
    if (argc != 3) {
        printf("[ERROR] Usage: ./spinlock_app <filename> <0:1>\r\n");
        return -1;
    }

    const char *filename = argv[1];
    unsigned char databuf[1];

    /* 1. 打开设备 */
    int fd = open(filename, O_RDWR);
    if (fd == -1) {
        printf("[ERROR] Can't open file %s.\r\n", filename);
        return -1;
    }
    printf("[OK] Open device success.\r\n");

    /* 2. 控制LED */
    databuf[0] = atoi(argv[2]);
    int ret = write(fd, databuf, sizeof(databuf));
    if (ret == -1) {
        printf("[ERROR] LED switch failed.\r\n");
        close(fd);
        return -1;
    }
    printf("[OK] LED switch success.\r\n");

    /* 3. 模拟长时间占用驱动(关键!用于验证并发) */
    int cnt = 0;
    while (1) {
        sleep(5);
        cnt++;
        printf("[INFO] App Running times: %d\r\n", cnt);
        if (cnt >= 5) break;  // 占用约25秒后退出
    }

    printf("[INFO] App Running finished.\r\n");
    close(fd);
    return 0;
}
3.3 验证步骤

📝 测试步骤:

1. Uboot临时启动加载测试

复制代码
# 1. 从EMMC BOOT分区加载自定义dtb到内存0x83000000
fatload mmc 1:1 0x83000000 imx6ull-kaydon-emmc.dtb

# 2. 加载内核镜像zImage
fatload mmc 1:1 0x80800000 zImage

# 3. 设置内核启动参数
setenv bootargs console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw

# 4. 启动内核
bootz 0x80800000 - 0x83000000

2. 加载驱动模块

复制代码
insmod spinlock.ko

3. 打开终端,运行测试APP

复制代码
./spinlock_app /dev/kaydon-gpioled 1
  1. 此时APP会打开设备并占用约25秒,同时点亮LED。

  2. 在第一个APP运行期间,打开第二个终端,再次运行测试APP

  3. 观察结果

    1. 预期结果 :第二个APP打开设备失败,返回Can't open file /dev/kaydon-gpioled错误。

    2. 如果成功打开:说明自旋锁没有起作用,存在并发问题。

  4. 等待第一个APP运行结束(约25秒)后,再次在第二个终端运行APP

    1. 预期结果:此时设备已被释放,第二个APP可以正常打开。

💡 验证思路总结: 通过让第一个APP长时间占用设备(sleep循环),我们创造了一个"设备被占用"的时间窗口。在这个窗口内尝试打开第二个实例,如果失败,就证明自旋锁成功阻止了并发访问。


四、Makefile

复制代码
KERNELDIR := /home/kaydon/alientek_linux
DTS_NAME  := imx6ull-kaydon-emmc.dts
DTB_NAME  := $(patsubst %.dts,%.dtb,$(DTS_NAME))
DTS_KERNEL_PATH := $(KERNELDIR)/arch/arm/boot/dts/$(DTS_NAME)
DTB_KERNEL_PATH := $(KERNELDIR)/arch/arm/boot/dts/$(DTB_NAME)
CURRENT_PATH := $(shell pwd)

obj-m := spinlock.o

ccflags-y += -Wno-declaration-after-statement -std=gnu11

export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-

# 编译驱动模块
build: kernel_modules

kernel_modules:
        $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

# 设备树编译
dtbs:
        cp $(CURRENT_PATH)/$(DTS_NAME) $(DTS_KERNEL_PATH)
        cd $(KERNELDIR) && make ARCH=arm dtbs
        cp $(DTB_KERNEL_PATH) $(CURRENT_PATH)/

clean:
        $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
        rm -f $(CURRENT_PATH)/$(DTB_NAME)

五、总结

本文通过一个GPIO LED驱动的实例,详细讲解了Linux内核中**自旋锁(Spinlock)**的使用方法:

  1. 自旋锁的本质:一种忙等待的锁机制,当锁被占用时,CPU会原地循环等待,直到锁被释放。

  2. 自旋锁的适用场景

    1. 保护短时间的临界区操作

    2. 中断上下文(因为中断上下文中不能休眠)

    3. 多核SMP系统中的并发保护

  3. 自旋锁的基本操作

    1. spin_lock_init():初始化自旋锁

    2. spin_lock():获取自旋锁

    3. spin_unlock():释放自旋锁

  4. 使用注意事项

    1. 持有自旋锁期间绝对不能休眠

    2. 临界区要尽可能短

    3. 所有退出路径都要释放锁,避免死锁

  5. 验证方法:通过两个终端同时运行测试APP,验证设备是否能被独占打开。

自旋锁是Linux驱动开发中最基础也是最重要的并发控制机制之一,掌握自旋锁的使用是写出稳定、可靠的Linux驱动的第一步。后续我们还会继续讲解信号量、互斥锁等其他并发控制机制,敬请期待!


完整驱动代码

复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/spinlock.h>

/* 
 * File_Name: spinlock.c 
 * Description: 基于gpio的led驱动程序
 * Author: kaydon
 * Date: 2026-06-30
 */

#define GPIOLED_COUNT   1
#define GPIOLED_NAME    "kaydon-gpioled"
#define LEDOFF          0x01
#define LEDON           0x00

/* gpioled设备结构体 */
struct gpioled_dev {
    dev_t devid;
    int major;
    int minor;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    struct device_node *nd;
    int led_gpio;

    int dev_status;     // 0表示设备可以使用, 大于1表示不可使用
    spinlock_t lock;
};
struct gpioled_dev gpioled; /* gpioled设备实例 */

/* gpioled文件操作结构体 */
static int gpioled_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &gpioled;

    /* 加锁保护状态 */
    spin_lock(&gpioled.lock);
    if (gpioled.dev_status) {        // 大于1, 驱动不能使用
        spin_unlock(&gpioled.lock);  // 先解锁后返回
        return -EBUSY;
    }

    gpioled.dev_status++;   // 标记被使用

    /* 解锁 */
    spin_unlock(&gpioled.lock);

    return 0;
}

static int gpioled_release(struct inode *inode, struct file *filp)
{   
    struct gpioled_dev *dev = filp->private_data;

    /* 加锁保护状态 */
    spin_lock(&gpioled.lock);
    if (gpioled.dev_status) {        // 大于1, 驱动已使用
        gpioled.dev_status--;        // 释放,标记驱动可以使用
    }

    /* 解锁 */
    spin_unlock(&gpioled.lock);

    return 0;
}

static ssize_t gpioled_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
    int ret = 0;
    unsigned char databuf[1];
    struct gpioled_dev *dev = filp->private_data;

    ret = copy_from_user(databuf, buf, count);
    if (ret != 0) {
        return -EINVAL;
    }

    if (databuf[0] == LEDON) {
        gpio_set_value(dev->led_gpio, 0);
    } else {
        gpio_set_value(dev->led_gpio, 1);
    }

    return 0;
}

static const struct file_operations gpio_fops = {
    .owner = THIS_MODULE,
    .write = gpioled_write,
    .open = gpioled_open,
    .release = gpioled_release,
};

/* 驱动入口函数 */
static int __init led_init(void)
{
    int ret = 0;

    /* 初始化自旋锁 */
    spin_lock_init(&gpioled.lock);
    gpioled.dev_status = 0; // 标记驱动可以使用

    /* 注册设备号 */
    gpioled.major = 0;
    if (gpioled.major) {    // 已给定设备号
        gpioled.devid = MKDEV(gpioled.major, 0);
        ret = register_chrdev_region(gpioled.devid, GPIOLED_COUNT, GPIOLED_NAME);
        if (ret < 0) {
            goto err_regchrdev;
        }
    } else {    // 无给定设备号
        ret = alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_COUNT, GPIOLED_NAME);
        if (ret < 0) {
            goto err_allocchrdev;
        }
        gpioled.major = MAJOR(gpioled.devid);
        gpioled.minor = MINOR(gpioled.devid);
    }
    printk("[OK] GPIOLED register_chrdev_region success.\r\n");
    printk("[INFO] GPIOLED major = %d, minor = %d\r\n", gpioled.major, gpioled.minor);

    /* 初始化cdev */
    cdev_init(&gpioled.cdev, &gpio_fops);
    gpioled.cdev.owner = THIS_MODULE;
    ret = cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_COUNT);
    if (ret < 0) {
        goto err_cdev_add;
    }
    printk("[OK] GPIOLED cdev_add success.\r\n");

    /* 注册设备类 */
    gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
    if (IS_ERR(gpioled.class)) {
        goto err_class_create;
    }
    printk("[OK] GPIOLED class_create success.\r\n");

    /* 注册设备 */
    gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);
    if (IS_ERR(gpioled.device)) {
        goto err_device_create;
    }
    printk("[OK] GPIOLED device_create success.\r\n");

    /* 获取设备节点 */
    gpioled.nd = of_find_node_by_path("/gpioled");  // 设备树自己添加的路径
    if (gpioled.nd == NULL) {
        goto err_find_node;
    }
    printk("[OK] Find gpioled node success.\r\n");

    /* 获取GPIOLED所对应的GPIO */
    gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio_new", 0);    // 节点,gpio对应哪个属性名,gpio索引
    if (gpioled.led_gpio < 0) {
        goto err_find_gpio;
    }
    printk("[OK] Get GPIOLED gpio num success.\r\n");
    printk("[INFO] GPIOLED gpio num = %d\r\n", gpioled.led_gpio);

    /* 申请IO */
    ret = gpio_request(gpioled.led_gpio, "LED-GPIO");
    if (ret) {
        goto err_request_gpio;
    }
    printk("[OK] Request GPIOLED gpio success.\r\n");

    /* 使用IO,设置为输出 */
    ret = gpio_direction_output(gpioled.led_gpio, 1);   // 设置哪个gpio,默认高/低电平
    if (ret) {
        goto err_gpio_output;
    }

    /* 输出低电平,点亮LED灯 */
    gpio_set_value(gpioled.led_gpio, 0);    // 设置哪个gpio,设置高/低电平



    return 0;

err_regchrdev:
    printk("[ERROR] GPIOLED register_chrdev_region failed.\r\n");
    return ret;
err_allocchrdev:
    printk("[ERROR] GPIOLED alloc_chrdev_region failed.\r\n");
    return ret;
err_cdev_add:
    printk("[ERROR] GPIOLED cdev_add failed.\r\n");
    return ret;
err_class_create:
    printk("[ERROR] GPIOLED class_create failed.\r\n");
    ret = PTR_ERR(gpioled.class);
    return ret;
err_device_create:
    printk("[ERROR] GPIOLED device_create failed.\r\n");
    ret = PTR_ERR(gpioled.device);
    return ret;
err_find_node:
    printk("[ERROR] Find gpioled node failed.\r\n");
    ret = PTR_ERR(gpioled.nd);
    return ret;
err_find_gpio:
    printk("[ERROR] Find gpio failed.\r\n");
    ret = -EINVAL;
    return ret;
err_request_gpio:
    printk("[ERROR] Request GPIOLED gpio failed.\r\n");
    ret = -EINVAL;
    return ret;
err_gpio_output:
    gpio_free(gpioled.led_gpio);    // 前面申请过gpio,故要释放
    printk("[ERROR] GPIO output failed.\r\n");
    ret = -EINVAL;
    return ret;

}

static void __exit led_exit(void)
{
    /* 注销设备 */
    device_destroy(gpioled.class, gpioled.devid);

    /* 注销设备类 */
    class_destroy(gpioled.class);

    /* 注销cdev */
    cdev_del(&gpioled.cdev);

    /* 注销设备号 */
    unregister_chrdev_region(gpioled.devid, GPIOLED_COUNT);

    /* 复位GPIO */
    gpio_free(gpioled.led_gpio);

    printk("[OK] GPIOLED exit success.\r\n");
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kaydon");