第十八章 并发与竞争
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_t 和 atomic64_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