网站图标代码,wordpress自动水印代码,wordpress 自定义链接地址,北京企业网站建设公司哪家好并发与竞争
并发与竞争的产生 Linux是一个多任务操作系统#xff0c;肯定会存在多个任务共同操作同一段内存或者设备的情况#xff0c;多个任务甚至中断都能访问的资源叫做共享资源#xff0c;就和共享单车一样。在驱动开发中要注意对共享资源的保护#xff0c;也就是要处…并发与竞争
并发与竞争的产生 Linux是一个多任务操作系统肯定会存在多个任务共同操作同一段内存或者设备的情况多个任务甚至中断都能访问的资源叫做共享资源就和共享单车一样。在驱动开发中要注意对共享资源的保护也就是要处理对共享资源的并发访问。 并发就是多个“用户”同时访问同一个共享资源。
Linux 系统是个多任务操作系统会存在多个任务同时访问同一片内存区域这些任务可能会相互覆盖这段内存中的数据造成内存数据混乱。针对这个问题必须要做处理严重的话可能会导致系统崩溃。现在的 Linux 系统并发产生的原因很复杂总结一下有下面几个主要原因
①、多线程并发访问Linux 是多任务(线程)的系统所以多线程访问是最基本的原因。 ②、抢占式并发访问从 2.6 版本内核开始Linux 内核支持抢占也就是说调度程序可以在任意时刻抢占正在运行的线程从而运行其他的线程。 ③、中断程序并发访问硬件中断的权利是很大的。 ④、SMP(多核)核间并发访问现在 ARM 架构的多核 SOC 很常见多核 CPU 存在核间并发访问。
并发访问带来的问题就是竞争临界区就是共享数据段对于临界区必须保证一次只有一个线程访问也就是要保证临界区是原子访问的原子访问就表示这一个访问是一个步骤不能再进行拆分。如果多个线程同时操作临界区就表示存在竞争编写驱动的时候要注意避免并发和防止竞争访问。
原子操作
原子操作简介
原子操作就是指不能再进一步分割的操作一般原子操作用于变量或者位操作。
例如C语言的a 3 对应的汇编是
ldr r0, 0X30000000 /* 变量 a 地址
*ldr r1, 3
str r1, [r0] /* 将 3 写入到 a 变量中 */一个最简单的设置变量值的并发与竞争的例子
原子整形操作 API 函数
Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作在使用中用原子变量来代替整形变量此结构体定义在 include/linux/types.h 文件中如果要使用原子操作 API 函数首先要先定义一个 atomic_t 的变量。
typedef struct {int counter;
} atomic_t;例如一个定时器计数的变量可以定义为atomic_t count
定义原子变量的时候给原子变量赋初值atomic_t b ATOMIC_INIT(0); 不能直接(atomi_t b0) 如果使用 64 位的 SOC 的话就要用到 64 位的原子变量Linux 内核也定义了 64 位原子结构体。
typedef struct {long counter;
} atomic64_t;
#endif相应的也提供了 64 位原子变量的操作 API 函数这里我们就不详细讲解了和atomic_t的 API 函数有用法一样只是将“atomic_”前缀换为“atomic64_”将 int 换为 long long。
原子位操作 API 函数
函数描述set_bit(int nr, void *p)将p地址的第nr位置1。clear_bit(int nr, void *p)将p地址的第nr位清零。change_bit(int nr, void *p)将p地址的第nr位进行翻转。test_bit(int nr, void *p)获取p地址的第nr位的值。test_and_set_bit(int nr, void *p)将p地址的第nr位置1并且返回nr位原来的值。test_and_clear_bit(int nr, void *p)将p地址的第nr位清零并且返回nr位原来的值。test_and_change_bit(int nr, void *p)将p地址的第nr位翻转并且返回nr位原来的值。
自旋锁 原子操作只能对整形变量或者位进行保护在实际的使用环境中不可能只有整形变量这么简单的临界区。例如结构体变量就不是整型变量对于结构体中成员变量的操作也要保证原子性在线程 A 对结构体变量使用期间应该禁止其他的线程来访问此结构体变量这些工作原子操作都不能实现。
自旋锁简介
自旋锁Spinlock是 Linux 内核中用于同步不同 CPU 之间对共享资源访问的一种锁机制。当一个CPU 获得自旋锁时它会阻止其他 CPU 获取同一把锁直到锁被释放。自旋锁的主要特点是等待锁的 CPU 不会进入睡眠状态而是在循环中不断检查锁是否可用因此称为“自旋”。
不可睡眠持有自旋锁的 CPU 不能进入睡眠状态这意味着自旋锁适用于不会被长时间持有的场景否则可能会导致 CPU 资源的浪费。无阻塞请求自旋锁的进程不会释放 CPU而是忙等待busy-wait直到获得锁。适用于中断上下文自旋锁可以在中断处理程序中使用因为中断处理程序不能睡眠。避免死锁由于自旋锁不会导致进程睡眠因此不会导致死锁。性能开销在单核处理器上自旋锁实际上是无操作因为不存在多个 CPU 竞争的问题。但在多核处理器上如果锁被长时间持有会导致其他 CPU 忙等从而浪费 CPU 资源。
Linux 内核使用结构体 spinlock_t 表示自旋锁结构体定义如下所示
#include linux/spinlock.h
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;定义自旋锁spinlock_t lock;
自旋锁 API 函数
函数描述DEFINE_SPINLOCK(spinlock_t lock)定义并初始化一个自旋锁变量。spin_lock_init(spinlock_t *lock)初始化自旋锁。spin_lock(spinlock_t *lock)获取指定的自旋锁也叫做加锁。spin_unlock(spinlock_t *lock)释放指定的自旋锁。spin_trylock(spinlock_t *lock)尝试获取指定的自旋锁如果没有获取到就返回0。spin_is_locked(spinlock_t *lock)检查指定的自旋锁是否被获取如果没有被获取就返回非0否则返回0。
自旋锁API 函数适用于 SMP多核或支持抢占的单 CPU下线程之间的并发访问也就是用于线程与线程之间被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数否则的话会可能会导致死锁现象的发生。自旋锁会自动禁止抢占也就说当线程 A得到锁以后会暂时禁止内核抢占。如果线程 A 在持有锁期间进入了休眠状态那么线程 A 会自动放弃 CPU 使用权。线程 B 开始运行线程 B 也想要获取锁但是此时锁被 A 线程持有而且内核抢占还被禁止了线程 B 无法被调度出去那么线程 A 就无法运行锁也就无法释放死锁发生了
中断需要访问共享资源
中断里面可以使用自旋锁但是在中断里面使用自旋锁的时候在获取锁之前一定要先禁止本地中断(也就是本CPU中断对于多核 SOC来说会有多个 CPU 核)否则可能导致锁死现象的发生。 发生死锁的情况线程 A 先运行并且获取到了 lock 这个锁当线程 A 运行 functionA 函数的时候中断发生了中断抢走了 CPU 使用权。右边的中断服务函数也要获取 lock 这个锁但是这个锁被线程 A 占有着中断就会一直自旋等待锁有效。但是在中断服务函数执行完之前线程 A 是不可能执行的。
最好的解决方法就是获取锁之前关闭本地中断Linux 内核提供了相应的 API 函数
函数include\linux\spinlock.h描述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_irq/spin_unlock_irq 的时候需要用户能够确定加锁之前的中断状态但实际上很难确定中断状态因此不推荐使用spin_lock_irq/spin_unlock_irq。建议使用 spin_lock_irqsave/ spin_unlock_irqrestore因为这一组函数会保存中断状态在释放锁的时候会恢复中断状态。一般在线程中使用 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)/* 释放锁*/
}下半部(BH)也会竞争共享资源有些资料也会将下半部叫做底半部。如果要在下半部里面使用自旋锁可以使用下面的API 函数
函数include\linux\spinlock.h描述void spin_lock_bh(spinlock_t *lock)关闭下半部并获取自旋锁。void spin_unlock_bh(spinlock_t *lock)打开下半部并释放自旋锁。
其它类型的自旋锁
读写自旋锁
现在有个学生信息表此表存放着学生的年龄、家庭住址、班级等信息此表可以随时被修改和读取。此表肯定是数据那么必须要对其进行保护如果我们现在使用自旋锁对其进行保护。每次只能一个读操作或者写操作但是实际上此表是可以并发读取的。只需要保证在修改此表的时候没人读取或者在其他人读取此表的时候没有人修改此表就行了。也就是此表的读和写不能同时进行但是可以多人并发的读取此表。像这样当某个数据结构符合读/写或生产者/消费者模型的时候就可以使用读写自旋锁。读写自旋锁为读和写操作提供了不同的锁一次只能允许一个写操作也就是只能一个线程持有写锁而且不能进行读操作。但是当没有写操作的时候允许一个或多个线程持有读锁可以进行并发的读操作。
Linux 内核使用 rwlock_t 结构体表示读写锁。头文件包含linux/rwlock.h
函数描述DEFINERWLOCK(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 read_lock_bh(rwlock_t *lock)关闭下半部并获取读锁。void read_unlock_bh(rwlock_t *lock)打开下半部并释放读锁。写锁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)将中断状态恢复到以前的状态并且激活本地中断释放读锁。void write_lock_bh(rwlock_t *lock)关闭下半部并获取读锁void write_unlock_bh(rwlock_t *lock)打开下半部并释放读锁。
顺序锁
自旋锁使用注意事项
①、因为在等待自旋锁的时候处于“自旋”状态因此锁的持有时间不能太长一定要短否则的话会降低系统性能。如果临界区比较大运行时间比较长的话要选择其他的并发处理方式比如稍后要讲的信号量和互斥体。 ②、自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数否则的话可能导致死锁。 ③、不能递归申请自旋锁因为一旦通过递归的方式申请一个你正在持有的锁那么你就必须“自旋”等待锁被释放然而你正处于“自旋”状态根本没法释放锁。结果就是自己把自己锁死了 ④、在编写驱动程序的时候我们必须考虑到驱动的可移植性因此不管你用的是单核的还是多核的 SOC都将其当做多核 SOC 来编写驱动程序。
信号量
信号量Semaphore是操作系统中用于控制多个进程或线程对共享资源访问的一种同步机制。它是一个计数器用于多进程或线程之间的同步以及提供对共享资源的访问控制。信号量主要解决的问题是资源共享和互斥。
信号量和自旋锁的区别
• 阻塞与非阻塞 自旋锁当一个线程尝试获取一个已经被其他线程持有的自旋锁时它会进入忙等busy-wait状态即不断检查锁是否可用而不进入睡眠状态。这意味着CPU资源会被占用因为线程在不断地检查锁的状态。 信号量当一个线程尝试获取一个不可用的信号量时它会被阻塞即操作系统将其挂起不会占用CPU资源直到信号量变为可用状态。 • 使用场景 自旋锁适用于持有锁的时间非常短的情况因为忙等避免了线程上下文切换的开销但如果锁被持有的时间较长会导致CPU资源浪费。 信号量适用于持有锁的时间可能较长的情况或者需要同步的资源数量较多时因为它们允许线程在等待时释放CPU资源。 • 上下文切换 自旋锁不涉及上下文切换因为线程在等待锁时仍然保持运行状态。 信号量涉及上下文切换因为线程在等待信号量时会被挂起直到信号量可用时再被唤醒。 • 优先级问题 自旋锁可能导致优先级反转问题因为低优先级的线程持有锁时高优先级的线程可能无法获取锁而陷入忙等。 信号量不会导致优先级反转问题因为线程在等待信号量时会被阻塞不会占用CPU资源。 • 可中断性 自旋锁通常不可中断线程在忙等期间不能响应中断。 信号量可以是可中断的线程在等待信号量时可以响应中断并处理。总的来说自旋锁适用于需
信号量的开销要比自旋锁大因为信号量使线程进入休眠状态以后会切换线程切换线程会有开销。
如果共享资源的持有时间比较短那就不适合使用信号量了因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。 计数信号量当信号量的值可以大于1时它被称为计数信号量。这种类型的信号量可以用来控制多个相同的源。
二值信号量当信号量的值只能是0或1时它被称为二进制信号量或互斥锁。用于确保只有一个线程或进程可以访问特定的资源。