imx6ull-驱动开发篇15——linux自旋锁

目录

自旋锁简介

定义自旋锁

[自旋锁 API 函数](#自旋锁 API 函数)

线程之间自旋锁

中断与自旋锁

下半部与自旋锁

其他类型的锁

读写自旋锁

顺序锁

自旋锁使用注意事项


Linux 内核提供的几种并发和竞争的处理方法中:

我们已经学习了:驱动开发篇14------原子操作

本讲实验我们学习自旋锁。

自旋锁简介

当一个线程要访问某个共享资源的时候首先要先获取相应的锁, 锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。

自旋锁是一种 ​​忙等待(Busy-Waiting)锁​​,当线程尝试获取已被占用的锁时,会持续循环检查("自旋")直到锁释放,而非进入睡眠状态。

特性​ ​说明​
​忙等待机制​ 获取锁失败时持续占用CPU循环检测(对比互斥锁的睡眠等待)
​短临界区优化​ 适用于极短耗时操作(通常<10μs)
​不可休眠​ 持有锁期间禁止休眠(否则可能导致死锁)
​SMP/UP通用​ 在单核(UP)和多核(SMP)系统均有优化实现
​中断安全​ 提供spin_lock_irqsave等变体支持中断上下文

把自旋锁比作一个变量 a,变量 a=1 的时候表示共享资源可用,当 a=0的时候表示共享资源不可用。现在线程 A 要访问共享资源,发现 a=0(自旋锁被其他线程持有),那么线程 A 就会不断的查询 a 的值,直到 a=1。

从这里我们可以看到自旋锁的一个缺点:那就等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长,也就是自旋锁适用于短时期的轻量级加锁。

定义自旋锁

Linux 内核使用结构体 spinlock_t 表示自旋锁,结构体定义如下所示:

cpp 复制代码
/* 
 * 自旋锁核心数据结构定义
 * 采用联合体(union)实现调试与生产环境的内存布局兼容
 */
typedef struct spinlock {
    union {
        /* 生产环境实际使用的自旋锁结构 */
        struct raw_spinlock rlock;  // 基础自旋锁实现

#ifdef CONFIG_DEBUG_LOCK_ALLOC
        /* 调试模式下扩展的锁验证结构 */
        struct {
            /* 填充字节保证与raw_spinlock内存对齐 */
            u8 __padding[LOCK_PADSIZE];  
            
            /* 锁依赖跟踪映射(用于死锁检测) */
            struct lockdep_map dep_map;  
        };
#endif
    };
} spinlock_t;

在使用自旋锁之前,需要先定义一个自旋锁变量。

自旋锁定义方法如下所示:

cpp 复制代码
spinlock_t lock; //定义自旋锁

自旋锁 API 函数

线程之间自旋锁

linux下最基本的自旋锁 API 函数如表:

这些自旋锁API 函数,适用于SMP或支持抢占的单CPU下线程之间的并发访问,也就是用于线程与线程之间。

被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数,否则的话会可能会导致死锁现象的发生。

API函数使用的示例代码如下:

cpp 复制代码
#include <linux/module.h>
#include <linux/spinlock.h>

// 静态定义自旋锁(方法1)
static DEFINE_SPINLOCK(static_lock);

// 动态自旋锁(方法2)
static spinlock_t dynamic_lock;

static int __init spinlock_demo_init(void)
{
    printk(KERN_INFO "Spinlock Demo Start\n");

    // 初始化动态锁
    spin_lock_init(&dynamic_lock);

    // 1. 基本加锁/解锁
    spin_lock(&static_lock);
    printk("Critical section 1\n");
    spin_unlock(&static_lock);

    // 2. 尝试获取锁(非阻塞)
    if (spin_trylock(&dynamic_lock)) {
        printk("Got dynamic lock\n");
        spin_unlock(&dynamic_lock);
    } else {
        printk("Failed to get dynamic lock\n");
    }

    // 3. 检查锁状态
    printk("Static lock is %slocked\n", 
           spin_is_locked(&static_lock) ? "" : "not ");

    return 0;
}

static void __exit spinlock_demo_exit(void)
{
    printk(KERN_INFO "Spinlock Demo End\n");
}

module_init(spinlock_demo_init);
module_exit(spinlock_demo_exit);
MODULE_LICENSE("GPL");

中断与自旋锁

中断里面可以使用自旋锁,但是在中断里面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断,否则可能导致锁死现象的发生,如图:

线程 A 先运行,并且获取到了 lock 这个锁,当线程 A 运行 functionA 函数的时候中断发生了,中断抢走了 CPU 使用权。右边的中断服务函数也要获取 lock 这个锁,但是这个锁被线程 A 占有着,中断就会一直自旋,等待锁有效。这样就形成了死锁。

所以需要在获取锁之前关闭本地中断, Linux 内核提供了相应的 API 函数:

示例代码如下:

cpp 复制代码
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/interrupt.h>

static DEFINE_SPINLOCK(irq_lock);  // 定义自旋锁
static unsigned long irq_flags;     // 保存中断状态

// 共享资源
static int critical_data = 0;

// 模拟中断处理函数
static irqreturn_t sample_irq_handler(int irq, void *dev_id)
{
    spin_lock(&irq_lock);  // 基本中断上下文锁定
    critical_data++;
    spin_unlock(&irq_lock);
    
    return IRQ_HANDLED;
}

static int __init spinlock_irq_init(void)
{
    printk(KERN_INFO "Spinlock IRQ Demo Start\n");

    // 场景1:明确知道中断未禁用时
    spin_lock_irq(&irq_lock);    // 禁用中断+获取锁
    critical_data = 100;
    spin_unlock_irq(&irq_lock);  // 恢复中断+释放锁

    // 场景2:安全通用版本(推荐)
    spin_lock_irqsave(&irq_lock, irq_flags);  // 保存状态+禁用中断+锁
    printk("Critical data: %d\n", critical_data);
    spin_unlock_irqrestore(&irq_lock, irq_flags);  // 恢复精确中断状态

    // 注册模拟中断
    request_irq(1, sample_irq_handler, 0, "sample_irq", NULL);

    return 0;
}

static void __exit spinlock_irq_exit(void)
{
    free_irq(1, NULL);
    printk(KERN_INFO "Spinlock IRQ Demo End\n");
}

module_init(spinlock_irq_init);
module_exit(spinlock_irq_exit);
MODULE_LICENSE("GPL");

一般在线程中使用 spin_lock_irqsave/ spin_unlock_irqrestore,在中断中使用 spin_lock/spin_unlock。

下半部与自旋锁

Linux 将中断处理分为两个部分:

  • 上半部(Top Half):直接响应硬件中断的快速处理
  • 下半部(Bottom Half):延迟执行的耗时操作

下半部也会竞争共享资源,如果要在下半部里面使用自旋锁,可以使用表中的API函数:

示例代码如下:

cpp 复制代码
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/interrupt.h>

static DEFINE_SPINLOCK(bh_lock);  // 定义自旋锁
static int shared_data = 0;       // 共享数据

// 模拟的下半部处理函数
static void bottom_half_handler(unsigned long data)
{
    spin_lock_bh(&bh_lock);      // 关闭下半部并加锁
    printk("BH processing: %d\n", shared_data);
    shared_data++;
    spin_unlock_bh(&bh_lock);    // 释放锁并恢复下半部
}

// 声明tasklet(一种下半部实现)
static DECLARE_TASKLET(my_tasklet, bottom_half_handler, 0);

static int __init bh_lock_demo_init(void)
{
    printk(KERN_INFO "Bottom Half Lock Demo Start\n");

    spin_lock_bh(&bh_lock);      // 关闭下半部并加锁
    shared_data = 100;           // 修改共享数据
    spin_unlock_bh(&bh_lock);    // 释放锁并恢复下半部

    tasklet_schedule(&my_tasklet);  // 触发下半部处理

    return 0;
}

static void __exit bh_lock_demo_exit(void)
{
    tasklet_kill(&my_tasklet);     // 确保tasklet完成
    printk(KERN_INFO "Final shared_data: %d\n", shared_data);
    printk(KERN_INFO "Bottom Half Lock Demo End\n");
}

module_init(bh_lock_demo_init);
module_exit(bh_lock_demo_exit);
MODULE_LICENSE("GPL");

其他类型的锁

读写自旋锁

读写自旋锁是一种​​区分读写操作​​的同步机制,允许多个读者同时访问共享资源,但写者必须独占访问。

Linux 内核使用 rwlock_t 结构体表示读写锁,结构体定义如下:

cpp 复制代码
typedef struct {
        arch_rwlock_t raw_lock;
} rwlock_t;

读写锁操作 API 函数分为两部分,一个是给读使用的,一个是给写使用的。

示例代码如下:

cpp 复制代码
#include <linux/module.h>
#include <linux/rwlock.h>
#include <linux/interrupt.h>

static DEFINE_RWLOCK(data_rwlock);  // 定义并初始化读写锁
static int shared_data = 0;         // 共享数据
static unsigned long irq_flags;      // 保存中断状态

// 模拟中断处理函数
static irqreturn_t sample_irq_handler(int irq, void *dev_id)
{
    // 中断上下文获取读锁(安全版本)
    read_lock_irqsave(&data_rwlock, irq_flags);
    printk("IRQ Read: %d\n", shared_data);
    read_unlock_irqrestore(&data_rwlock, irq_flags);
    
    return IRQ_HANDLED;
}

static int __init rwlock_demo_init(void)
{
    printk(KERN_INFO "Read-Write Spinlock Demo Start\n");

    // 1. 写者模式(完全独占)
    write_lock_bh(&data_rwlock);      // 禁用下半部+获取写锁
    shared_data = 100;                // 安全修改数据
    write_unlock_bh(&data_rwlock);    // 释放写锁+启用下半部

    // 2. 读者模式(并发读取)
    read_lock(&data_rwlock);
    printk("Reader 1: %d\n", shared_data);
    read_unlock(&data_rwlock);

    // 3. 尝试获取写锁(非阻塞)
    if (write_trylock(&data_rwlock)) {
        shared_data += 20;
        write_unlock(&data_rwlock);
    }

    // 注册模拟中断
    request_irq(1, sample_irq_handler, 0, "sample_irq", NULL);

    return 0;
}

static void __exit rwlock_demo_exit(void)
{
    // 中断安全写操作
    write_lock_irqsave(&data_rwlock, irq_flags);
    shared_data = 0;
    write_unlock_irqrestore(&data_rwlock, irq_flags);

    free_irq(1, NULL);
    printk(KERN_INFO "Read-Write Spinlock Demo End\n");
}

module_init(rwlock_demo_init);
module_exit(rwlock_demo_exit);
MODULE_LICENSE("GPL");

顺序锁

顺序锁是一种​​读写共存锁​​,通过版本号机制实现:

  • 写操作:完全独占(类似普通自旋锁)
  • 读操作:无锁访问,通过检测版本变化发现冲突

Linux 内核使用 seqlock_t 结构体表示顺序锁,结构体定义如下:

cpp 复制代码
typedef struct {
    unsigned sequence;       // 版本计数器
    spinlock_t lock;         // 写锁
} seqlock_t;

关于顺序锁的 API 函数如表:

示例代码如下:

cpp 复制代码
#include <linux/module.h>
#include <linux/seqlock.h>
#include <linux/interrupt.h>

static DEFINE_SEQLOCK(data_seqlock);  // 定义并初始化顺序锁
static int shared_data = 0;           // 共享数据
static unsigned long irq_flags;       // 保存中断状态

// 模拟中断处理函数
static irqreturn_t sample_irq_handler(int irq, void *dev_id)
{
    unsigned seq;
    int val;
    
    // 中断上下文读取(无锁)
    do {
        seq = read_seqbegin(&data_seqlock);
        val = shared_data;  // 安全读取
    } while (read_seqretry(&data_seqlock, seq));
    
    printk("IRQ Read: %d\n", val);
    return IRQ_HANDLED;
}

static int __init seqlock_demo_init(void)
{
    printk(KERN_INFO "Seqlock Demo Start\n");

    // 1. 普通写操作
    write_seqlock(&data_seqlock);
    shared_data = 100;
    write_sequnlock(&data_seqlock);

    // 2. 中断安全写操作
    write_seqlock_irqsave(&data_seqlock, irq_flags);
    shared_data += 50;
    write_sequnlock_irqrestore(&data_seqlock, irq_flags);

    // 3. 下半部安全写操作
    write_seqlock_bh(&data_seqlock);
    shared_data *= 2;
    write_sequnlock_bh(&data_seqlock);

    // 4. 读操作(可与其他读者并发)
    unsigned seq;
    int val;
    do {
        seq = read_seqbegin(&data_seqlock);
        val = shared_data;
    } while (read_seqretry(&data_seqlock, seq));
    printk("Final Read: %d\n", val);

    // 注册模拟中断
    request_irq(1, sample_irq_handler, 0, "sample_irq", NULL);

    return 0;
}

static void __exit seqlock_demo_exit(void)
{
    // 禁用中断+写锁
    write_seqlock_irq(&data_seqlock);
    shared_data = 0;
    write_sequnlock_irq(&data_seqlock);

    free_irq(1, NULL);
    printk(KERN_INFO "Seqlock Demo End\n");
}

module_init(seqlock_demo_init);
module_exit(seqlock_demo_exit);
MODULE_LICENSE("GPL");

自旋锁使用注意事项

我们在使用自旋锁的时候要注意一下几点:

  1. 因为在等待自旋锁的时候处于"自旋"状态,因此锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,比如信号量和互斥体。
  2. 自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死锁。
  3. 不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须"自旋",等待锁被释放,然而你正处于"自旋"状态,根本没法释放锁。结果就是自己把自己锁死了!
  4. 在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管我们用的是单核的还是多核的 SOC,都将其当做多核 SOC 来编写驱动程序。
相关推荐
花小璇学linux1 小时前
imx6ull-驱动开发篇16——信号量与互斥体
linux·驱动开发·嵌入式软件
葵野寺1 小时前
【JVM】深入解析Java虚拟机
java·linux·jvm·gc·垃圾回收
Johny_Zhao2 小时前
Rsync + Sersync 实时数据同步方案
linux·网络安全·信息安全·云计算·rsync·系统运维·sersync
zhangxiaomm2 小时前
Ubuntu 搭建 yolov5
linux·yolo·ubuntu
skywalk81633 小时前
Ubuntu24.04启动后显示:推荐安装输入法面板这个Gnome Shell,否则可能无法看到输入法窗口 extension/261/kimpanel
linux·运维·服务器
网硕互联的小客服3 小时前
CentOS8 Stream 网卡配置及重启
linux·运维·服务器
www.023 小时前
在ubuntu服务器下安装cuda和cudnn(笔记)
linux·ubuntu·cuda·cudnn·服务器环境
早睡冠军候选人4 小时前
Linux高级逻辑卷配置
linux·云原生
青草地溪水旁4 小时前
Linux 信号处理标志sa_flags详解
linux·信号处理