Linux驱动开发——(三)并发与竞争

news2024/9/20 3:27:28

目录

一、并发与竞争简介

二、原子操作

2.1 原子操作简介

2.2 原子整形操作API

2.3 原子位操作API

2.4 原子操作驱动代码

三、自旋锁 

3.1 自旋锁简介

3.2 自旋锁API

3.3 自旋锁驱动代码

四、信号量

4.1 信号量简介

4.2 信号量API

4.3 信号量驱动代码 


一、并发与竞争简介

Linux系统是个多线程操作系统,会存在多个线程同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱,重者可能会导致系统崩溃。Linux系统并发访问产生的原因很复杂,总结有以下原因:

①多线程并发访问,这是最基本的原因。

②抢占式并发访问。

③中断程序并发访问。

④SMP/多核核间并发访问。

并发访问导致竞争,对于临界区(共享数据段)必须保证一次只有一个线程访问,也就是要保证临界区是原子访问(原子即是不可再分的基本微粒原子访问就表示这一个访问是一个步骤,不能再进行拆分)的。如果多个线程同时操作临界区就表示存在竞争,编写驱动的时候一定要注意避免并发和防止竞争访问!


二、原子操作

2.1 原子操作简介

原子操作指不能再进一步分割的操作,一般原子操作用于变量或者位操作。

假如现在要对无符号整形变量a赋值为3,对于C语言就是:

a = 3

但在C语言编译为汇编指令后,因为ARM架构不支持直接对寄存器进行读写操作,要借助寄存器 R0、 R1等来完成赋值操作,所以编译后的汇编指令:

ldr r0, =0X30000000 /* 变量a地址 */ 
ldr r1, = 3 /* 要写入的值 */ 
str r1, [r0] /* 将3写入到a变量中 */

假设现在线程A要向a变量写入10这个值,而线程B也要向a变量写入20这个值,我们理想中的执行顺序如图:

上图确实可以实现线程A将a变量设置为10,线程B将a变量设置为20。但是实际上的执行流程可能如图: 

线程A最终将变量a设置为了20,而并不是要求的10。 要解决这个问题就要保证三行汇编指令作为一个整体运行,也就是作为一个原子存在。

2.2 原子整形操作API

Linux内核定义了叫做atomic_t的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在include/linux/types.h文件中,定义如下:

typedef struct {
    int counter; 
} atomic_t;

如果要使用原子操作API函数,首先要先定义一个atomic_t 的变量:

atomic_t a; //定义a

也可以在定义原子变量的时候给原子变量赋初值:

atomic_t b = ATOMIC_INIT(0); //定义原子变量b 并赋初值为0

还有读、写、增加、减少等等:

函数描述
ATOMIC_INIT(int i)定义原子变量的时候对其初始化。
int atomic_read(atomic_t *v)读取v的值,并且返回。
void atomic_set(atomic_t *v, int i)向v写入i值。
void atomic_add(int i, atomic_t *v)给v加上i值。
void atomic_sub(int i, atomic_t *v)从v减去i值。
void atomic_inc(atomic_t *v)给v加1,也就是自增。
void atomic_dec(atomic_t *v)从v减1,也就是自减。
int atomic_dec_return(atomic_t *v)从v减1,并且返回v的值。
int atomic_inc_return(atomic_t *v)给v加1,并且返回v的值。
int atomic_sub_and_test(int i, atomic_t *v)从v减i,如果结果为0就返回真,否则返回假
int atomic_dec_and_test(atomic_t *v)从v减1,如果结果为0就返回真,否则返回假
int atomic_inc_and_test(atomic_t *v)给v加 1,如果结果为0就返回真,否则返回假
int atomic_add_negative(int i, atomic_t *v)给v加i,如果结果为负就返回真,否则返回假

2.3 原子位操作API

位操作也是很常用的操作,它不像原子整形变量那样有个atomic_t的数据结构,而是直接对内存进行操作:

函数描述
void set_bit(int nr, void *p)将p地址的第nr位置1。
void clear_bit(int nr,void *p)将p地址的第nr位清零。
void change_bit(int nr, void *p)将p地址的第nr位进行翻转。
int test_bit(int nr, void *p)获取p地址的第nr位的值。
int test_and_set_bit(int nr, void *p)将p地址的第nr位置1,并且返回nr位原来的值。
int test_and_clear_bit(int nr, void *p)将p地址的第nr位清零,并且返回nr位原来的值。
int test_and_change_bit(int nr, void *p)将p地址的第nr位翻转,并且返回nr位原来的值。

2.4 原子操作驱动代码

先定义全局变量:

atomic_t lock;

open操作函数里添加以下代码:

/* 通过判断原子变量的值来检查有没有被别的应用使用 */ 
if (!atomic_dec_and_test(&gpioled.lock)) { 
    atomic_inc(&gpioled.lock);/* 小于0的话就加1,使其原子变量等于0 */ 
    return -EBUSY; /* LED被使用,返回忙 */ 
}

release操作函数里添加以下代码:

/* 关闭驱动文件的时候释放原子变量 */
atomic_inc(&dev->lock);

驱动入口函数里添加以下代码:

/* 初始化原子变量 */
atomic_set(&gpioled.lock, 1); /* 原子变量初始值为1 */

三、自旋锁 

3.1 自旋锁简介

原子操作只能对整形变量或者位进行保护,但事实上临界区不止有这些类型,比如还有设备结构体变量——自旋锁便可胜任。

当一个线程要访问某个共享资源的时候首先要先获取相应的锁,锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。对于自旋锁而言,如果自旋锁正在被线A持有,线程B想要获取自旋锁,那么线程B就会处于忙循环-旋转-等待状态,线程B不会进入休眠状态或者说去做其他的处理,而是会一直等待锁。

3.2 自旋锁API

Linux内核使用结构体spinlock_t表示自旋锁:

typedef struct spinlock { 
    union { 
        struct raw_spinlock rlock; 

#ifdef CONFIG_DEBUG_LOCK_ALLOC 
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map)) 
    struct { 
        u8 __padding[LOCK_PADSIZE];
        struct lockdep_map dep_map; 
    }; 
#endif 
    }; 
} spinlock_t;

最基本的自旋锁API函数:

函数描述
DEFINE_SPINLOCK(spinlock_t lock)定义并初始化一个自选变量。
int spin_lock_init(spinlock_t *lock)初始化自旋锁。
void spin_lock(spinlock_t *lock)获取指定的自旋锁,也叫做加锁。
void spin_unlock(spinlock_t *lock)释放指定的自旋锁。
int spin_trylock(spinlock_t *lock)尝试获取指定的自旋锁,如果没有获取到就返回0。
int spin_is_locked(spinlock_t *lock)

检查指定的自旋锁是否被获取,如果没有被获取就返回非0否则返回0。

自旋锁API函数适用于用于线程与线程之间,被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API函数,否则的话会可能会导致死锁现象的发生。自旋锁会自动禁止抢占

中断里面可以使用自旋锁,但是在中断里面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断(也就是本CPU中断,对于多核SOC来说会有多个CPU核),否则可能导致锁死现象的发生。

线程A 先运行,并且获取到了lock 这个锁,当线程A 运行functionA函数的时候中断发生了,中断抢走了CPU使用权。右边的中断服务函数也要获取lock这个锁,但是这个锁被线程A占有着,中断就会一直自旋,等待锁有效。最好的解决方法就是获取锁之前关闭本地中断,Linux内核提供了相应的内核提供了相应的API: 

函数描述
void spin_lock_irq(spinlock_t *lock)禁止本地中断,并获取自旋锁。
void spin_unlock_irq(spinlock_t *lock)激活本地中断,并释放自旋锁。
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags)保存中断状态,禁止本地中断,并获取自旋锁。
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。

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

DEFINE_SPINLOCK(lock) /* 定义并初始化一个锁 */ 

/* 线程A */ 
void functionA (){ 
    unsigned long flags; /* 中断状态 */ 
    spin_lock_irqsave(&lock, flags) /* 获取锁 */ 
    /* 临界区 */ 
    spin_unlock_irqrestore(&lock, flags) /* 释放锁 */ 
} 

/* 中断服务函数 */ 
void irq() { 
    spin_lock(&lock) /* 获取锁 */ 
    /* 临界区 */ 
    spin_unlock(&lock) /* 释放锁 */ 
}

3.3 自旋锁驱动代码

先定义全局变量:

int dev_stats; /* 设备状态,0,设备未使用;>0,设备已经被使用 */
spinlock_t lock; /* 自旋锁 */

open操作函数里添加以下代码:

spin_lock_irqsave(&gpioled.lock, flags); /* 上锁 */ 
if (gpioled.dev_stats) { /* 如果设备被使用了 */
    spin_unlock_irqrestore(&gpioled.lock, flags); /* 解锁 */ 
    return -EBUSY; 
} 
gpioled.dev_stats++; /* 如果设备没有打开,那么就标记已经打开了 */ 
spin_unlock_irqrestore(&gpioled.lock, flags);/* 解锁 */

release操作函数里添加以下代码: 

spin_lock_irqsave(&dev->lock, flags); /* 上锁 */ 
if (dev->dev_stats) { 
    dev->dev_stats--; 
} 
spin_unlock_irqrestore(&dev->lock, flags);/* 解锁 */

驱动入口函数里添加以下代码:

/* 初始化自旋锁 */
spin_lock_init(&gpioled.lock);

四、信号量

4.1 信号量简介

相比于自旋锁,信号量可以使线程进入休眠状态,不会让线程一直等待。但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。信号量总结:

①因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。

②因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。

③如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。

在初始化的时候将信号量值设置的大于1,那么这个信号量就是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。如果要互斥的访问共享资源那么信号量的值就不能大于1,此时的信号量就是一个二值信号量

4.2 信号量API

Linux内核使用semaphore结构体表示信号量:

struct semaphore { 
    raw_spinlock_t lock; 
    unsigned int count; 
    struct list_head wait_list; 
};

信号量的API函数:

函数描述
DEFINE_SEAMPHORE(name)定义一个信号量,并且设置信号量的值为1。
void sema_init(struct semaphore *sem, int val)初始化信号量sem,设置信号量值为 val。
void down(struct semaphore *sem)获取信号量,因为会导致休眠,因此不能在中断中使用。
int down_trylock(struct semaphore *sem);尝试获取信号量,如果能获取到信号量就获取,并且返回0。如果不能就返回非0,并且不会进入休眠。
int down_interruptible(struct semaphore *sem)获取信号量,和down类似,只是使用 down进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的。
void up(struct semaphore *sem)        释放信号量

 信号量的使用如下所示:

struct semaphore sem; /* 定义信号量 */ 
sema_init(&sem, 1); /* 初始化信号量 */ 
down(&sem); /* 申请信号量 */ 
/* 临界区 */ 
up(&sem); /* 释放信号量 */

4.3 信号量驱动代码 

头文件加入以下代码:

#include <linux/semaphore.h>

定义全局变量:

struct semaphore sem; /* 信号量 */

 在open操作函数里添加以下代码:

/* 获取信号量,进入休眠状态的进程可以被信号打断 */ 
if (down_interruptible(&gpioled.sem)) { 
    return -ERESTARTSYS; 
} 
#if 0 
    down(&gpioled.sem); /* 不能被信号打断 */ 
#endif

release操作函数里添加以下代码: 

up(&dev->sem); /* 释放信号量,信号量值加1 */

驱动入口函数里添加以下代码:

/* 初始化信号量 */
sema_init(&gpioled.sem, 1);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1612868.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringCloud系列(4)--SpringCloud微服务工程构建

前言&#xff1a;在上节我们新建了一个SpringCloud父工程&#xff0c;这一节主要是构建微服务工程&#xff0c;通过实现订单模块和支付模块来熟悉微服务的概念和构建过程。 1、在父工程下新建模块 2、选择模块的项目类型为Maven并选择模块要使用的JDK版本 3、填写子模块的名称&…

算法|最大堆、最小堆和堆排序的实现(JavaScript)

一些概念 堆&#xff1a;特殊的完全二叉树&#xff0c;具有特定性质的完全二叉树。大根堆&#xff1a;父节点 > 子节点小根堆&#xff1a;父节点 < 子节点 二叉堆也属于完全二叉树&#xff0c;所以可以用数组表示。 若下标从1开始&#xff0c;左节点为 2*i &#xff0…

Java的Future机制详解

Java的Future机制详解 一、为什么出现Future机制二、Future的相关类图2.1 Future 接口2.2 FutureTask 类 三、FutureTask的使用方法四、FutureTask源码分析4.1 state字段4.2 其他变量4.4 构造函数4.5 run方法及其他 一、为什么出现Future机制 常见的两种创建线程的方式。一种是…

高架学习笔记之软件架构风格

目录 零、什么是软件架构风格 一、常见的软件架构风格 二、数据流风格 2.1. 批处理风格 2.2. 管道-过滤器风格 三、调用/返回风格 3.1. 主/子程序风格 3.2. 面向对象风格 3.3. 层次型风格 3.4. 客户端/服务器风格 3.4.1. 两层C/S体系结构 3.4.2. 三层C/S体系结构 …

MBD_入门篇_20_Simulink子系统

20.Simulink子系统 20.1 概述 Simulink的子系统&#xff0c;相当于代码的function函数&#xff0c;但是模型的子系统又不完全等效于代码的函数。虚拟子系统并不会生成函数&#xff0c;而是以代码块的形式放在相应的调用位置上。模型层面我们使用子系统去做模块化的设计&#xf…

Mini-Gemini: 探索多模态视觉语言模型的新境界

一、背景 在数字化时代&#xff0c;人工智能的发展正以前所未有的速度推进。特别是在多模态学习领域&#xff0c;结合视觉和语言的能力已成为研究的热点。最近&#xff0c;一篇名为“Mini-Gemini: Mining the Potential of Multi-modality Vision Language Models”的文章在arX…

[已解决]react打包部署

react打包部署 问题 npm install 命令无反应 思路 换成 yarn install 安装完hadoop的环境后&#xff0c;使用node的yarn会报错&#xff1a; 我们在cmd使用where yarn&#xff0c;如下&#xff1a; 看你想保留哪一个&#xff0c;我平时node用的多&#xff0c;就把hadoop的y…

飞书API(5):查看多维表 28 种数据类型的数据结构

一、引入 前面我们用于测试的数据集其实都是比较常用的数据&#xff0c;比如说文本、数字、单选等&#xff0c;但飞书多维表并不仅仅只有这些数据&#xff0c;截止发文&#xff0c;飞书多维表应用上支持28种数据类型&#xff0c;在数据层面飞书官方只提供了23种数据类型&#…

Cadence软件安装

Cadence软件 iscape 用于安装cadence家的安装软件 解压缩安装包tar -xvf IScape04.23.tar.gz运行bash IScape/iscape/bin/iscape.sh 设置默认安装路径(可选)IC618 这里使用的是IC618.320版本作为示例,其他版本安装过程差不多 安装 首先安装终端模拟器,不然安装之后会失败…

【前端】校园二手书交易系统javascript+css+html (源码)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

Vnode是如何产生的?

源码 流程图 源码解读 Vue.js2.0中有两种生成方式&#xff1a;第一种是直接在Vue对象的option中添加render字段&#xff1b;第二种是像Vue.js 1.x版本那样写一个模板或者指定一个el根元素&#xff0c;它会首先转换成模板&#xff0c;经过HTMI语法解析器生成一个 ast 抽象语法树…

JAVAEE——IP协议

文章目录 IP协议IP协议报头格式IP协议报头的各个区段四位版本四位首部长度八位服务类型16位总长度16位标识&#xff0c;3位标志&#xff0c;13位片偏移八位生存时间八位协议 地址管理IP地址解决提议1&#xff1a;动态分配Ip地址解决提议2&#xff1a;NAT机制 IP协议 IP协议报头…

【新手入门必看】从零开始学指针

我使用VS CODEMSYS2的编译环境进行学习&#xff0c;想使用VS CODE进行C/C代码编写的小伙伴参考这篇文章进行环境配置VS Code 配置 C/C 编程运行环境&#xff08;保姆级教程&#xff09; 一、指针的引入 指针地址 #include <stdio.h>int main() {int a 10;printf(&quo…

编写函数fun,函数的功能是:根据以下公式计算s,计算结果作为函数值返回;n通过形参传入。

本文收录于专栏:算法之翼 https://blog.csdn.net/weixin_52908342/category_10943144.html 订阅后本专栏全部文章可见。 本文含有题目的题干、解题思路、解题思路、解题代码、代码解析。本文分别包含C语言、C++、Java、Python四种语言的解法完整代码和详细的解析。 题干 编写…

Java:二叉树(1)

从现在开始&#xff0c;我们进入二叉树的学习&#xff0c;二叉树是数据结构的重点部分&#xff0c;在了解这个结构之前&#xff0c;我们先来了解一下什么是树型结构吧&#xff01; 一、树型结构 1、树型结构简介 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>…

深度学习系列64:数字人openHeygen详解

1. 主流程分析 从inference.py函数进入&#xff0c;主要流程包括&#xff1a; 1&#xff09; 使用cv2获取视频中所有帧的列表&#xff0c;如下&#xff1a; 2&#xff09;定义Croper。核心代码为69行&#xff1a;full_frames_RGB, crop, quad croper.crop(full_frames_RGB)。…

openobserve-filebeat配置

优势 rustgolang开发的日志工具组合&#xff0c;自带日志数据存储&#xff0c;简化部署和管理。日志数据可配置保留x天。从日志文件中采集&#xff0c;做到非侵入式日志集中管理。 可从日志内容中提取信息进行报警等二次开发。 下载 openobserve-v0.10.1-windows-amd64 fil…

VL02N交货单清除字段:VLSTK(分配状态)

VL02N交货单清除字段&#xff1a;VLSTK(分配状态) 通过查找增强对应的BADI&#xff1a;LE_SHP_DELIVERY_PROC 修改方法&#xff1a;IF_EX_LE_SHP_DELIVERY_PROC~CHANGE_DELIVERY_HEADER&#xff0c;代码如下&#xff1a;

JSS作业

JSS作业&#xff1a; 1: <script>var cnt parseInt(window.prompt("请输入打印的行数&#xff1a;"));for (var i 1; i < cnt; i){for (var j 1; j < i; j){document.write("*")}document.write("<br>")} </script>…

XTuner 微调 LLM:1.8B、多模态、Agent

两种微调范式&#xff1a;增量预训练和指令微调 在大语言模型下游应用中&#xff0c;主要有两种微调范式&#xff1a;增量预训练和指令微调。增量预训练旨在让模型学习特定领域的常识&#xff0c;而不需要有监督标注的数据&#xff1b;指令微调则是通过高质量的对话数据训练模型…