Linux驱动开发(速记版)--并发与竞争

news/2024/11/17 3:43:54/

第十八章 并发与竞争

18.1 并发与竞争

18.1.1 并发

        早期计算机 CPU单核心时,由于 CPU执行速度快于I/O操作,常因等待 I/O而空闲

        为提高 CPU利用率,引入了并发执行理论。并发通过算法在CPU执行I/O等待时切换至其他任务,使多个任务看似同时执行,实际上CPU快速切换任务执行,从而提高了整体效率。

18.1.2 并行

        并发适用于单核CPU,通过任务切换模拟同时执行;

        并行针对多核CPU,每个核心独立执行不同任务,实现真正的同时运行,提高执行效率。

18.1.3 并发+并行

        实际场景中,CPU核心数少于任务数时,会同时出现并发(单核内任务切换)和并行(多核间任务独立执行)。

        例如,双核CPU执行四个任务时,每核并行执行一个任务,同时每核内部还需并发处理其他任务。

        为简化讨论,后续章节将并发与并行统称为并发。

18.1.4 竞争

        并发访问共享资源时可能导致的不一致性称为“竞争条件”。

        这种竞争由以下主要原因造成:

        多线程并发:在Linux等多任务操作系统中,多个线程(或进程)可能同时执行,它们尝试访问和修改同一资源时,若未进行适当同步,便会产生竞争条件。

        中断处理:系统中断(如硬件中断)能够打断当前正在执行的程序,如果中断服务例程(ISR)也访问了被打断程序正在使用的共享资源,且未做适当同步,将导致数据竞争。

        抢占式调度:在支持抢占式调度的系统中(如Linux 2.6及以后版本),高优先级任务可以中断低优先级任务的执行。若两者均操作同一共享资源,且未采取同步措施,则会发生竞争。

        多处理器并发:在多处理器(SMP)系统中,不同处理器核心可能并发执行不同线程,这些线程若同时访问同一共享资源而未同步,将引发跨核心的竞争条件。

        简而言之,并发访问共享资源时,若未加同步控制,多线程、中断处理、抢占式调度及多处理器并发均可能导致竞争条件的发生。

18.1.5 共享资源的保护

        共享资源是多个实体可访问的资源,为避免竞争需同步保护。

        比如同一个驱动里面,写两个方法,一个方法延时4秒,另一个方法延时2秒,两个方法读取同一个变量。装载驱动以后,用write方法写驱动两次,由于驱动装载后以文件形式存在,属于共享资源,会导致竞争条件,出现数据不一致。

第十九章 原子操作

19.1 原子操作

        在 Linux内核中,原子操作是确保数据在并发环境下一致性的关键机制。原子操作意味着操作在执行过程中不会被中断,从而避免了竞争条件。

19.1.1 原子整型操作

        在Linux内核中,atomic_tatomic64_t 分别用于32位和64位系统的原子整形操作。

        原子整型操作基于 内存屏障来确保原子性,

        Load Barrier 和 Store Barrier即读屏障和写屏障。

typedef struct { int counter; } atomic_t;  
#ifdef CONFIG_64BIT  
typedef struct { long counter; } atomic64_t;  
#endif
ATOMIC_INIT(int i);                          //初始化原子变量,并返回原子整型变量。
int atomic_read(atomic_t *v);                //读取原子变量的值。
void atomic_set(atomic_t *v, int i);         //设置原子变量的值。
void atomic_add(int i, atomic_t *v);         //原子地将i加到v上。
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,并返回新值。
int atomic_inc_return(atomic_t *v);          //原子地将v加1,并返回新值。
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); //原子地将i加到v上,如果结果为负则返回真。

19.1.2 原子位操作

        原子位操作是直接对内存中的位进行操作,无需 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,并返回原值。
int test_and_clear_bit(int nr, void *p); //将p地址的第nr位清零,并返回原值。
int test_and_change_bit(int nr, void *p);//将p地址的第nr位翻转,并返回原值。

        使用了原子操作后,同一时间只允许一个应用来打开设备节点,以此防止共享资源竞争的产生。第二次打开同一个设备节点会报错。

第二十章 自旋锁

        原子操作只能对整形变量或者位进行保护,对于结构体或者其他类型的共享资源,就轮到自旋锁的出场了。

        自旋锁是一种用于保护共享资源的非阻塞锁机制。当某个线程尝试获取已被占用的锁时,该线程会持续消耗CPU资源,不断尝试获取锁,而不是被挂起。这种机制适用于锁持有时间非常短的场景,以避免线程切换的开销。

        内核中以 spinlock_t 结构体来表示自旋锁,定义在“内核源码/include/linux/spinlock_types.h” 文件中。

DEFINE_SPINLOCK();        //定义并初始化自旋锁。
spin_lock_init();                   //初始化自旋锁。
spin_lock();                          //尝试获取锁,若失败则持续自旋。
spin_unlock();                      //释放锁。 
spin_trylock();                      //尝试获取锁,若失败则立即返回0。
spin_is_locked();                 //检查锁是否被占用。

//定义并初始化自旋锁。
DEFINE_SPINLOCK(spinlock_t lock); //初始化自旋锁。
spin_lock_init(spinlock_t *lock);//尝试获取锁,若失败则持续自旋。
spin_lock(spinlock_t *lock);    //释放锁。 
spin_unlock(spinlock_t *lock);    //尝试获取锁,若失败则立即返回0
spin_trylock(spinlock_t *lock);  //检查锁是否被占用。
spin_is_locked(spinlock_t *lock); 

使用步骤:

        申请锁:在访问共享资源前尝试获取自旋锁。

        临界区:获取锁后进入临界区执行操作。

        释放锁:操作完成后释放锁。

#include <linux/spinlock.h>  DEFINE_SPINLOCK(lock);  //lock在使用DEFINE_SPINLOCK时是直接定义并初始化的,无需别处定义
//spinlock_t lock = SPIN_LOCK_UNLOCKED;// SPIN_LOCK_UNLOCKED用在需要初始化的场合,返回未上锁的spinlock_tvoid increment_counter() {  spin_lock(&lock);  // 获取锁  counter++;         // 临界区操作  spin_unlock(&lock); // 释放锁  
}  // 在其他线程中调用 increment_counter() 来安全地增加 counter

        如果在驱动的open函数中上了自旋锁,则另一个进程没有拿到锁就会自旋等待。

第二十一章 自旋锁死锁

        死锁是多个进程(或线程)在执行过程中,因互相等待对方持有的资源而无法继续执行,从而导致无限期阻塞的现象。在自旋锁(spinlock)的上下文中,死锁可以发生在以下几种情况:

1. 进程阻塞导致的死锁

情况描述:

        进程A持有自旋锁,在内核态中被阻塞(如等待I/O操作或睡眠)。如果此时内核调度了进程B,而进程B也尝试获取同一个自旋锁,由于自旋锁的特性(忙等待),进程B会无限期地循环检查锁的状态,而进程A因阻塞无法释放锁,导致死锁。

解决办法:

        确保持有自旋锁的代码执行时间尽可能短。

        避免在持有自旋锁时调用可能导致进程阻塞的函数(如睡眠、等待I/O等)。

2. 中断导致的死锁

情况描述:

        进程A持有自旋锁时,CPU响应了一个中断,中断处理函数也尝试获取同一个自旋锁。如果中断被允许,则中断处理函数将自旋等待锁释放,但锁由于被进程A持有而无法释放(因为进程A在中断发生时被暂停),导致死锁。

解决办法:

        在获取自旋锁之前禁用中断(使用spin_lock_irq或spin_lock_irqsave)。

        在释放自旋锁后重新启用中断(使用spin_unlock_irq或spin_unlock_irqrestore)。

//禁止本地中断,并获取自旋锁
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) //用来存储函数调用前的中断状态//关闭下半部,获取自旋锁
void spin_lock_bh(spinlock_t *lock) 
//打开下半部,获取自旋锁
void spin_unlock_bh(spinlock_t *lock) 

第二十二章 信号量

        信号量是操作系统中用于同步和互斥的机制,它通过一个全局变量(信号量值)来控制对共享资源的访问。信号量可以是计数型(允许多个线程同时访问)或二值型(只允许一个线程访问)。信号量的值不能为负,当值为0时,访问资源的线程需等待。访问资源时信号量减一,访问完成后加一。

        相比于自旋锁,信号量具有休眠特性,适用于长时间占用资源的场合,但不能用于中断处理。在短时间访问资源的场景下,自旋锁效率更高。

        同时使用自旋锁和信号量时,应先获取信号量,再获取自旋锁,以避免在持有自旋锁时进入睡眠状态

        Linux内核中,信号量由 semaphore结构体表示,包含锁、计数器和等待队列。

/*信号量结构体*/
struct semaphore { atomic_t count;             // 信计数值  raw_spinlock_t lock;        // 自旋锁  struct list_head wait_list; // 等待该信号量的线程队列  
};  // 注意:raw_spinlock_t 是Linux内核中用于低级自旋锁的类型,  
// 它与通用的 spinlock_t 不同,后者在中断上下文中可能不安全。  

DEFINE_SEMAPHORE();    //定义并初始化信号量为1。
sema_init();                           //初始化信号量,设置其值为val。
down();                                  //阻塞式获取信号量,无法被中断。
down_interruptible();             //可中断地获取信号量,允许被信号中断。   
down_trylock();                     //获取信号量,拿不到则返回非0值,不阻塞。
up();                                      //释放信号量。

//定义并初始化信号量为1。
DEFINE_SEMAPHORE(name);  //初始化信号量,设置其值为val。
sema_init(struct semaphore *sem, int val); //阻塞式获取信号量,无法被中断。
down(struct semaphore *sem);   //可中断地获取信号量,允许被信号中断。            
down_interruptible(struct semaphore *sem); //尝试获取信号量,获取不到则返回非0值,不阻塞。
down_trylock(struct semaphore *sem);   //释放信号量。
up(struct semaphore *sem);                 
#include <linux/semaphore.h>  struct semaphore sem;  // 初始化信号量  
sema_init(&sem, 1); // 例如,设置为二值信号量  // 访问共享资源  
void access_resource(void) {  if (down_interruptible(&sem)) {  // 获取信号量失败,可能是被信号中断  return;  }  // 访问共享资源...  // 释放信号量  up(&sem);  
}

第二十三章 互斥锁

        互斥锁确保多线程访问共享资源时互斥,即一次只有一个线程能访问。锁有两种状态:锁定和非锁定。线程访问资源前需先尝试锁定锁,若锁已被占用则线程等待;锁释放后,等待的线程继续执行。

        在Linux内核中,互斥锁通过 mutex结构体表示,定义在include/linux/mutex.h中。它包含了一些关键的成员,如锁的所有者、等待锁的线程队列等。

/*互斥锁结构体*/
struct mutex {  atomic_long_t owner;       //锁持有者spinlock_t wait_lock;      //自旋锁struct list_head wait_list;//等待该信号量的线程对列// 其他调试和性能优化相关字段  
};

DEFINE_MUTEX();        //定义并初始化一个互斥锁变量
mutex_init();                    //初始化一个互斥锁
mutex_lock();                   //尝试获取锁,如果锁被占用则休眠
mutex_unlock();               //释放锁        
mutex_is_locked();          //判断锁是否被占用

//定义并初始化一个互斥锁变量
DEFINE_MUTEX(name);//初始化一个互斥锁
void mutex_init(struct mutex *lock);//尝试获取锁,如果锁被占用则休眠
void mutex_lock(struct mutex *lock);//释放锁
void mutex_unlock(struct mutex *lock)//判断锁是否被占用
int mutex_is_locked(struct mutex *lock) 
#include <linux/mutex.h>  DEFINE_MUTEX(my_mutex);  void thread_function(void) {  mutex_lock(&my_mutex);  // 访问或修改共享资源  // ...  mutex_unlock(&my_mutex);  
}  // 假设在多个线程或任务中调用 thread_function


http://www.ppmy.cn/news/1531197.html

相关文章

Mybatis映射文件详解-mapper.xml文件

在Mybatis中&#xff0c;Mapper XML文件是用于定义SQL语句和Java方法之间映射关系的核心配置文件。通过这些文件&#xff0c;开发者可以将数据库中的表与Java对象进行映射&#xff0c;实现数据的持久化操作。本文将详细介绍Mybatis映射文件的相关知识&#xff0c;包括其结构、标…

js 如何监听 body 内容是否改变

如果您想监听body内容的变化&#xff0c;并作出响应&#xff0c;可以使用MutationObserver。以下是一个简单的例子&#xff0c;它会在body内容变化时在控制台输出一条消息&#xff1a; // 创建一个观察者对象 const observer new MutationObserver(function(mutations, obser…

001、restful设计规范

https://www.kancloud.cn/kancloud/rest-api-design-safety/78113 https://www.kancloud.cn/kancloud/http-api-design/78123 https://www.kancloud.cn/kancloud/http-api-guide/56268 restful接口设计规范 按照restful接口设计规范 GET &#xff08;SELECT&#xff09;&…

flutter 设置字体大小,适应各种屏幕

起因&#xff0c; 目的: 来源就是客户需求。 从个人角度来说&#xff0c;我讨厌 flutter, 和 java 一样&#xff0c; 都是 臃肿&#xff0c;繁琐&#xff0c;死板. 1. 过程: 根据用户的屏幕尺寸&#xff0c;把子元素大小&#xff0c; 字体的大小&#xff0c;都设置为百分比&…

Mac pnpm安装

安装pnpm的时候一定要把npm更新到最新版 不然pnpm下载不成功。 &#xff08;更新npm&#xff09;&#xff1a;sudo npm install -g npm (安装pnpm:) sudo npm install -g pnpm 检验安装是否成功&#xff1a;pnpm --version 项目内安装依赖&#xff1a;pnpm install / 运行项目&…

用Python实现运筹学——Day 4: 线性规划的几何表示

一、学习内容 线性规划的几何表示&#xff1a; 线性规划问题的解通常位于一个凸多边形&#xff08;即可行解空间&#xff09;的顶点上&#xff0c;这意味着在求解线性规划问题时&#xff0c;只需要找到可行解空间中的顶点并计算出目标函数值&#xff0c;再选择其中的最优解。 可…

车道线拟合

聚类如何帮助解决斑马线误拟合问题 聚类算法通过将相似的像素或特征点归为一类&#xff0c;可以用来分析图像中的不同结构&#xff08;例如车道线和斑马线&#xff09;。以下是一些具体的聚类方法和它们在车道线检测中的应用&#xff1a; 1. 基于几何特征的聚类 斑马线和车道…

解决R包依赖版本不兼容问题

ERROR: dependency ‘Matrix’ is not available for package ‘irlba’ removing ‘/root/anaconda3/envs/myview/lib/R/library/irlba’ ERROR: dependency ‘Matrix’ is not available for package ‘N2R’ removing ‘/root/anaconda3/envs/myview/lib/R/library/N2R’ ER…