Linux驱动开发实战(十三):RGB LED驱动并发控制------自旋锁与信号量对比详解
文章目录
- [Linux驱动开发实战(十三):RGB LED驱动并发控制------自旋锁与信号量对比详解](#Linux驱动开发实战(十三):RGB LED驱动并发控制——自旋锁与信号量对比详解)
- 前言
- 一、生动比喻:理解自旋锁与信号量
- 二、实战代码解析:自旋锁版本
- 三、实战代码解析:信号量版本代码解析
- 总结
前言
在Linux驱动开发中,并发控制是一个核心问题。当多个进程同时访问同一个硬件设备时,如果没有合理的保护机制,就会出现数据竞争、资源冲突等问题。
想象一下这个场景:
- 进程A正在控制RGB LED显示红色
- 进程B同时也想控制LED显示蓝色
- 没有保护机制的话,LED的状态会混乱
本文将通过一个RGB LED驱动实例,用最通俗易懂的方式对比讲解Linux内核中两种最常用的并发控制机制:
自旋锁(Spinlock) - 忙等待机制
信号量(Semaphore) - 睡眠等待机制
一、生动比喻:理解自旋锁与信号量
单人卫生间问题
假设公司只有一个卫生间(共享资源),多个员工(进程/线程)需要使用它。如何保证同一时刻只有一个人在使用?
自旋锁:门口原地转圈等
小明的做法(自旋锁方式):
小明急需使用卫生间,走到门口
如果门锁着(资源被占用):
站在门口,疯狂拧门把手
"能开吗?不能!"
"能开吗?不能!"
"能开吗?不能!"
... 不停尝试 ...
一旦门开了,立刻冲进去!
如果门开着(资源空闲):
直接进去,反锁门
特点分析:
✅ 反应超快:门一开,立刻进去,零延迟
✅ 适合急事:如果前面的人很快出来(1-2秒),这种方式最高效
❌ 消耗体力:一直在门口转圈,不停拧门把手,累得满头大汗
❌ 不适合长时间:如果前面的人蹲半小时,小明会虚脱
内核对应:
c
spin_lock(&lock); // 疯狂拧门把手,直到打开
// 使用卫生间(临界区代码)
spin_unlock(&lock); // 开门出来
信号量:排队休息等
小红的做法(信号量方式):
小红需要使用卫生间,走到门口
如果门锁着(资源被占用):
在旁边的休息室坐下(进入睡眠)
???(什么都不做,睡觉)
前面的人出来后会叫醒她:"小红,轮到你了!"
小红醒来,走进卫生间
如果门开着(资源空闲):
直接进去,反锁门
特点分析:
✅ 节省体力:在休息室睡觉,完全不消耗体力(CPU资源)
✅ 适合长时间:前面的人蹲多久都没关系,反正我在睡觉
❌ 反应慢:需要被叫醒→站起来→走到卫生间(上下文切换开销)
❌ 短时间不划算:如果前面的人1秒就出来,这套流程反而慢
内核对应:
c
down(&sem); // 尝试进入,如果不行就睡觉
// 使用卫生间(临界区代码)
up(&sem); // 开门出来,叫醒下一个人
二、实战代码解析:自旋锁版本
核心数据
c
spinlock_t s_lock; // 自旋锁变量
char flag; // 设备占用标志:0=空闲,非0=占用
为什么需要flag?
- 自旋锁本身只保护临界区,不记录状态
- flag用于记录设备是否被占用
- 自旋锁保护flag变量的读写操作
初始化:驱动加载时
c
static int __init led_platform_driver_init(void)
{
int DriverState;
/* 初始化自旋锁 */
spin_lock_init(&s_lock);
DriverState = platform_driver_register(&led_platform_driver);
printk(KERN_ALERT "\tDriverState is %d\n", DriverState);
return 0;
}
}
打开设备时的自旋锁保护
c
static int led_chr_dev_open(struct inode *inode, struct file *filp)
{
/* 获取自旋锁(原子操作开始) */
spin_lock(&s_lock);
if(flag) {
/* 设备正在被使用,释放锁并返回错误 */
spin_unlock(&s_lock);
printk("\n driver on using! open failed !!!\n");
return -EBUSY; // 返回设备忙错误
}
else {
/* 设备空闲,标记为占用 */
flag++;
}
/* 释放自旋锁(原子操作结束) */
spin_unlock(&s_lock);
printk("\n open form driver \n");
return 0;
}
关闭设备时释放标志
c
static int led_chrdev_release(struct inode *inode, struct file *filp)
{
/* 获取自旋锁 */
spin_lock(&s_lock);
if(flag)
flag--; // 清除占用标志
/* 释放自旋锁 */
spin_unlock(&s_lock);
printk("KERN_ALERT \n finished !!!\n");
return 0;
}

实验现象

如果第二次太快打开就会直接返回打开失败
三、实战代码解析:信号量版本代码解析
关键变量
c
struct semaphore sem; // 定义信号量结构体
信号量初始化
c
static int __init led_platform_driver_init(void)
{
int DriverState;
/* 初始化信号量,初始值为1(二值信号量,相当于互斥锁) */
sema_init(&sem, 1);
DriverState = platform_driver_register(&led_platform_driver);
printk(KERN_ALERT "\tDriverState is %d\n", DriverState);
return 0;
}
说明:sema_init(&sem, 1) 将信号量初始化为1,表示资源可用,只允许一个进程访问。
打开设备时获取信号量
c
static int led_chr_dev_open(struct inode *inode, struct file *filp)
{
/* 获取信号量,如果信号量值为0,进程将睡眠等待 */
down(&sem);
printk("\n open form driver \n");
return 0;
}
关闭设备时释放信号量
c
static int led_chrdev_release(struct inode *inode, struct file *filp)
{
/* 释放信号量,唤醒等待队列中的进程 */
up(&sem);
printk("KERN_ALERT \n finished !!!\n");
return 0;
}
说明:up(&sem) 将信号量值+1,如果有进程在等待,则唤醒它。

实验现象

在上一个命令没执行完就执行下一个命令的话(很急)
要等待上一个没执行完的命令执行完再执行下一个命令(别急)
总结
信号量:像银行取号,拿到号就等着,轮到你自然会叫你
自旋锁:像查快递柜,有就拿,没有就走,下次再来看
适用场景
- 信号量:文件I/O、网络、设备独占、长临界区
- 自旋锁:中断处理、计数器、状态检查、短临界区
- 持有自旋锁时绝不能睡眠
- 中断上下文绝不能用信号量
- 自旋锁临界区要尽可能短
- 不确定时优先选择信号量(更安全)