11. Linux内核并发与竞争¶
Linux内核竞态的核心根源是并发执行流无保护争抢共享资源,解决竞态的本质是划定临界区、隔离并发访问, 让共享资源在同一时刻仅被一个执行流操作。内核针对不同并发场景,提供了分层级的防护方案,本章将介绍常用的解决方法。
11.1. 并发与竞态问题剖析¶
11.1.1. 并发产生场景¶
Linux内核是抢占式多任务系统,且支持多核CPU并行运算,驱动程序作为内核的一部分,会面临多种并发访问场景:
多进程并发访问:多个用户态进程同时open/write同一个设备节点,争抢硬件资源;
中断上下文抢占:硬件中断触发时,中断服务程序会抢占当前进程上下文,打断正在执行的驱动代码;
多核CPU并行:SMP架构下,不同CPU核心同时执行同一段驱动代码,同时操作寄存器/全局变量;
内核线程抢占:内核线程与用户进程、中断服务程序之间的调度抢占。
11.1.2. 竞态定义与危害¶
竞态是指多个执行单元同时操作共享资源,且操作过程不具备原子性,导致最终执行结果依赖于执行顺序,出现不可预期的错误。
例如在led驱动实验中,进程A执行led打开操作,刚读取寄存器值、未完成写入; 进程B同时抢占执行,读取到旧寄存器值并修改,最终导致引脚配置失效、led亮灭失控。
11.3. 原子操作¶
11.3.1. 原子操作定义¶
原子操作(Atomic Operation)指操作过程不可被中断、不可被分割,要么完整执行完毕,要么完全不执行,不存在中间状态。 Linux内核的原子操作依赖于CPU硬件指令支持(如ARM的LDREX/STREX指令、X86的XCHG指令),保证单指令操作的原子性。
原子操作核心特性:
原子性:操作全程不可拆分、不可被抢占打断,单硬件指令完成读写修改,彻底杜绝竞态中间态。
轻量无阻塞:无需线程睡眠、无需内核调度切换,无额外上下文开销,执行效率远高于自旋锁、信号量等锁机制。
架构无关性:内核封装统一的atomic_t类型与API接口,底层屏蔽ARM、X86等不同CPU的硬件指令差异,驱动代码可跨平台移植。
类型局限性:仅支持整型数据操作(32位atomic_t、64位atomic64_t),无法直接保护复杂结构体、硬件寄存器批量操作等场景。
全上下文兼容:可在进程上下文、中断上下文、软中断等所有内核执行场景中使用,无使用场景限制。
无死锁风险:操作耗时极短、无锁等待逻辑,不存在持有锁后睡眠、嵌套死锁等问题,稳定性极高。
11.3.2. 内核原子类型定义¶
Linux内核针对不同架构封装了统一的原子类型,核心原子整型为atomic_t,定义于内核源码/include/linux/types.h与架构相关头文件,以arm64为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 内核源码/include/linux/types.h
typedef struct {
int counter; // 存储原子整型值
} atomic_t;
// 内核源码/arch/arm64/include/asm/atomic.h
// arm64架构原子操作扩展定义
// 原子变量初始化宏
#define ATOMIC_INIT(i) (i) }
// 原子操作核心依赖硬件指令,保证不可分割性
#define atomic_read(v) READ_ONCE((v)->counter)
#define atomic_set(v, i) WRITE_ONCE(((v)->counter), (i))
|
说明:
atomic_t本质是封装了int类型的结构体,禁止直接操作counter成员,必须通过内核API访问;
不同CPU架构的原子操作实现不同,但内核提供统一API,驱动无需关注底层硬件差异;
原子操作仅支持整型变量(32位/64位),不支持复杂数据结构。
11.3.3. 原子操作解决竞态流程¶
以led驱动多进程write竞态为例,原子操作的互斥逻辑如下:
原子变量作为互斥标记,通过不可分割的比较交换操作,保证同一时间只有一个执行单元能获取设备使用权,彻底杜绝竞态。
11.3.4. 原子操作标准API函数¶
内核提供完善的原子操作专用API,所有API均以atomic_为前缀,仅支持操作atomic_t类型变量, 禁止直接读写结构体内部counter成员,以下为驱动开发常用API,按功能分类详细说明:
11.3.4.1. 原子变量初始化¶
11.3.4.1.1. atomic_set函数¶
atomic_set函数是运行期动态设置原子变量值的核心API,原子性完成变量赋值操作,无中间状态。
函数原型:
1 | void atomic_set(atomic_t *v, int i);
|
参数说明:
v:指向atomic_t类型原子变量的指针,为待赋值的目标变量;
i:要设置的目标整型值,支持正负整数,驱动常用0/1做状态标记。
11.3.4.2. 原子变量读取¶
11.3.4.2.1. atomic_read函数¶
atomic_read函数是原子性读取atomic_t变量当前值的API,避免多核并发下的读取乱序问题。
函数原型:
1 | int atomic_read(const atomic_t *v);
|
参数说明:
v:指向atomic_t类型原子变量的指针,为待读取的目标变量。
返回值:
原子读取到的变量当前整型值,结果可靠无竞态。
11.3.4.3. 原子变量比较交换¶
11.3.4.3.1. atomic_cmpxchg函数¶
atomic_cmpxchg函数是原子比较并交换核心API,实现CAS(Compare-And-Swap)逻辑, 是原子互斥锁的核心实现接口,全程由硬件单指令完成,不可分割。
函数原型:
1 | int atomic_cmpxchg(atomic_t *v, int old, int new);
|
参数说明:
v:指向atomic_t类型原子变量的指针;
old:预期的原子变量旧值,用于对比校验;
new:对比一致后要写入的新值。
返回值:
若原子变量实际值等于old值,返回old,且赋值new成功;
若原子变量实际值不等于old值,返回当前实际值,赋值失败。
11.3.4.4. 原子变量加减¶
11.3.4.4.1. atomic_inc函数¶
atomic_inc函数是原子变量自增1操作API,无返回值,纯计数递增,全程不可中断。
函数原型:
1 | void atomic_inc(atomic_t *v);
|
参数说明:
v:为atomic_t类型原子变量指针。
11.3.4.4.2. atomic_inc_return函数¶
atomic_inc_return函数是原子变量自增1并返回新值API,需获取递增后结果时优先使用。
函数原型:
1 | int atomic_inc_return(atomic_t *v);
|
参数说明:
v:为atomic_t类型原子变量指针。
返回值:
自增完成后的最新整型值。
11.3.4.4.3. atomic_dec函数¶
atomic_dec函数是原子变量自减1操作API,无返回值,纯计数递减,全程不可中断。
函数原型:
1 | void atomic_dec(atomic_t *v);
|
参数说明:
v:atomic_t类型原子变量指针。
11.3.4.4.4. atomic_dec_and_test函数¶
atomic_dec_and_test函数是原子变量自减1并判断是否为0的API,驱动常用作资源释放标记。
函数原型:
1 | int atomic_dec_and_test(atomic_t *v);
|
参数说明:
v:atomic_t类型原子变量指针。
返回值:
自减后值为0返回1,非0返回0。
11.3.4.4.5. atomic_add函数¶
atomic_add函数是原子变量批量加法API,指定数值累加,无返回值。
函数原型:
1 | void atomic_add(int i, atomic_t *v);
|
参数说明:
i:累加的整型值;
v:目标原子变量指针。
11.3.4.4.6. atomic_sub函数¶
atomic_sub函数是原子变量批量减法API,指定数值递减,无返回值。
函数原型:
1 | void atomic_sub(int i, atomic_t *v);
|
参数说明:
i:递减的整型值;
v:目标原子变量指针。
注意
所有原子操作API严禁直接操作atomic_t结构体内部counter成员,必须通过内核接口调用;仅支持整型操作,不可用于复杂数据结构保护;全上下文兼容,无死锁风险。
11.3.5. 原子操作实验¶
本实验在设备树插件实验基础上添加原子操作部分,设备树插件完全一致,驱动仅说明修改部分,重复部分不作说明。
本章的示例代码目录为: linux_driver/11_atomic
11.3.5.1. 实验代码讲解¶
结构体与原子变量定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /* 定义 LED 字符设备结构体 */
struct led_chrdev {
/* 字符设备结构体 */
struct cdev dev;
/* 数据寄存器的虚拟地址,用于设置输出的电压 */
unsigned int __iomem *va_dr;
/* 数据方向寄存器的虚拟地址,用于设置输入或者输出 */
unsigned int __iomem *va_ddr;
/* 引脚高低位 */
unsigned int hl_pos;
/* 引脚偏移量 */
unsigned int led_pin;
/* LED 的设备树子节点 */
struct device_node *device_node;
/* 原子变量 */
atomic_t atomic;
};
/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;
|
结构体中加入atomic_t atomic作为互斥锁,标记设备是否被占用。
原子变量初始化
1 2 3 4 5 6 7 8 9 | static int pdrv_led_probe(struct platform_device *pdev)
{
// 内存分配、设备树解析、字符设备注册(省略重复代码)
/* 初始化原子变量 */
atomic_set(&led_cdev->atomic, 0);
// 错误处理(省略重复代码)
}
|
临界区保护
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | static ssize_t pdrv_led_write(struct file *filp, const char __user * buf,
size_t count, loff_t * ppos)
{
unsigned long val = 0;
unsigned long ret = 0;
/* 从文件结构体的私有数据中获取 led_chrdev 结构体指针 */
struct led_chrdev *led_cdev = filp->private_data;
/* 打印设备写操作信息 */
printk("pdrv_led write \r\n");
/* 从用户空间读取一个字符 */
if (get_user(ret, buf)) {
printk(KERN_ERR "get_user failed!\n");
return -EFAULT;
}
/* 尝试获取锁,如果锁已经被占用,则返回 -EBUSY */
if (atomic_cmpxchg(&led_cdev->atomic, 0, 1) != 0) {
printk(KERN_ERR "device is busy\n");
return -EBUSY;
}
/* 读取数据寄存器的值 */
val = ioread32(led_cdev->va_dr);
if (ret == '0') {
/* 设置高 16 位的使能位 */
val |= ((unsigned int)0x1 << (led_cdev->led_pin + 16));
/* 设置低 16 位的对应引脚输出低电平 */
val &= ~((unsigned int)0x01 << (led_cdev->led_pin));
} else {
/* 设置高 16 位的使能位 */
val |= ((unsigned int)0x1 << (led_cdev->led_pin + 16));
/* 设置低 16 位的对应引脚输出高电平 */
val |= ((unsigned int)0x01 << (led_cdev->led_pin));
}
/* 将修改后的值写回到数据寄存器 */
iowrite32(val, led_cdev->va_dr);
/* 释放锁 */
atomic_set(&led_cdev->atomic, 0);
return count;
}
|
说明:
第19行:调用atomic_cmpxchg(&led_cdev->atomic, 0, 1),原子判断锁状态:若返回值≠0,说明设备已被占用,直接返回-EBUSY;若返回值=0,说明获取锁成功,执行硬件初始化;
第41行:操作完毕后,调用atomic_set置0释放锁,允许其他进程访问。
11.3.5.3. 程序运行结果¶
如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
11.3.5.3.1. 实验操作¶
在本节实验中,鲁班猫系列板卡,系统设备树中均默认使能了 LED 的设备功能,需要关闭设备树的leds节点,可以修改leds节点的 status = "okay"; 为 status = "disabled";,然后编译设备树进行替换,也可以在板卡中直接使用以下命令关闭系统leds驱动对LED的控制:
1 2 3 4 5 6 7 8 | #心跳灯命名可能为sys_status_led或sys_led,需先确认
ls /sys/class/leds/
#如果为sys_status_led
sudo sh -c 'echo 0 > /sys/class/leds/sys_status_led/brightness'
#如果为sys_led
sudo sh -c 'echo 0 > /sys/class/leds/sys_led/brightness'
|
将led的亮度调为0,与此同时led的触发条件自动变为none,从而取消leds驱动对LED的控制。
设备树插件加载方法和设备树插件实验完全一致,加载设备树插件并重启板卡后使用以下命令加载驱动:
1 2 3 4 5 6 7 8 | #加载驱动
sudo insmod dts_led.ko
#信息输出如下
[ 3402.518948] led platform driver init
[ 3402.520096] led platform driver probe
[ 3402.520563] GPIO_BASE address: 0xFDD60000
[ 3402.520697] major=236, minor=0
|
通过驱动代码,最后会在/dev下创建led设备,可以使用echo命令来测试我们的led驱动是否正常。 我们使用以下命令控制灯的亮灭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #控制灯亮
sudo sh -c "echo 0 > /dev/atomic_led"
#控制灯灭
sudo sh -c "echo 1 > /dev/atomic_led"
#如果两个进程同时执行,此处让第一个进程后台执行,第二个进程前台执行进行模拟
sudo sh -c "echo 0 > /dev/atomic_led" & sudo sh -c "echo 1 > /dev/atomic_led"
#信息输出如下
[ 547.342205] pdrv_led open
[ 547.342278] pdrv_led open
[ 547.342353] pdrv_led write
[ 547.342354] pdrv_led write
[ 547.342368] pdrv_led release
[ 547.342379] pdrv_led release
|
“&”会把第一个echo 0的进程放到后台执行,第二个echo 1的进程前台执行,两个进程会几乎同时调用驱动的write函数; 驱动中atomic_cmpxchg会保证只有一个进程能获取原子锁,如果另一个进程同时获取原子锁会触发-EBUSY错误,提示device is busy。
如果想看到device is busy,可以添加头文件 #include <linux/delay.h>,然后在临界保护代码中使用 mdelay(200); 进行忙等待延时200ms。
1 2 3 4 5 6 7 8 9 10 11 12 13 | sudo sh -c "echo 0 > /dev/atomic_led" & sudo sh -c "echo 1 > /dev/atomic_led"
#加200ms忙等待延时信息输出
[1] 63563
[ 1151.062149] pdrv_led open
[ 1151.062342] pdrv_led write
[ 1151.063830] pdrv_led open
sh: 1: echo: echo: I/O error
[ 1151.063980] pdrv_led write
[ 1151.064005] device is busy
[ 1151.064211] pdrv_led release
[ 1151.262600] pdrv_led release
[1]+ Exit 1
|
需注意,实际使用原子操作不能使用休眠类延时,如:
1 | msleep()、ssleep()、usleep_range()、msleep_interruptible()
|
原子操作的场景(定时器回调、软中断、中断)属于原子上下文,没有进程调度权,休眠会直接触发内核BUG。 哪怕是进程上下文,原子操作保护的临界区也严禁加休眠延时。
语法允许但绝对不推荐,忙等待延时,如:
1 | udelay()、ndelay()、mdelay()
|
这些函数是CPU空转忙等,不休眠、不调度,语法上能跑。
11.4. 自旋锁¶
原子操作仅能解决单整型变量的竞态问题,面对多步骤硬件操作、中断上下文抢占、多核CPU并行访问等复杂场景,原子操作无法覆盖全流程保护。 自旋锁作为Linux内核适用于中断上下文的轻量级锁机制,通过忙等待机制保证临界区代码独占执行,是解决多核、中断场景下共享资源竞态的核心方案。
11.4.1. 自旋锁定义¶
自旋锁(Spinlock)是一种基于忙等待的内核同步机制,当线程尝试获取已被占用的自旋锁时, 不会进入睡眠状态,而是在原地循环检测锁状态(自旋),直到锁被释放后立即获取锁并执行临界区代码。
自旋锁核心特性:
不阻塞、不调度:锁竞争时线程原地自旋,不会触发进程上下文切换,开销极低;
临界区禁止睡眠:自旋锁持有期间,内核不可调度、不可睡眠,否则会导致死锁;
中断安全:可配合中断标志位,防止中断上下文抢占导致的死锁;
SMP架构适配:专为多核CPU设计,单核场景下自旋锁退化为禁止抢占机制。
11.4.2. 自旋锁类型定义¶
Linux内核自旋锁的核心类型为spinlock_t,定义于内核源码include/linux/spinlock_types.h, ARM64架构实现位于arch/arm64/include/asm/spinlock_types.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // 内核源码/include/linux/spinlock_types.h
// 底层原始自旋锁
typedef struct raw_spinlock {
arch_spinlock_t raw_lock;
} raw_spinlock_t;
// 上层自旋锁
typedef struct spinlock {
struct raw_spinlock rlock;
} spinlock_t;
// 静态初始化宏
#define DEFINE_RAW_SPINLOCK(x) raw_spinlock_t x = __RAW_SPIN_LOCK_UNLOCKED(x)
#define DEFINE_SPINLOCK(x) spinlock_t x = __SPIN_LOCK_UNLOCKED(x)
// 内核源码/arch/arm64/include/asm/spinlock_types.h
#ifndef __ASM_SPINLOCK_TYPES_H
#define __ASM_SPINLOCK_TYPES_H
// ARM64直接复用通用队列自旋锁,无自定义结构体
#include <asm-generic/qspinlock_types.h>
#include <asm-generic/qrwlock_types.h>
#endif /* __ASM_SPINLOCK_TYPES_H */
|
spinlock_t本质是封装了底层架构自旋锁的结构体,内核提供统一API屏蔽硬件差异,驱动开发无需关注底层架构实现。
11.4.4. 自旋锁标准API函数¶
自旋锁API适配进程、中断全上下文,所有接口仅操作spinlock_t类型变量,加锁与解锁必须严格成对使用,以下为内核标准常用API:
11.4.4.1. 自旋锁初始化¶
11.4.4.1.1. spin_lock_init函数¶
spin_lock_init函数是运行期动态初始化自旋锁的核心API,将自旋锁置为空闲未锁定状态,必须在硬件操作前调用。
函数原型:
1 | void spin_lock_init(spinlock_t *lock);
|
参数说明:
lock:指向spinlock_t类型自旋锁变量的指针,为待初始化的目标锁。
11.4.4.2. 自旋锁加锁/解锁¶
11.4.4.2.1. spin_lock函数¶
spin_lock函数是进程上下文普通加锁API,仅获取自旋锁,不关闭本地CPU中断,适用于无中断抢占的进程上下文场景。
函数原型:
1 | void spin_lock(spinlock_t *lock);
|
参数说明:
lock为spinlock_t类型自旋锁指针。
返回值:无返回值,获取锁成功则进入临界区,失败则原地自旋等待。
11.4.4.2.2. spin_unlock函数¶
spin_unlock函数是普通解锁API,与spin_lock成对使用,释放自旋锁,允许其他线程获取。
函数原型:
1 | void spin_unlock(spinlock_t *lock);
|
参数说明:
lock为spinlock_t类型自旋锁指针。
返回值:无返回值,解锁后锁状态变为空闲。
11.4.4.2.3. spin_lock_irqsave函数¶
spin_lock_irqsave函数是中断安全加锁API,内核最常用的自旋锁接口, 同时完成“保存本地中断状态+关闭本地中断+获取自旋锁”三步操作, 彻底杜绝中断抢占导致的死锁,适用于所有上下文。
函数原型:
1 | void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
|
参数说明:
lock:spinlock_t类型自旋锁指针;
flags:无符号长整型变量,用于存储当前CPU中断状态,解锁时恢复,需提前定义。
返回值:无返回值,flags变量会自动保存中断状态,无需手动赋值。
11.4.4.2.4. spin_unlock_irqrestore函数¶
spin_unlock_irqrestore函数是中断安全解锁API,与spin_lock_irqsave成对使用,完成“释放自旋锁+恢复本地中断状态”操作, 必须传入加锁时的flags变量。
函数原型:
1 | void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
|
参数说明:
lock:spinlock_t类型自旋锁指针;
flags:加锁时保存的中断状态变量,必须与加锁时一致。
返回值:无返回值,解锁后自动恢复中断状态。
11.4.4.2.5. spin_trylock函数¶
spin_trylock函数是非阻塞尝试加锁API,尝试获取自旋锁,失败不自旋,直接返回结果,适用于需快速判断锁状态的场景。
函数原型:
1 | int spin_trylock(spinlock_t *lock);
|
参数说明:
lock:spinlock_t类型自旋锁指针;
返回值:获取锁成功返回1,失败返回0。
11.4.4.2.6. spin_lock_irq函数¶
spin_lock_irq函数是关闭中断加锁API,不保存中断状态,适用于已知中断初始状态的场景,驱动开发少用。
函数原型:
1 | void spin_lock_irq(spinlock_t *lock);
|
参数说明:
lock:spinlock_t类型自旋锁指针;
注意
自旋锁临界区严禁任何睡眠、阻塞操作(如msleep、kmalloc、copy_from_user);临界区代码必须精简,缩短自旋时长,降低CPU开销;中断上下文必须使用spin_lock_irqsave/irqrestore组合,禁止使用普通spin_lock;加锁解锁必须成对,禁止中途退出不解锁。
11.4.5. 自旋锁实验¶
本实验在设备树插件实验基础上添加自旋锁部分,设备树插件完全一致,驱动仅说明修改部分,重复部分不作说明。
本章的示例代码目录为: linux_driver/12_spinlock
11.4.5.1. 实验代码讲解¶
结构体与自旋锁定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /* 定义 LED 字符设备结构体 */
struct led_chrdev {
/* 字符设备结构体 */
struct cdev dev;
/* 数据寄存器的虚拟地址,用于设置输出的电压 */
unsigned int __iomem *va_dr;
/* 数据方向寄存器的虚拟地址,用于设置输入或者输出 */
unsigned int __iomem *va_ddr;
/* 引脚高低位 */
unsigned int hl_pos;
/* 引脚偏移量 */
unsigned int led_pin;
/* LED 的设备树子节点 */
struct device_node *device_node;
/* 自旋锁 */
spinlock_t spinlock;
};
/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;
|
自旋锁作为成员变量,绑定LED设备实例,实现单设备资源独占保护。
自旋锁初始化
1 2 3 4 5 6 7 8 9 | static int pdrv_led_probe(struct platform_device *pdev)
{
// 内存分配、设备树解析、字符设备注册(省略重复代码)
/* 初始化自旋锁 */
spin_lock_init(&led_cdev->spinlock);
// 错误处理(省略重复代码)
}
|
自旋锁初始化必须在硬件操作之前完成,保证后续加锁逻辑有效。
临界区保护
加锁后GPIO寄存器读写全程独占执行,即使发生多核并行或中断触发,也不会打断临界区代码,杜绝寄存器操作竞态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | static ssize_t pdrv_led_write(struct file *filp, const char __user * buf,
size_t count, loff_t * ppos)
{
unsigned long val = 0;
unsigned long ret = 0;
/* 从文件结构体的私有数据中获取 led_chrdev 结构体指针 */
struct led_chrdev *led_cdev = filp->private_data;
/* 用于保存中断状态信息 */
unsigned long flags;
/* 打印设备写操作信息 */
printk("pdrv_led write \r\n");
/* 从用户空间读取一个字符 */
if (get_user(ret, buf)) {
printk(KERN_ERR "get_user failed!\n");
return -EFAULT;
}
/* 获取自旋锁并保存中断状态 */
spin_lock_irqsave(&led_cdev->spinlock, flags);
/* 读取数据寄存器的值 */
val = ioread32(led_cdev->va_dr);
if (ret == '0') {
/* 设置高 16 位的使能位 */
val |= ((unsigned int)0x1 << (led_cdev->led_pin + 16));
/* 设置低 16 位的对应引脚输出低电平 */
val &= ~((unsigned int)0x01 << (led_cdev->led_pin));
} else {
/* 设置高 16 位的使能位 */
val |= ((unsigned int)0x1 << (led_cdev->led_pin + 16));
/* 设置低 16 位的对应引脚输出高电平 */
val |= ((unsigned int)0x01 << (led_cdev->led_pin));
}
/* 将修改后的值写回到数据寄存器 */
iowrite32(val, led_cdev->va_dr);
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&led_cdev->spinlock, flags);
return count;
}
|
调用spin_lock_irqsave获取自旋锁,最后调用spin_unlock_irqrestore释放自旋锁。
需要注意 ,持有自旋锁时如果调用msleep,当前进程会进入睡眠状态,但自旋锁不会释放; 其他进程尝试获取锁时会一直忙等(CPU 100%占用),最终导致系统卡死/死锁,只能重启设备。 因此,要求 临界区不能有任何睡眠/阻塞操作;
11.4.5.3. 程序运行结果¶
如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
11.4.5.3.1. 实验操作¶
在本节实验中,鲁班猫系列板卡,系统设备树中均默认使能了 LED 的设备功能,需要关闭设备树的leds节点,可以修改leds节点的 status = "okay"; 为 status = "disabled";,然后编译设备树进行替换,也可以在板卡中直接使用以下命令关闭系统leds驱动对LED的控制:
1 2 3 4 5 6 7 8 | #心跳灯命名可能为sys_status_led或sys_led,需先确认
ls /sys/class/leds/
#如果为sys_status_led
sudo sh -c 'echo 0 > /sys/class/leds/sys_status_led/brightness'
#如果为sys_led
sudo sh -c 'echo 0 > /sys/class/leds/sys_led/brightness'
|
将led的亮度调为0,与此同时led的触发条件自动变为none,从而取消leds驱动对LED的控制。
设备树插件加载方法和设备树插件实验完全一致,加载设备树插件并重启板卡后使用以下命令加载驱动:
1 2 3 4 5 6 7 8 | #加载驱动
sudo insmod spinlock_led.ko
#信息输出如下
[ 72.789402] led platform driver init
[ 72.789923] led platform driver probe
[ 72.790012] GPIO_BASE address: 0xFDD60000
[ 72.790354] major=236, minor=0
|
通过驱动代码,最后会在/dev下创建led设备,可以使用echo命令来测试我们的led驱动是否正常。 我们使用以下命令控制灯的亮灭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #控制灯亮
sudo sh -c "echo 0 > /dev/spinlock_led"
#控制灯灭
sudo sh -c "echo 1 > /dev/spinlock_led"
#如果两个进程同时执行,此处让第一个进程后台执行,第二个进程前台执行进行模拟,多次重复执行以下命令
sudo sh -c "echo 0 > /dev/spinlock_led" & sudo sh -c "echo 1 > /dev/spinlock_led"
#信息输出如下
[ 4969.517237] pdrv_led open
[ 4969.517239] pdrv_led open
[ 4969.517310] pdrv_led write
[ 4969.517381] pdrv_led write
[ 4969.517387] pdrv_led release
[ 4969.517398] pdrv_led release
|
当两个进程同时竞争自旋锁时,其中一个进程会成功获取锁并执行临界区代码,另一个进程会进入忙等状态, 会在CPU上循环检查锁的状态,直到持有锁的进程释放锁后,这个忙等的进程才能获取到锁并执行自己的临界区代码。
11.5. 信号量¶
原子操作仅适配单变量竞态、自旋锁禁止临界区睡眠,面对进程上下文长时间持有锁、允许阻塞等待、多实例资源共享等场景,自旋锁无法满足需求。 信号量作为Linux内核可阻塞、支持计数的同步机制,允许线程在资源不可用时进入睡眠状态,不占用CPU资源,是进程上下文并发控制的主流方案。
11.5.1. 信号量定义¶
信号量(Semaphore)是一种基于计数机制的内核同步锁,通过维护一个整型计数器实现资源互斥访问:计数器值>0表示资源可用,线程可直接获取资源; 计数器值=0表示资源被占用,线程进入睡眠等待队列,直到资源释放后被唤醒。
信号量核心特性:
支持阻塞等待:资源不足时线程睡眠,不消耗CPU资源,适合长时间持有锁场景;
支持计数功能:计数器初始值可设为N,允许最多N个线程同时访问共享资源;
进程上下文专用:信号量会触发调度睡眠,禁止在中断上下文使用;
支持可中断等待:可响应信号唤醒,避免线程永久阻塞。
11.5.2. 信号量类型定义¶
Linux内核信号量定义于内核源码/include/linux/semaphore.h,基于等待队列与计数器实现,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // 内核源码/include/linux/semaphore.h
// 信号量核心结构体
struct semaphore {
raw_spinlock_t lock; // 保护信号量内部结构的自旋锁
unsigned int count; // 资源计数器:>0可用,=0不可用
struct list_head wait_list; // 等待线程队列
};
// 静态初始化信号量(初始值为1,互斥信号量)
#define __SEMAPHORE_INITIALIZER(name, n) \
{ \
.lock = __RAW_SPIN_LOCK_UNLOCKED((name).lock), \
.count = n, \
.wait_list = LIST_HEAD_INIT((name).wait_list), \
}
#define DEFINE_SEMAPHORE(name) \
struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)
|
说明:
count是信号量核心计数器,决定资源是否可用;
内置raw_spinlock_t自旋锁,保护计数器与等待队列的原子操作;
wait_list管理睡眠线程,资源释放时唤醒队列中的线程;
初始值设为1时,信号量退化为互斥信号量(同一时间仅1个线程访问)。
11.5.4. 信号量标准API函数¶
信号量仅适用于进程上下文,所有API操作struct semaphore类型变量,申请与释放必须成对,常用API如下:
11.5.4.1. 信号量初始化¶
11.5.4.1.1. sema_init函数¶
sema_init函数是运行期动态初始化信号量API,设置计数器初始值,决定资源可并发访问数量。
函数原型:
1 | void sema_init(struct semaphore *sem, int val);
|
参数说明:
sem:指向struct semaphore类型信号量变量的指针,为待初始化的目标信号量;
val:信号量计数器初始值,设为1时为互斥信号量,设为大于1的整数为计数信号量,不可设为负数。
返回值:无返回值,初始化后信号量计数器生效,等待队列初始化完成。
11.5.4.2. 信号量申请/释放¶
11.5.4.2.1. down函数¶
down函数是信号量阻塞申请API,内核标准资源获取接口,用于申请信号量资源,全程不可被信号中断,属于不可中断阻塞型申请。
函数原型:
1 | void down(struct semaphore *sem);
|
参数说明:
sem:struct semaphore类型信号量指针,指向待申请的目标信号量。
返回值:无返回值,申请成功则直接进入临界区;若计数器为0,当前线程进入不可中断睡眠状态,直到信号量被释放唤醒。
需注意,睡眠过程不可被信号打断,若资源长期不释放,线程会永久阻塞,驱动开发慎用,优先选用可中断版本。
11.5.4.2.2. down_interruptible函数¶
down_interruptible函数是信号量可中断阻塞申请API,驱动开发最常用的信号量申请接口,支持信号中断唤醒,避免线程永久阻塞,适配绝大多数进程上下文场景。
函数原型:
1 | int down_interruptible(struct semaphore *sem);
|
参数说明:
sem:struct semaphore类型信号量指针。
返回值:申请信号量成功返回0;若睡眠过程被信号打断唤醒,返回非0负值(-EINTR),需驱动代码判断返回值做异常处理。
需注意,使用时必须校验返回值,若返回非0值,需直接退出临界区并返回对应错误码,不可继续执行后续逻辑。
11.5.4.2.3. down_trylock函数¶
down_trylock函数是信号量非阻塞尝试申请API,尝试获取信号量资源,失败不睡眠、不阻塞,直接返回结果,适配快速判断资源状态的场景。
函数原型:
1 | int down_trylock(struct semaphore *sem);
|
参数说明:
sem:struct semaphore类型信号量指针。
返回值:获取信号量成功返回0;资源被占用、获取失败返回1,全程无阻塞、无睡眠。
需注意,无阻塞开销,可在不希望线程睡眠的场景替代阻塞型down接口,需根据返回值分支处理业务逻辑。
11.5.4.2.4. up函数¶
up函数是信号量释放API,与各类down接口成对使用,用于释放信号量资源,唤醒等待队列中的阻塞线程。
函数原型:
1 | void up(struct semaphore *sem);
|
参数说明:
sem:struct semaphore类型信号量指针,指向待释放的目标信号量。
返回值:无返回值,释放后信号量计数器自增1,若有等待线程,会唤醒队列中第一个线程竞争资源。
11.5.4.2.5. down_killable函数¶
down_killable函数是信号量可杀死阻塞申请API,进阶实用接口,仅能被致命信号中断唤醒, 安全性高于普通可中断版本,适用于对阻塞稳定性要求较高的场景。
函数原型:
1 | int down_killable(struct semaphore *sem);
|
参数说明:
sem:struct semaphore类型信号量指针,指向可杀死阻塞的目标信号量。
返回值:申请成功返回0;被致命信号打断返回-EINTR,普通信号无法唤醒该阻塞线程。
注意
信号量基于睡眠阻塞实现,严禁在中断上下文、软中断、定时器回调等原子上下文使用,否则会触发内核崩溃;申请与释放接口必须严格成对使用,禁止只申请不释放导致资源泄漏;计数信号量初始值需贴合实际资源数量,不可随意设置过大或负值;临界区可正常执行睡眠、阻塞操作,无自旋锁的严苛限制。
11.5.5. 信号量实验¶
本实验在设备树插件实验基础上添加信号量部分,设备树插件完全一致,驱动仅说明修改部分,重复部分不作说明。
本章的示例代码目录为: linux_driver/13_semaphore
11.5.5.1. 实验代码讲解¶
结构体与信号量定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /* 定义 LED 字符设备结构体 */
struct led_chrdev {
/* 字符设备结构体 */
struct cdev dev;
/* 数据寄存器的虚拟地址,用于设置输出的电压 */
unsigned int __iomem *va_dr;
/* 数据方向寄存器的虚拟地址,用于设置输入或者输出 */
unsigned int __iomem *va_ddr;
/* 引脚高低位 */
unsigned int hl_pos;
/* 引脚偏移量 */
unsigned int led_pin;
/* LED 的设备树子节点 */
struct device_node *device_node;
/* 信号量 */
struct semaphore sem;
};
/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;
|
信号量作为LED设备私有成员,绑定硬件资源,实现单设备实例的互斥访问。
信号量初始化
1 2 3 4 5 6 7 8 9 | static int pdrv_led_probe(struct platform_device *pdev)
{
// 内存分配、设备树解析、字符设备注册(省略重复代码)
/* 初始化信号量,初始值为1表示可用 */
sema_init(&led_cdev->sem, 1);
// 错误处理(省略重复代码)
}
|
信号量初始化必须在字符设备注册、硬件资源映射完成后,确保信号量生效前硬件已准备就绪。
临界区保护
led写操作涉及寄存器读写,属于临界区代码,通过down_interruptible申请信号量,操作完毕后通过up释放信号量,实现完整的互斥保护。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | static ssize_t pdrv_led_write(struct file *filp, const char __user * buf,
size_t count, loff_t * ppos)
{
unsigned long val = 0;
unsigned long ret = 0;
/* 从文件结构体的私有数据中获取 led_chrdev 结构体指针 */
struct led_chrdev *led_cdev = filp->private_data;
/* 打印设备写操作信息 */
printk("pdrv_led write \r\n");
/* 从用户空间读取一个字符 */
if (get_user(ret, buf)) {
printk(KERN_ERR "get_user failed!\n");
return -EFAULT;
}
/* 尝试获取信号量,如果信号量不可用,则进入睡眠状态 */
if (down_interruptible(&led_cdev->sem)) {
printk(KERN_ERR "Failed to acquire semaphore in write\n");
return -ERESTARTSYS;
}
/* 读取数据寄存器的值 */
val = ioread32(led_cdev->va_dr);
if (ret == '0') {
/* 设置高 16 位的使能位 */
val |= ((unsigned int)0x1 << (led_cdev->led_pin + 16));
/* 设置低 16 位的对应引脚输出低电平 */
val &= ~((unsigned int)0x01 << (led_cdev->led_pin));
} else {
/* 设置高 16 位的使能位 */
val |= ((unsigned int)0x1 << (led_cdev->led_pin + 16));
/* 设置低 16 位的对应引脚输出高电平 */
val |= ((unsigned int)0x01 << (led_cdev->led_pin));
}
/* 将修改后的值写回到数据寄存器 */
iowrite32(val, led_cdev->va_dr);
/* 释放信号量 */
up(&led_cdev->sem);
return count;
}
|
说明:
第19行:通过down_interruptible抢占信号量,资源被占用时,进程进入睡眠,不占用CPU,等待资源释放;
第41行:操作完成后调用up释放信号量,唤醒等待的进程。
11.5.5.3. 程序运行结果¶
如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
11.5.5.3.1. 实验操作¶
在本节实验中,鲁班猫系列板卡,系统设备树中均默认使能了 LED 的设备功能,需要关闭设备树的leds节点,可以修改leds节点的 status = "okay"; 为 status = "disabled";,然后编译设备树进行替换,也可以在板卡中直接使用以下命令关闭系统leds驱动对LED的控制:
1 2 3 4 5 6 7 8 | #心跳灯命名可能为sys_status_led或sys_led,需先确认
ls /sys/class/leds/
#如果为sys_status_led
sudo sh -c 'echo 0 > /sys/class/leds/sys_status_led/brightness'
#如果为sys_led
sudo sh -c 'echo 0 > /sys/class/leds/sys_led/brightness'
|
将led的亮度调为0,与此同时led的触发条件自动变为none,从而取消leds驱动对LED的控制。
设备树插件加载方法和设备树插件实验完全一致,加载设备树插件并重启板卡后使用以下命令加载驱动:
1 2 3 4 5 6 7 8 | #加载驱动
sudo insmod semaphore_led.ko
#信息输出如下
[ 5553.284808] led platform driver init
[ 5553.285166] led platform driver probe
[ 5553.285208] GPIO_BASE address: 0xFDD60000
[ 5553.285319] major=236, minor=0
|
通过驱动代码,最后会在/dev下创建led设备,可以使用echo命令来测试我们的led驱动是否正常。 我们使用以下命令控制灯的亮灭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #控制灯亮
sudo sh -c "echo 0 > /dev/semaphore_led"
#控制灯灭
sudo sh -c "echo 1 > /dev/semaphore_led"
#如果两个进程同时执行,此处让第一个进程后台执行,第二个进程前台执行进行模拟
sudo sh -c "echo 0 > /dev/semaphore_led" & sudo sh -c "echo 1 > /dev/semaphore_led"
#信息输出如下
[ 5852.782952] pdrv_led open
[ 5852.782953] pdrv_led open
[ 5852.783027] pdrv_led write
[ 5852.783097] pdrv_led write
[ 5852.783105] pdrv_led release
[ 5852.783114] pdrv_led release
|
多进程访问时,未获取信号量的进程自动睡眠,不占用CPU,资源释放后自动唤醒,执行自己的临界区代码。
11.6. 互斥体¶
11.6.1. 互斥体定义¶
互斥体(Mutex,Mutual Exclusion)是Linux内核专为独占式临界区保护设计的同步机制,属于二进制排他锁范畴,核心作用是保证同一时间仅有一个线程/进程进入临界区执行代码。 互斥体的底层逻辑依托原子变量、自旋锁和等待队列实现,既保留了原子操作的状态读写原子性, 又具备睡眠阻塞的低CPU开销特性。
互斥体核心特性:
独占排他性:同一时刻仅允许一个持有者,临界区完全独占,彻底杜绝并发竞态,互斥效果无中间态,和原子操作的不可分割性高度契合。
原子状态管控:锁状态的查询、抢占、释放均通过原子变量实现,保证状态切换的原子性,避免锁状态出现并发错乱。
阻塞休眠特性:锁竞争时线程进入睡眠等待队列,不占用CPU资源,适配长临界区、耗时硬件操作场景,区别于自旋锁的忙等待。
持有者唯一性:仅当前持有互斥体的线程可释放锁,禁止跨线程/跨进程非法释放,同步逻辑更严谨。
进程上下文专属:仅支持在进程上下文使用,无中断相关API,避免调度异常和内核崩溃风险。
轻量无冗余:结构体体积小、调度开销低,无计数信号量的多余功能,专注于独占互斥场景。
可中断阻塞:支持可中断式锁申请,线程可被信号唤醒,避免出现永久阻塞的异常线程。
禁止递归持有:同一线程不可重复申请同一互斥体,内核自带死锁检测,规避递归死锁问题。
11.6.2. 互斥体类型定义¶
Linux内核互斥体定义于内核源码/include/linux/mutex.h,底层依托原子操作和自旋锁封装,屏蔽硬件架构差异,提供统一的操作接口,核心源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // 内核源码/include/linux/mutex.h
/**
* struct mutex - 内核互斥体核心结构体
* @owner: 原子长整型变量,标识锁持有者,保证状态原子性
* @wait_lock: 原始自旋锁,保护等待队列的并发修改
* @wait_list: 阻塞线程等待队列
*/
struct mutex {
atomic_long_t owner;
spinlock_t wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
struct list_head wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
// 静态初始化互斥体宏
#define __MUTEX_INITIALIZER(lockname) \
{ .owner = ATOMIC_LONG_INIT(0) \
, .wait_lock = __SPIN_LOCK_UNLOCKED(lockname.wait_lock) \
, .wait_list = LIST_HEAD_INIT(lockname.wait_list) \
__DEBUG_MUTEX_INITIALIZER(lockname) \
__DEP_MAP_MUTEX_INITIALIZER(lockname) }
// 定义并初始化静态互斥体
#define DEFINE_MUTEX(mutexname) \
struct mutex mutexname = __MUTEX_INITIALIZER(mutexname)
|
互斥体通过atomic_long_t类型的owner变量实现锁状态的原子管控,这是互斥体与原子操作设计理念相通的核心体现; 内置自旋锁仅用于保护等待队列的短时间操作,不影响整体阻塞特性,兼顾效率与安全性。
11.6.4. 互斥体标准API函数¶
互斥体API专为进程上下文互斥场景设计,所有接口仅操作struct mutex类型变量,加锁解锁规则严苛, 必须遵循“持有者释放”原则,常用标准API如下:
11.6.4.1. 互斥体初始化¶
11.6.4.1.1. mutex_init函数¶
mutex_init函数是运行期动态初始化互斥体的核心API,将互斥体置为空闲解锁状态,是使用互斥体的第一步,必须在硬件操作、加锁逻辑前调用。
函数原型:
1 | void mutex_init(struct mutex *lock);
|
参数说明: - lock:指向struct mutex类型互斥体变量的指针,为待初始化的目标互斥锁。
返回值:无返回值,初始化后互斥体状态为空闲,可正常申请加锁。
需注意,禁止对已初始化、已加锁的互斥体重复调用mutex_init,否则会破坏锁状态,导致内核异常。
11.6.4.2. 互斥体加锁/解锁¶
11.6.4.2.1. mutex_lock函数¶
mutex_lock函数是互斥体阻塞加锁API,标准不可中断加锁接口,申请互斥体资源,失败则进入不可中断睡眠,直到获取锁。
函数原型:
1 | void mutex_lock(struct mutex *lock);
|
参数说明:
lock:struct mutex类型互斥体指针。
返回值:无返回值,获取锁成功则进入临界区;资源占用时线程不可中断睡眠,直至锁被释放。
需注意,不可被信号打断,可能出现永久阻塞,常规驱动开发优先选用可中断版本。
11.6.4.2.2. mutex_lock_interruptible函数¶
mutex_lock_interruptible函数是互斥体可中断阻塞加锁API,驱动开发首选加锁接口,支持信号中断唤醒,避免线程永久阻塞,安全性更高
函数原型:
1 | int mutex_lock_interruptible(struct mutex *lock);
|
参数说明:
lock:struct mutex类型互斥体指针。
返回值:加锁成功返回0;睡眠过程被信号打断返回非0负值(-EINTR),需驱动校验返回值。
需注意,必须判断返回值,非0值需直接退出,不可执行临界区代码,这是驱动稳定性的核心要求。
11.6.4.2.3. mutex_trylock函数¶
mutex_trylock函数是互斥体非阻塞尝试加锁API,无睡眠、无阻塞,快速尝试获取锁,适用于不允许线程阻塞的场景。
函数原型:
1 | int mutex_trylock(struct mutex *lock);
|
参数说明:
lock:struct mutex类型互斥体指针。
返回值:加锁成功返回1;锁被占用、加锁失败返回0,全程无调度、无睡眠。
需注意,失败直接返回,不会阻塞线程,适合快速判断资源状态后执行分支逻辑。
11.6.4.2.4. mutex_unlock函数¶
mutex_unlock函数是互斥体解锁API,与所有mutex_lock类接口成对使用,必须由加锁持有者主动调用释放,禁止跨线程解锁。
函数原型:
1 | void mutex_unlock(struct mutex *lock);
|
参数说明:
lock:struct mutex类型互斥体指针。
返回值:无返回值,释放后互斥体变为空闲状态,唤醒等待队列中的阻塞线程。
需注意,严禁重复解锁、未加锁先解锁、跨线程解锁,内核调试模式下会直接抛出BUG,触发系统崩溃。
11.6.4.2.5. mutex_is_locked函数¶
mutex_is_locked函数是互斥体状态查询API,用于查询当前互斥体是否被占用,无加锁解锁操作。
函数原型:
1 | int mutex_is_locked(const struct mutex *lock);
|
参数说明:
lock:struct mutex类型互斥体指针。
返回值:互斥体被占用返回1,空闲未加锁返回0,仅做状态查询,不改变锁状态。
注意
互斥体绝对禁止在中断上下文、软中断、定时器回调等原子上下文使用,否则直接触发内核panic;必须遵循“谁加锁谁释放”原则,严禁跨线程释放锁;禁止递归重复加锁,内核不支持互斥体递归嵌套,会直接触发死锁检测;临界区可正常执行睡眠、阻塞、内存分配等操作,无自旋锁的限制;互斥体专为单资源互斥设计,效率远高于信号量,常规互斥场景优先选用互斥体,而非信号量。
11.6.5. 互斥体实验¶
本实验在设备树插件实验基础上添加互斥体部分,设备树插件完全一致,驱动仅说明修改部分,重复部分不作说明。
本章的示例代码目录为: linux_driver/14_mutex
11.6.5.1. 实验代码讲解¶
结构体与互斥体定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /* 定义 LED 字符设备结构体 */
struct led_chrdev {
/* 字符设备结构体 */
struct cdev dev;
/* 数据寄存器的虚拟地址,用于设置输出的电压 */
unsigned int __iomem *va_dr;
/* 数据方向寄存器的虚拟地址,用于设置输入或者输出 */
unsigned int __iomem *va_ddr;
/* 引脚高低位 */
unsigned int hl_pos;
/* 引脚偏移量 */
unsigned int led_pin;
/* LED 的设备树子节点 */
struct device_node *device_node;
/* 互斥体 */
struct mutex mutex;
};
/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;
|
将互斥体作为LED设备私有成员,绑定硬件资源实现独占保护。
互斥体初始化
1 2 3 4 5 6 7 8 9 | static int pdrv_led_probe(struct platform_device *pdev)
{
// 内存分配、设备树解析、字符设备注册(省略重复代码)
/* 初始化互斥体 */
mutex_init(&led_cdev->mutex);
// 错误处理(省略重复代码)
}
|
互斥体初始化必须在字符设备注册、硬件资源映射完成后,确保互斥体生效前硬件已准备就绪。
临界区保护
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | static ssize_t pdrv_led_write(struct file *filp, const char __user * buf,
size_t count, loff_t * ppos)
{
unsigned long val = 0;
unsigned long ret = 0;
/* 从文件结构体的私有数据中获取 led_chrdev 结构体指针 */
struct led_chrdev *led_cdev = filp->private_data;
/* 打印设备写操作信息 */
printk("pdrv_led write \r\n");
/* 从用户空间读取一个字符 */
if (get_user(ret, buf)) {
printk(KERN_ERR "get_user failed!\n");
return -EFAULT;
}
/* 尝试获取互斥体,如果互斥体不可用,则进入睡眠状态 */
if (mutex_lock_interruptible(&led_cdev->mutex)) {
printk(KERN_ERR "Failed to acquire mutex in write\n");
return -ERESTARTSYS;
}
/* 读取数据寄存器的值 */
val = ioread32(led_cdev->va_dr);
if (ret == '0') {
/* 设置高 16 位的使能位 */
val |= ((unsigned int)0x1 << (led_cdev->led_pin + 16));
/* 设置低 16 位的对应引脚输出低电平 */
val &= ~((unsigned int)0x01 << (led_cdev->led_pin));
} else {
/* 设置高 16 位的使能位 */
val |= ((unsigned int)0x1 << (led_cdev->led_pin + 16));
/* 设置低 16 位的对应引脚输出高电平 */
val |= ((unsigned int)0x01 << (led_cdev->led_pin));
}
/* 将修改后的值写回到数据寄存器 */
iowrite32(val, led_cdev->va_dr);
/* 释放互斥体 */
mutex_unlock(&led_cdev->mutex);
return count;
}
|
第19行:通过mutex_lock_interruptible尝试获取互斥体,如果互斥体不可用,则进入睡眠状态;
第41行:操作完成后调用mutex_unlock释放互斥体,唤醒等待的进程。
11.6.5.3. 程序运行结果¶
如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
11.6.5.3.1. 实验操作¶
在本节实验中,鲁班猫系列板卡,系统设备树中均默认使能了 LED 的设备功能,需要关闭设备树的leds节点,可以修改leds节点的 status = "okay"; 为 status = "disabled";,然后编译设备树进行替换,也可以在板卡中直接使用以下命令关闭系统leds驱动对LED的控制:
1 2 3 4 5 6 7 8 | #心跳灯命名可能为sys_status_led或sys_led,需先确认
ls /sys/class/leds/
#如果为sys_status_led
sudo sh -c 'echo 0 > /sys/class/leds/sys_status_led/brightness'
#如果为sys_led
sudo sh -c 'echo 0 > /sys/class/leds/sys_led/brightness'
|
将led的亮度调为0,与此同时led的触发条件自动变为none,从而取消leds驱动对LED的控制。
设备树插件加载方法和设备树插件实验完全一致,加载设备树插件并重启板卡后使用以下命令加载驱动:
1 2 3 4 5 6 7 8 | #加载驱动
sudo insmod mutex_led.ko
#信息输出如下
[ 145.169694] led platform driver init
[ 145.170026] led platform driver probe
[ 145.170070] GPIO_BASE address: 0xFDD60000
[ 145.170211] major=236, minor=0
|
通过驱动代码,最后会在/dev下创建led设备,可以使用echo命令来测试我们的led驱动是否正常。 我们使用以下命令控制灯的亮灭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #控制灯亮
sudo sh -c "echo 0 > /dev/mutex_led"
#控制灯灭
sudo sh -c "echo 1 > /dev/mutex_led"
#如果两个进程同时执行,此处让第一个进程后台执行,第二个进程前台执行进行模拟
sudo sh -c "echo 0 > /dev/mutex_led" & sudo sh -c "echo 1 > /dev/mutex_led"
#信息输出如下
[ 257.420672] pdrv_led open
[ 257.420674] pdrv_led open
[ 257.420761] pdrv_led write
[ 257.420844] pdrv_led write
[ 257.420855] pdrv_led release
[ 257.420863] pdrv_led release
|
两条命令并发执行时,日志中虽有两个open和write输出,但互斥体已保证两个write对应的临界区(寄存器读写)不会同时执行, 本质就是未获取锁的进程短暂睡眠,等待前一个进程释放锁后再执行,这和信号量的并发控制逻辑完全相同。
11.7. 解决竞态方法的对比¶
原子操作详细参数表:
对比维度 |
原子操作 |
|---|---|
核心定义 |
基于CPU硬件指令,实现单整型变量的不可中断读写操作,无锁机制,仅保证单变量操作原子性 |
底层实现 |
依赖CPU硬件原子指令(如ARM64的LDREX/STREX),无等待队列、无调度开销,直接操作硬件寄存器 |
阻塞特性 |
非阻塞,无等待逻辑,操作失败直接返回,不占用额外CPU资源 |
上下文适配 |
全上下文兼容,可在进程上下文、中断上下文、软中断、tasklet等所有场景使用 |
临界区规则 |
仅支持单整型变量操作,操作过程为单指令,无多步骤临界区 |
使用约束 |
无严格约束,仅限制操作类型为整型变量,无递归、持有者等限制 |
资源开销 |
极低,仅占用CPU硬件指令开销,无内核调度、等待队列等额外开销 |
死锁风险 |
无死锁风险,无等待逻辑,不涉及锁持有与释放的成对约束 |
核心优势 |
极致轻量、无调度开销,全上下文兼容,适合简单变量状态控制 |
核心劣势 |
仅支持整型变量,无法保护多步骤临界区、复杂数据结构 |
适用场景 |
单整型变量修改(如状态标记、计数、标志位翻转),全上下文场景 |
底层依赖 |
依赖CPU硬件原子指令,无需内核调度、等待队列支持 |
自旋锁详细参数表:
对比维度 |
自旋锁 |
|---|---|
核心定义 |
基于忙等待的轻量级锁,多CPU竞争时线程循环查询锁状态,不睡眠,保证临界区不被中断 |
底层实现 |
封装arch_spinlock_t底层结构,内置自旋锁循环逻辑,结合raw_spinlock保护临界区,无睡眠调度 |
阻塞特性 |
非阻塞(忙等待),锁竞争时线程循环查询,持续占用CPU,不触发调度 |
上下文适配 |
全上下文兼容,进程/中断上下文均可使用 |
临界区规则 |
支持多步骤临界区,但必须精简(避免长时间占用CPU),禁止睡眠、延时等调度操作 |
使用约束 |
禁止临界区睡眠,禁止递归持有,中断上下文需用spin_lock_irqsave/irqrestore保护 |
资源开销 |
低,无调度开销,但锁竞争时忙等待会消耗CPU资源,临界区越长开销越大 |
死锁风险 |
有死锁风险(如递归持有、中断抢占、锁嵌套不当),需依赖调试配置检测 |
核心优势 |
中断上下文兼容,无调度开销,适合短临界区、硬件寄存器操作 |
核心劣势 |
锁竞争时忙等待消耗CPU,临界区过长会导致系统性能下降 |
适用场景 |
中断上下文临界区、短时间硬件寄存器操作、多核CPU短临界区保护 |
底层依赖 |
依赖CPU架构自旋锁实现(如ARM64的arch_spinlock_t),无调度依赖 |
信号量详细参数表:
对比维度 |
信号量 |
|---|---|
核心定义 |
基于计数的同步机制,通过计数器控制资源访问数量,支持阻塞睡眠,可实现互斥或同步 |
底层实现 |
封装raw_spinlock_t,内置count计数器,结合等待队列,支持计数同步与阻塞唤醒 |
阻塞特性 |
阻塞(睡眠等待),计数器为0时线程进入等待队列,释放CPU,支持可中断/不可中断等待 |
上下文适配 |
仅支持进程上下文,严禁在原子上下文使用,无中断安全API |
临界区规则 |
支持长临界区,允许睡眠、延时,无长度限制,适配耗时操作场景 |
使用约束 |
支持计数(初始值可设为N),无持有者唯一性限制,可跨线程释放 |
资源开销 |
较高,计数逻辑、等待队列管理均需额外开销,调度开销高于互斥体 |
死锁风险 |
有死锁风险(如锁嵌套、信号量泄漏),调试检测难度高于互斥体 |
核心优势 |
灵活通用,支持计数同步,可实现多资源共享、多线程同步 |
核心劣势 |
开销较高,使用约束宽松,易因滥用导致同步失效、死锁 |
适用场景 |
多资源共享、多线程同步、计数控制(如多进程访问共享缓冲区) |
底层依赖 |
依赖raw_spinlock和内核调度机制,需等待队列、计数逻辑支持 |
互斥体详细参数表:
对比维度 |
互斥锁 |
|---|---|
核心定义 |
专用二进制独占锁,同一时间仅允许一个线程持有,支持阻塞睡眠,专为进程上下文互斥设计 |
底层实现 |
基于原子变量、自旋锁和等待队列实现,依赖内核调度机制完成阻塞与唤醒 |
阻塞特性 |
阻塞(睡眠等待),锁竞争时线程进入等待队列,释放CPU,触发内核调度 |
上下文适配 |
仅支持进程上下文,严禁在中断、软中断等原子上下文使用,无中断安全API |
临界区规则 |
支持长临界区,允许睡眠、延时等调度操作,无临界区长度限制 |
使用约束 |
禁止递归持有,仅持有者可释放锁,禁止跨线程释放,无计数功能 |
资源开销 |
中,存在内核调度开销(阻塞/唤醒),但无忙等待消耗,整体开销低于信号量 |
死锁风险 |
有死锁风险(如递归持有、锁泄漏、嵌套不当),内核内置调试检测机制 |
核心优势 |
安全可靠,调试友好,专属互斥场景,进程上下文互斥首选 |
核心劣势 |
仅支持进程上下文,无计数功能,无法实现多资源共享 |
适用场景 |
进程上下文单资源独占访问(如LED驱动、设备节点操作),长临界区场景 |
底层依赖 |
依赖原子变量、自旋锁和内核调度机制,需等待队列支持 |