七、(正点原子)Linux并发与竞争

        Linux是多任务操作系统,肯定会存在多个任务共同操作同一段内存或者设备的情况,多个任务甚至中断都能访问的资源叫做共享资源。在驱动开发中要注意对共享资源的保护,也就是要处理对共享资源的并发访问。

一、并发与竞争

        1、简介

        并发就是多个“用户”同时访问同一个共享资源,带来的问题就是竞争问题。Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱。针对这个问题必须要做处理,严重的话可能会导致系统崩溃。现在的 Linux 系统并发产生的原因很复杂,总结一下有下面几个主要原因:

  • 多线程并发访问
  • 抢占式并发访问
  • 中断程序并发访问
  • SMP(多核)间并发访问

        举一个简单的例子:比如说C语言中 a = 3 这样一句代码,它编译时候其实不是一句代码,而是编译成汇编的3句代码:

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

        比如正在执行上面的第二条语句时候,这个时候被其他任务打断,CPU被其他任务占用而且这个任务也是给一个变量赋值 b = 5 那么当这个任务运行到第二步时又被前面 a = 3 的任务打断,那么此时 r1 寄存器的值保存的就不是 3 而是 5 导致出现错误。

        2、保护的内容是什么

        并发是同时访问一个共享资源,那么共享资源是什么呢?说简单一点,共享资源其实就是数据,比如一个全局变量等等,所以找到要保护的数据才是重点,一般像全局变量设备结构体这些肯定是必须保护的对象,其他的数据就要根据实际的驱动而定。

二、原子操作

        1、简介

        原子在化学中是最小的组成,不可再往下分,所以,原子操作就是指不可以再进一步分割的操作,一般原子操作用于变量或位操作。Linux内核提供了两组API函数,一组是对整型(int)变量进行操作,一组是对进性操作。

  • 整型原子操作API

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

        2、原子操作API函数 

        要使用原子操作API函数(定义在include/asm/atomic.h),首先要定义一个atomic_t的原子变量。

函数描述
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_inc_return(atomic_t *v)v自增1,并返回v的值
int atomic_dec_return(atomic_t *v)v自减1,并返回v的值
int atomic_inc_and_test(atomic_t *v)v自增1,如果v==0返回真,否者返回假
int atomic_dec_and_test(atomic_t *v)v自减1,如果v==0返回真,否者返回假
int atomic_sub_and_test(int i,atomic_t *v)v减i值,如果v==0返回真,否者返回假
int atomic_add_negative(int i,atomic_t *v)v加i值,如果v为负返回真,否者返回假

                如果使用的时64位的SOC的话,要使用到64位的原子变量:

         相应的API函数也是一样,将“atomic_”换位“atomic64_”。将“int”换为“long long”

  •         原子位操作API

        位操作也就是将对应的位置1或清零这样的操作。Linux 内核也提供了一系列的原子位操作 API 函数,只不过原子位操作不像原子整形变量那样有个 atomic_t 的数据结构,原子位操作是直接对内存进行操作,API 函数(定义在include/asm/bitops.h):

函数描述
void set_bit(int nr, void *p)将p地址的第nr位置1
void clear_bit(int nr, void *p)pps将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位原来的值

三、自旋锁

        1、简介

        原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中不可能只有整形变量或这么简单的临界区(代码保护区)。举个最简单的例子,设备结构体变量就不是整型变量,我们对于结构体中成员变量的操作也要保证原子性,在线程 A 对结构体变量使用期间,应该禁止其他的线程来访问此结构体变量,这些工作原子操作都不能胜任,需要本节要讲的锁机制,在 Linux内核中就是自旋锁。

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

        自旋锁的“自旋”也就是“原地打转”的意思,“原地打转”的目的是为了等待自旋锁可以用,可以访问共享资源。把自旋锁比作一个变量 a,变量 a=1 的时候表示共享资源可用,当 a=0的时候表示共享资源不可用。现在线程 A 要访问共享资源,发现 a=0(自旋锁被其他线程持有),那么线程 A 就会不断的查询 a 的值,直到 a=1。从这里我们可以看到自旋锁的一个缺点:那就等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长。

        Linux内核使用结构体spinlock_t表示自旋锁,在linux/spinlock_types.h中定义:

        在使用自旋锁之前,要先定义一个自旋锁变量,然后可以使用相应的API函数(定义在include/linux/spinlock.h)来操作自旋锁。

2、自旋锁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)检查lock是否被获取,如果没有被获取返回非0,否则返回0

        自旋锁API适用于SMP(多核)或单CPU支持抢占的线程之间的并发访问,中断中也可以使用自旋锁,但是在中断里面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断(也就是本 CPU 中断,对于多核 SOC来说会有多个 CPU 核),否则可能导致锁死现象的发生。被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数,否则的话会可能会导致死锁现象的发生自旋锁会自动禁止抢占,也就说当线程 A得到锁以后会暂时禁止内核抢占。如果线程 A 在持有锁期间进入了休眠状态,那么线程 A 会自动放弃 CPU 使用权。线程 B 开始运行,线程 B 也想要获取锁,但是此时锁被 A 线程持有,而且内核抢占还被禁止了!线程 B 无法被调度出去,那么线程 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 falgs)保存中断状态,禁止本地中断,并自锁
void spin_unlock_irqrestore(spinlock_t *lock,unsigned long falgs)将中断恢复原来状态,激活本地中断,并解锁

        3、其他类型的锁

  •         读写自旋锁

        我们的数据使用自旋锁对其保护时,每次只能一个读和写操作,但是,实际上是可以多个线程并发读取数据的。只需要保证在修改数据的时候没有线程读取,或者读取时,没有线程在修改数据。也就是渡河写操作不能同时进性。所以引入了读写自旋锁。

        读写自旋锁为读操作和写操作提供了不同的锁,一次只能允许一个写操作,在进性写操作时,不能进性读操作。可以多线程一起读操作,但读操作时不能进性写操作。Linux内核中定义了rwlock_t结构体(定义在include/linux/rwlock_types.h中)表示读写锁操作:

        读写自旋锁的API函数(定义在include/linux/rwlock.h):

函数描述
DEFINE_RWLOCK(rwlock_t lock)定义并初始化读写锁
void rwlock_init(rwlock_t *lock)初始化读写锁
读锁
void read_lock(rwlock_t *lock)获取读锁
void read_unlock(rwlock_t *lock)释放读锁
void read_lock_irq(rwlock_t *lock)禁止本地中断,并且获取读锁
void read_unlock_irq(rwlock_t *lock)打开本地中断,并且释放读锁
void read_lock_irqsave(rwlock_t *lock, unsigned        long flags)保存中断状态,禁止本地中断,并获取读锁
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags)将中断状态恢复到以前的状态,并且激活本地中断,释放读锁
写锁
void write_lock(rwlock_t *lock)获取写锁
void write_unlock(rwlock_t *lock)释放写锁
void write_lock_irq(rwlock_t *lock)禁止本地中断,并且获取写锁
void write_unlock_irq(rwlock_t *lock)打开本地中断,并且释放写锁
void write_lock_irqsave(rwlock_t *lock, unsigned long flags)保存中断状态,禁止本地中断,并获取写锁
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags)将中断状态恢复到以前的状态,并且激活本地中断,释放读锁
  •         顺序锁

        顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。虽然顺序锁的读和写操作可以同时进行,但是如果在读的过程中发生了写操作,最好重新进行读取,保证数据完整性。顺序锁保护的资源不能是指针,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如读取野指针导致系统崩溃。

        Linux 内核使用 seqlock_t 结构体(定义在include/linux/seqlock.h)表示顺序锁,结构体定义如下:

         关于顺序锁的API函数(定义在include/linux/seqlock.h):

函数描述
DEFINE_SEQLOCK(seqlock_t sl)定义并初始化顺序锁
void seqlock_ini seqlock_t *sl)初始化顺序锁
顺序表写操作
void write_seqlock(seqlock_t *sl)获取写顺序锁
void write_sequnlock(seqlock_t *sl)释放写顺序锁
void write_seqlock_irq(seqlock_t *sl)禁止本地中断,并且获取写顺序锁
void write_sequnlock_irq(seqlock_t *sl)打开本地中断,并且释放写顺序锁
void write_seqlock_irqsave(seqlock_t *sl, unsigned long flags)保存中断状态,禁止本地中断,并获取写顺序锁
void write_sequnlock_irqrestore(seqlock_t *sl, unsigned long flags)将中断状态恢复到以前的状态,并且激活本地中断,释放写顺序锁
顺序表读操作
unsigned read_seqbegin(const seqlock_t *sl)读单元访问共享资源的时候调用此函数,此函数会返回顺序锁的顺序号
unsigned read_seqretry(const seqlock_t *sl,
unsigned start)
读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读

        4、自旋锁使用注意事项

  •    因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短,否则的     话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,     比如稍后要讲的信号量和互斥体。 
  •    自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死       锁。    
  •    不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须      “自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。所以就死锁
  •    在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管你用的是单核的还是多核     的 SOC,都将其当做多核 SOC 来编写驱动程序。

四、信号量

        1、简介

        这里的信号量与FreeRTOS或者UCOS的信号量一样,常常用于控制对共享资源的访问。比如停车位举例:有10个停车位,信号量为10,当有一辆车进来停车,停车位减一,相当于一个线程读取数据一次,信号量减1。当停车位停满了,信号量减为零,就不能再停下一辆车,线程不能再读取数据,只能等别的车离开,才能停车,相当于线程读取数据完成后,信号量加一。此时,又可以停车。

        相比于自旋锁,信号量可以时线程进入休眠状态,比如说:A现在再厕所上厕所,这个时候B也想上厕所,但是看到厕所现在A在使用,B就无法使用,B可以一直在厕所等待,就相当于自旋锁。或者B可以先离开去干其他的事,等A上完厕所后再通知B让B去上厕所,就相当于信号量线程可以进入休眠状态。

        信号量特点

  •         因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场
  •         信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
  •         如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线            程引起的开销要远大于信号量带来的那点优势。
  •         当信号量的值设置为1时,为二值信号量,可以互斥访问。当信号量的值大于1时,为计          数型信号量,不可以互斥访问。

        2、信号量API函数

        Linux 内核使用 semaphore 结构体(定义在include/linux/semaphore.h)表示信号量:

         使用信号量之前先定义和初始化信号量结构体,再使用信号量API函数(定义在include/linux/semaphore.h):

函数描述
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)释放信号量

四、互斥体

        1、简介

        将信号量的值设置为 1 就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行互斥,它就是互斥体—mutex
        互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。Linux内核使用mutex结构体(定义在include/linux/mutex.h)表示互斥体

         注意事项:

  •         mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁
  •         和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数
  •         因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。            并且 mutex 不能递归上锁和解锁

2、互斥体API函数

函数描述
DEFINE_MUTEX(name)定义并初始化一个 mutex 变量
void mutex_init(mutex *lock)初始化 mutex
void mutex_lock(struct mutex *lock)获取 mutex,也就是给 mutex 上锁。如果获取不到就进休眠
void mutex_unlock(struct mutex *lock)释放 mutex,也就给 mutex 解锁
int mutex_trylock(struct mutex *lock)尝试获取 mutex,如果成功就返回 1,如果失败就返回 0
int mutex_is_locked(struct mutex *lock)判断 mutex 是否被获取,如果是的话就返回1,否则返回 0
int mutex_lock_interruptible(struct mutex *lock)使用此函数获取信号量失败进入休眠以后可以被信号打断

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机行业的现状与未来之2024

年年都说编程好,编程工资涨不了。 人家骑车送外卖,月入两万好不好。 一、计算机专业的背景与现状 在过去几十年里,计算机科学相关专业一直是高考考生的热门选择。无论是计算机科学与技术、软件工程,还是人工智能与大数据&#xff…

PR软件视频抠图换背景

1 新建项目 2 新建序列 在项目的右下角有个图标,新建 序列 序列是视频的制作尺寸,根据自己的需要选择 3 新建颜色遮罩 在项目的右下角--新建颜色遮罩--选择黑色--确定 4 导入视频 把要导入视频的文件夹打开,把视频拖到 项目 里 把黑色遮罩拖…

#QT(QCharts绘制曲线)

1.IDE:QTCreator 2.实验:绘制曲线图表 3.记录: 4.代码 pro QT core gui #加入以下代码引入charts QT charts greaterThan(QT_MAJOR_VERSION, 4): QT widgetsCONFIG c17# You can make your code fail to compile if it uses depre…

jdk下载安装及电脑上同时安装多个jdk

一、jdk的下载 官方地址: Java Software | Oracle 系统环境变量配置 1、首先新建JDK1.8和17的JAVA_HOME,他们的变量名区分开,分别为JAVA17_HOME,JAVA8_HOME。分别指向他们的安装地址。 2、新建配置他们可变的地址,这个环境变量的值…

Java实现一个解析CURL脚本小工具

该工具可以将CURL脚本中的Header解析为KV Map结构;获取URL路径、请求类型;解析URL参数列表;解析Body请求体:Form表单、Raw Body、KV Body、XML/JSON/TEXT结构体等。 使用示例 获取一个http curl脚本: curl --locatio…

Blazor 中基于角色的授权

介绍 Blazor用于使用 .NET 代码库创建交互式客户端 Web UI。Microsoft 默认在 Blazor 应用程序中提供了一个用于身份验证和授权的身份框架。请注意,他们目前使用 MVC Core Razor 页面作为身份验证 UI。使用“Microsoft.AspNetCore.Identity.UI”包库来实现这一点。…

学会python——制作一款天气查询工具(python实例七)

目录 1、认识Python 2、环境与工具 2.1 python环境 2.2 Visual Studio Code编译 3、天气查询工具 3.1 代码构思 3.2 代码示例 3.3 运行结果 4、总结 1、认识Python Python 是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。 Python 的设计具有很强的…

奇怪的缓存一致性问题

天猫国际用户Push中心承接了国际用户触达相关的需求,比如短信、端内消息投放等等,并存在较高的并发场景。 该系统此前发现了一个查询投放计划plan为null的异常情况,在初期排查时有些丈二和尚摸不着头脑,后面突然灵光乍现——原来是…

网络层 IP协议【计算机网络】【协议格式 || 分片 || 网段划分 || 子网掩码】

博客主页:花果山~程序猿-CSDN博客 文章分栏:Linux_花果山~程序猿的博客-CSDN博客 关注我一起学习,一起进步,一起探索编程的无限可能吧!让我们一起努力,一起成长! 目录 一,前提 二&…

【Python】类和对象的深入解析

目录 前言 什么是类? 定义一个类 创建对象 访问和修改属性 方法 类的继承 多态 封装 特殊方法 属性装饰器 总结 前言 Python 是一种面向对象的编程语言,它允许程序员通过类和对象来组织和管理代码。面向对象编程(OOP&#xff09…

被腰斩的颍川郡守赵广汉

在颍川,他发明了举报箱,铁腕扫黑除恶。因为曾经在郡府所在地阳翟(禹州)当过县令,熟悉颍川社情民意,所以,任职郡守后雷厉风行,才不到一年,不但制服了骄横的豪门大族&#…

1.0 Android中Activity的基础知识

一:Activity的定义 Activity是一个应用组件,它提供了一个用户界面,允许用户执行一个单一的、明确的操作,用户看的见的操作都是在activity中执行的。Activity的实现需要在manifest中进行定义,不让会造成程序报错。 1.…

关机充电动画:流程与定制

关机充电动画:流程与定制 基于MTK平台Android 11分析 生成logo.bin 关机充电动画是由一系列的bmp图片组成的,这些图片资源存在于vendor/mediatek/proprietary/bootable/bootloader/lk/dev/logo目录下(当然不仅保护关机充电动画&#xff0c…

中医药人工智能大模型正式启动

6月15日,在横琴粤澳深度合作区举行的中医药广东省实验室(以下简称横琴实验室)第一届学术委员会第一次会议暨首届横琴中医药科技创新大会上,中医药横琴大模型、中药新药智能自动化融合创新平台同时启动。这也是该实验室揭牌半年来取…

Stable Diffusion WebUI 使用ControlNet:IP-Adapter保持生图的角色一致性

IP-Adapter-FaceID可以在保持人脸一致的条件下生成各种风格的图像。 下载 IP Adapter 需要的 Face ID 模型和 Lora 下载地址:https://huggingface.co/h94/IP-Adapter-FaceID/ 下载 ip-adapter-faceid-plusv2_sd15.bin 和 ip-adapter-faceid-plusv2_sd15_lora.saf…

瑞典农业科学大学《Nature Geoscience》(IF=18)!揭示北方森林碳汇对干旱的响应机制!

本文首发于“生态学者”微信公众号! 北方森林覆盖了地球陆地面积的11%,储存了全球陆地碳储量的约三分之一。因此,它们被认为是减缓气候变化政策的一个重要因素。然而,环极寒带地区是气候变化速度最快的地区。这包括更频繁和更严重…

入门 Axure RP 9 | 原型设计基础教程

选择正确的原型设计工具并非易事,Axure RP 9能够快速完成原型设计。原型设计是一种经过时间考验的方法,可以将你的设计快速放置在用户的设备并交到他们手中。替代Axure RP 9的原型设计工具即时设计是一个完全集成的协同设计工具,无需使用不同…

计算机专业毕设-校园二手交易平台

1 项目介绍 基于SpringBoot的校园二手交易平台:前端Freemarker,后端 SpringBoot、Jpa,系统用户分为两类,管理员、学生,具体功能如下: 管理员: 基本功能:登录、修改个人信息、修改…

“AI引擎”澎湃动力!深圳机场携手实在智能打造民航财务数字员工

近日,深圳机场集团(000089)与实在智能达成合作,引入了业内领先的平台级自动化产品——实在RPA数字员工,部署实在智能集“自动化平台开箱即用解决方案咨询”为一体的数字员工解决方案,实现纳税申报、报表制作…

海思SS928/SD3403开发笔记1——使用串口调试开发板

该板子使用串口可以调试,下面是win11 调试 该板子步骤 1、给板子接入鼠标、键盘、usb转串口 2、下载SecureCRT,并科学使用 下载地址: 链接:https://pan.baidu.com/s/11dIkZVstvHQUhE8uS1YO0Q 提取码:vinv 3、安装c…