11. Linux内核并发与竞争

Linux内核竞态的核心根源是并发执行流无保护争抢共享资源,解决竞态的本质是划定临界区、隔离并发访问, 让共享资源在同一时刻仅被一个执行流操作。内核针对不同并发场景,提供了分层级的防护方案,本章将介绍常用的解决方法。

11.1. 并发与竞态问题剖析

11.1.1. 并发产生场景

Linux内核是抢占式多任务系统,且支持多核CPU并行运算,驱动程序作为内核的一部分,会面临多种并发访问场景:

  • 多进程并发访问:多个用户态进程同时open/write同一个设备节点,争抢硬件资源;

  • 中断上下文抢占:硬件中断触发时,中断服务程序会抢占当前进程上下文,打断正在执行的驱动代码;

  • 多核CPU并行:SMP架构下,不同CPU核心同时执行同一段驱动代码,同时操作寄存器/全局变量;

  • 内核线程抢占:内核线程与用户进程、中断服务程序之间的调度抢占。

11.1.2. 竞态定义与危害

竞态是指多个执行单元同时操作共享资源,且操作过程不具备原子性,导致最终执行结果依赖于执行顺序,出现不可预期的错误。

例如在led驱动实验中,进程A执行led打开操作,刚读取寄存器值、未完成写入; 进程B同时抢占执行,读取到旧寄存器值并修改,最终导致引脚配置失效、led亮灭失控。

11.2. 解决竞态的常用方法

针对内核竞态问题,Linux提供了多级并发控制机制如下:

  • 原子操作

  • 自旋锁

  • 互斥锁

  • 信号量

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))

说明:

  1. atomic_t本质是封装了int类型的结构体,禁止直接操作counter成员,必须通过内核API访问;

  2. 不同CPU架构的原子操作实现不同,但内核提供统一API,驱动无需关注底层硬件差异;

  3. 原子操作仅支持整型变量(32位/64位),不支持复杂数据结构。

11.3.3. 原子操作解决竞态流程

以led驱动多进程write竞态为例,原子操作的互斥逻辑如下:

../_images/atomic_0.jpg

原子变量作为互斥标记,通过不可分割的比较交换操作,保证同一时间只有一个执行单元能获取设备使用权,彻底杜绝竞态。

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. 实验代码讲解

结构体与原子变量定义

原子变量定义(位于linux_driver/11_atomic/atomic_led.c)
 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作为互斥锁,标记设备是否被占用。

原子变量初始化

probe函数(位于linux_driver/11_atomic/atomic_led.c)
1
2
3
4
5
6
7
8
9
static int pdrv_led_probe(struct platform_device *pdev)
{
    // 内存分配、设备树解析、字符设备注册(省略重复代码)

    /* 初始化原子变量 */
    atomic_set(&led_cdev->atomic, 0);

    // 错误处理(省略重复代码)
}

临界区保护

write函数(位于linux_driver/11_atomic/atomic_led.c)
 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.2. 编译设备树和驱动

此部分和设备树插件实验完全一致不作过多说明。

编译得到设备树插件lubancat-led-overlay.dtb和驱动模块atomic_led.ko。

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.3. 自旋锁解决竞态流程

以led驱动多进程write竞态为例,自旋锁的互斥逻辑如下:

../_images/spinlock_0.jpg

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. 实验代码讲解

结构体与自旋锁定义

自旋锁定义(位于linux_driver/12_spinlock/spinlock_led.c)
 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设备实例,实现单设备资源独占保护。

自旋锁初始化

自旋锁初始化(位于linux_driver/12_spinlock/spinlock_led.c)
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寄存器读写全程独占执行,即使发生多核并行或中断触发,也不会打断临界区代码,杜绝寄存器操作竞态。

write函数自旋锁保护(位于linux_driver/12_spinlock/spinlock_led.c)
 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.2. 编译设备树和驱动

此部分和设备树插件实验完全一致不作过多说明。

编译得到设备树插件lubancat-led-overlay.dtb和驱动模块spinlock_led.ko。

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)

说明:

  1. count是信号量核心计数器,决定资源是否可用;

  2. 内置raw_spinlock_t自旋锁,保护计数器与等待队列的原子操作;

  3. wait_list管理睡眠线程,资源释放时唤醒队列中的线程;

  4. 初始值设为1时,信号量退化为互斥信号量(同一时间仅1个线程访问)。

11.5.3. 信号量解决竞态流程

以led驱动多进程write竞态为例,信号量的互斥逻辑如下:

../_images/semaphore_0.jpg

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. 实验代码讲解

结构体与信号量定义

信号量定义(位于linux_driver/13_semaphore/semaphore_led.c)
 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设备私有成员,绑定硬件资源,实现单设备实例的互斥访问。

信号量初始化

信号量初始化(位于linux_driver/13_semaphore/semaphore_led.c)
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释放信号量,实现完整的互斥保护。

write函数信号量保护(位于linux_driver/13_semaphore/semaphore_led.c)
 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.2. 编译设备树和驱动

此部分和设备树插件实验完全一致不作过多说明。

编译得到设备树插件lubancat-led-overlay.dtb和驱动模块semaphore_led.ko。

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.3. 互斥体解决竞态流程

以led驱动多进程write竞态为例,互斥体的互斥逻辑如下:

../_images/mutex_0.jpg

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. 实验代码讲解

结构体与互斥体定义

互斥体定义(位于linux_driver/14_mutex/mutex_led.c)
 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设备私有成员,绑定硬件资源实现独占保护。

互斥体初始化

互斥体初始化(位于linux_driver/14_mutex/mutex_led.c)
1
2
3
4
5
6
7
8
9
static int pdrv_led_probe(struct platform_device *pdev)
{
    // 内存分配、设备树解析、字符设备注册(省略重复代码)

    /* 初始化互斥体 */
    mutex_init(&led_cdev->mutex);

    // 错误处理(省略重复代码)
}

互斥体初始化必须在字符设备注册、硬件资源映射完成后,确保互斥体生效前硬件已准备就绪。

临界区保护

write函数互斥体保护(位于linux_driver/14_mutex/mutex_led.c)
 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.2. 编译设备树和驱动

此部分和设备树插件实验完全一致不作过多说明。

编译得到设备树插件lubancat-led-overlay.dtb和驱动模块mutex_led.ko。

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驱动、设备节点操作),长临界区场景

底层依赖

依赖原子变量、自旋锁和内核调度机制,需等待队列支持