当前位置: 首页 > news >正文

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

目录

自旋锁简介

定义自旋锁

自旋锁 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 表示自旋锁,结构体定义如下所示:

/* * 自旋锁核心数据结构定义* 采用联合体(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;

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

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

spinlock_t lock; //定义自旋锁

自旋锁 API 函数

线程之间自旋锁

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

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

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

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

#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 函数:

示例代码如下:

#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函数:

示例代码如下:

#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 结构体表示读写锁,结构体定义如下:

typedef struct {arch_rwlock_t raw_lock;
} rwlock_t;

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

示例代码如下:

#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 结构体表示顺序锁,结构体定义如下:

typedef struct {unsigned sequence;       // 版本计数器spinlock_t lock;         // 写锁
} seqlock_t;

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

示例代码如下:

#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 来编写驱动程序。

http://www.lryc.cn/news/615364.html

相关文章:

  • Orange的运维学习日记--36.NFS详解与服务部署
  • 回答“http协议 ,js组件化,工程化, seo优化策略 ,针对不同平台终端适配 web标注和兼容性”
  • Vue3的简单学习
  • Vuex 数据共享
  • JVM常用参数有哪些?
  • 06.【数据结构-C语言】队列(先进先出,队列实现:入队列、出队列、获取队头or队尾元素,队列实现代码,队列相关题目)
  • idea设置注释--带日期和作者和描述
  • 排序概念以及插入排序
  • Oracle字段操作
  • (nice!!!)(LeetCode 面试经典 150 题) 146. LRU 缓存 (哈希表+双向链表)
  • 在 Vue 中动态引入SVG图标的实现方案
  • STM32 外设驱动模块四:光敏电阻(LDR) 模块
  • 後端開發技術教學(四) 數據交互延伸
  • 2025年渗透测试面试题总结-09(题目+回答)
  • 力扣(轮转数组)
  • 欧拉公式的意义
  • gpt-oss 全量技术解读
  • AI鉴伪技术:守护数字时代的真实性防线
  • 数学学习 | 高数、线代、概率论及数理统计荐书
  • 【C++】set
  • AI热点周报(8.3~8.9):OpenAI重返开源,Anthropic放大招,Claude4.1、GPT5相继发布
  • 第二十八天(cookiesessiontokeny验证)
  • 李宏毅深度学习教程 第16-18章 终身学习+网络压缩+可解释性人工智能
  • STM32学习笔记6-TIM-2输出比较功能
  • 《汇编语言:基于X86处理器》第12章 复习题和练习
  • [每周一更]-(第155期):深入Go反射机制:架构师视角下的动态力量与工程智慧
  • 元宇宙技术如何改变社交方式?
  • (第三篇)spring cloud之Zookeeper注册中心
  • Go 实用指南:如何执行 Skyline 查询(Pareto 最优点筛选)
  • 图片拆分工具,自定义宫格切割