前言
在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 Section):
-
if (gpioled.dev_status) { ... }和gpioled.dev_status++这部分代码是临界区。 -
临界区内的代码访问了共享资源
dev_status,必须保证原子性,即同一时刻只能有一个执行路径进入。
-
-
为什么需要自旋锁?
-
假设没有自旋锁,两个应用程序A和B几乎同时调用
open:-
A读取
dev_status = 0 -
此时发生调度,B也读取
dev_status = 0 -
A和B都认为设备空闲,都执行
dev_status++ -
结果:两个应用都成功打开了设备,设备状态混乱!
-
-
加上自旋锁后,A先获取锁,B只能自旋等待;A完成状态检查和修改并释放锁后,B才能进入临界区,此时
dev_status已经是1,B会返回-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] 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
此时APP会打开设备并占用约25秒,同时点亮LED。
在第一个APP运行期间,打开第二个终端,再次运行测试APP
观察结果:
✅ 预期结果 :第二个APP打开设备失败,返回
Can't open file /dev/kaydon-gpioled错误。❌ 如果成功打开:说明自旋锁没有起作用,存在并发问题。
等待第一个APP运行结束(约25秒)后,再次在第二个终端运行APP
- ✅ 预期结果:此时设备已被释放,第二个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)**的使用方法:
-
自旋锁的本质:一种忙等待的锁机制,当锁被占用时,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");