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

第十八章 并发与竞争

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_t
  
void 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大象荒野

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值