【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标准字符设备驱动框架主要包含以下部分设备结构体定义文件操作集open/release/write驱动入口与出口函数自旋锁保护机制核心重点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; } 自旋锁工作原理详解spin_lock(gpioled.lock)获取自旋锁。如果锁当前未被持有则立即获取成功继续执行。如果锁已被其他执行路径持有则原地自旋等待忙等待直到锁被释放。临界区Critical Sectionif (gpioled.dev_status) { ... }和gpioled.dev_status这部分代码是临界区。临界区内的代码访问了共享资源dev_status必须保证原子性即同一时刻只能有一个执行路径进入。为什么需要自旋锁假设没有自旋锁两个应用程序A和B几乎同时调用openA读取dev_status 0此时发生调度B也读取dev_status 0A和B都认为设备空闲都执行dev_status结果两个应用都成功打开了设备设备状态混乱加上自旋锁后A先获取锁B只能自旋等待A完成状态检查和修改并释放锁后B才能进入临界区此时dev_status已经是1B会返回-EBUSY。spin_unlock(gpioled.lock)释放自旋锁允许其他等待的执行路径获取锁。⚠️重要注意事项自旋锁持有期间不能休眠不能调用可能导致休眠的函数如copy_from_user、kmalloc(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] Cant 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 consolettymxc0,115200 root/dev/mmcblk1p2 rootwait rw # 4. 启动内核 bootz 0x80800000 - 0x830000002. 加载驱动模块insmod spinlock.ko3. 打开终端运行测试APP./spinlock_app /dev/kaydon-gpioled 1此时APP会打开设备并占用约25秒同时点亮LED。在第一个APP运行期间打开第二个终端再次运行测试APP观察结果✅预期结果第二个APP打开设备失败返回Cant open file /dev/kaydon-gpioled错误。❌如果成功打开说明自旋锁没有起作用存在并发问题。等待第一个APP运行结束约25秒后再次在第二个终端运行APP✅预期结果此时设备已被释放第二个APP可以正常打开。验证思路总结 通过让第一个APP长时间占用设备sleep循环我们创造了一个设备被占用的时间窗口。在这个窗口内尝试打开第二个实例如果失败就证明自旋锁成功阻止了并发访问。四、MakefileKERNELDIR : /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 -stdgnu11 export ARCHarm export CROSS_COMPILEarm-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 ARCHarm 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的使用方法自旋锁的本质一种忙等待的锁机制当锁被占用时CPU会原地循环等待直到锁被释放。自旋锁的适用场景保护短时间的临界区操作中断上下文因为中断上下文中不能休眠多核SMP系统中的并发保护自旋锁的基本操作spin_lock_init()初始化自旋锁spin_lock()获取自旋锁spin_unlock()释放自旋锁使用注意事项持有自旋锁期间绝对不能休眠临界区要尽可能短所有退出路径都要释放锁避免死锁验证方法通过两个终端同时运行测试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);