2021-04-11

7.Linux设备驱动的并发控制
并发(concurrency)------指的是多个执行单元同时、并行被执行
竞态(race conditions)-------并发的多个执行单元对共享资源的不合顺序地访问,两个以上线程在同一个临界区内同时执行
共享资源-----硬件资源和软件上的全局变量、静态变量等
解决竞态问题-------保证共享资源互斥访问
互斥访问------指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问
实现互斥访问------用中断屏蔽、原子操作、自旋锁和信号量等机制对临界区进行保护
临界区-----访问和操作共享资源的代码区域
7.1.并发与竞态
并发指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源的访问很容易导致竟态。
7.1.1.对称多处理器(SMP)的多个CPU
SMP是一种紧耦合、共享存储的系统模型,其体系结构如图7.1所示。

图7.1 SMP体系结构
在SMP的情况下,两个核(CPU0和CPU1)的竟态可能发生在CPU0和CPU1的进程之间、CPU0的进程与CPU1的中断之间以及CPU0的中断与CPU1的中断之间。
7.1.2.单CPU内进程与抢占它的线程
Linux 2.6后的内核支持内核抢占调度、一个进程在内核执行的时候可能消耗完自己的时间片。也可能被另外一个高优先级的进程打断,进程与抢占它的进程访问共享资源的情况类似于SMP的多个CPU。
7.1.3.中断(硬中断、软中断、Tasklet、低半步)与进程之间
中断可以打断正在执行的进程,如果中断服务程序访问进程正在访问的资源,则竟态也会发生。此外,中断也有可能被新的更高优先级的中断打断,因此,多个中断之间本身也有可能引起并发而导致竟态。
解决竟态问题的途径是保证对共享资源的互斥访问,所谓的互斥访问是指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。
访问共享资源的代码区域称为临界区(Critical Sections),临界区需要被某种互斥机制加以保护。中断屏蔽、原子操作、自旋锁、信号量、互斥体等是linux设备驱动中可采用的互斥途径。

7.2.编译执行乱序
7.3.中断屏蔽
CPU一般都具备有屏蔽中断和打断中断的功能,这项功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,防止某些竟态条件的发生。具体而言,中断屏蔽将使得中断与进程之间的并发不在发生,而且,由于Linux内核的进程调度等操作都依赖中断来实现,内核抢占之间的并发就可以避免了。
中断屏蔽的使用方法为:
local_irq_disabled() /* 屏蔽中断 /

critical section /
临界区 */

local_irq_enable() /*开中断 */
其底层的实现原理是让CPU本身不响应中断,比如,对ARM处理器而言,其底层的实现是屏蔽ARM CPSR的I位
但是不能长期的将中断屏蔽掉,因为Linux的异步I/O、进程调度都依赖于中断,所以在屏蔽完中断过后,当前的内核执行路径要尽快的跑完临界区的代码。
但local_irq_disabled()和local_irq_enable()都只能禁止和使能中断,因此并不能解决SMP多CPU引发的竟态问题,所以它经常与自旋锁联合使用。
推荐使用disable_irq_save(flags)、enable_irq_restore(flags)代替local_irq_disabled()、local_irq_enable() ,disable_irq_save(flags)除了进行禁止中断的操作外,还能保存目前CPU中断位的信息,enable_irq_restore(flags),恢复中断位的信息。对应ARM处理器而言,其实就是保存和恢复CPSR。
7.4.原子操作
Linux 内核提供了一系列函数来实现内核中原子操作,位和整型变量的原子操作都依赖于CPU的原子操作,因此所有的这些函数都与CPU架构有关。对于ARM处理器而言,底层使用LDREX和STREX指令。
7.4.1.整原子操作
1.设置原子变量的值
void atomic_set(atomic_t *v, int i); /*设置原子变量的值为i
atomic_t v = ATOMIC_INIT(0); /*定义原子变量v并初始化为0

2.获取原子变量的值
atomic_read(atomic_t *v); //返回原子变量的值

3.原子变量加/减
void atomic_add(int i, atomic_t *v); /原子变量加i/
void atomic_sub(int i, atomic_t *v); /原子变量减i/

4.原子变量自增自减
void atomic_inc(atomic_t v); / 原子变量增加1*/
void atomic_sub(atomic_t v); / 原子变量减少1*/

5.测试并操作位
int atomic_inc_and_test(atomic_t *v);
int atomic_dev_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
/*对原子变量执行自增、自减和减操作后(注意没有加),测试其是否为0,为0返回 ture,否则返回false。/

7.4.2.位原子操作
void set_bit(nr, void *addr); /设置addr地址的第nr位, 为1/
void clear_bit(nr, void *addr); /清除addr地址的第nr位,位0/
void change_bit(nr, void *addr); /对addr 地址的第nr位进行反置/
/测试并操作位/
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);
并发中不被打断的最小原子(作用最多只能被一个进程打开)
#include <asm/atomic.h>
static atomic_t demo_atomic = ATMIC_INIT(1); /*定义原子变量demo_atomic, 并初始化为1/
static int demo_open(struct inode *inode, struct file *file)
{
/原子变量减1,并测试是否为0,为0返回真/
If(!atomic_dec_and_test(&demo_atomic)) {
atomic_inc(&demo_atomic); /原子变量加一,恢复自减前状态/
return - EBUSY; /已将被打开/
}
/*当第一次被open时,demo_atomic为1, !atomic_dec_and_test不 成立,正常打开/
printk(“demo open \n”);
return 0;
}
static int demo_release(struct inode *inode, struct file *file)
{
atomic_inc(&demo_atomic); //释放设备
printk(“demo release\n”);
return 0;
}

7.5.自旋锁
7.5.1.自旋锁的使用
自旋锁(Spin Lock)是一种典型的对临界资源区进行互斥访问的手段,其名称来源它的工作方式。理解自旋锁最简单的的方法是将它作为一个变量看待,该变量把一个临界区标记为“我当前在运行,请稍等一会”,或者我当前不在运行,可以被使用了”,如果A执行单元首先进入例程,它将持有自旋锁;当B执行单元试图进入同一个例程时,将获知自旋锁已被持有,需要等到A执行单元释放后才能进入。
相关自旋锁的操作:
spinlock_t spin; /定义/
spin_lock_init(lock) /初始化/
spin_lock(lock) /获得自旋锁,如果得到即返回该自旋锁,否则,原地等待/
spin_trylock(lock) /获得自旋锁,如果得到即返回真,否则返回假/
spin_unlock(lock) /释放/
自旋锁主要针对SMP或者单CPU但内核可抢占的情况,对于单CPU和内核不支持抢占的系统,自旋锁退化为空操作。
尽管用了自旋锁可以保证临界区不受别的CPU和本CPU内的抢占线程打扰,但还可能受到中断和底半部(BH)的影响,为了防止这种操作,就需要自旋锁的衍生。在spi_lock()/spi_unlock()是自旋锁的基础上加关中断和的操作,关中断local_irq_disable()/开中断local_irq_enable()、关底半部中断local_bh_disable()开底半部local_bh_enable、关中断并保存状态字local_irq_save()开中断并恢复状态字local_irq_restore()结合就形成了整套自旋锁机制,关系如下:
spin_lock_irq() = spi_lock() + local_irq_disable()
spin_unlock_irq() = spi_unlock() + local_irq_enable()
spin_lock_irqsave() = spi_lock() + local_irq_save()
Spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
Spi_lock_bh() = spi_lock() + lock_bh_disabled()
Spi_unlock_bh() = spi_unlock() + local_bh_enable()

Spin_lock_irq()、spi_lock_irqsave()、spin_lock_bh()类似函数会为自旋锁的使用系好安全带,以避免突如其来的中断驾入对系统的伤害。
7.5.2.使用自旋锁的注意事项
自旋锁实际上是忙等锁,当锁不能用时,CPU一直循环执行“测试并设置” 该锁直到可用而取得该锁,CPU在等待自旋锁时不做任何有用的工作,仅仅是等待。因此,只有在占用锁的时间极短的情况下,使用自旋锁才是合理的。当临界区很大或有共享设备的时候,需要较长时间占用锁,使用自旋锁会降低系统的性能。
自旋锁会导致系统死锁。引发这个问题最常见的情况是递归使用一个自旋锁,即如果一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁,则该CPU将死锁。
在自旋锁锁定期间不能调用可能引起进程调度的函数。如果进程获得进程自旋锁之后再堵塞,如调用copy_from_user()、copy_to_user()、kmalloc()、和msleep()等函数,可能导致内核的崩溃。
7.5.3.读写自旋锁
读写自旋锁再写操作方面,只能最多有一个进程,在读操作方面,同时可以有多个读执行单元(允许读的并发)。当然,读和写也不能同时进行。
读写自旋锁涉及的操作如下:
/定义和初始化读写自旋锁/
rwlock_t my_rwlock; /定义/
rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /静态初始化/
rwlock_init(&my_rwlock); /动态初始化/

/读锁定/
void read_lock(rwkock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);

/读解锁/
void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned log flags);
void read_lock_bh(rwlock_t *lock);

/写锁定/
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);

/写解锁/
void write_unlock(rwlock_t *lock)
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);

rwlock_t lock; /定义rwlock/
rwlock_init(&lock); /初始化rwlock/

/读时获取锁/
read_unlock(&lock)
…/临界资源/
read_unlock(&lock);

/写时获得设备锁/
write_lock_irqsave(&lock, flags);
…/临界锁/
write_unlock_irqrestore(&lock, flags);

在对共享资源进行读写之前,应该先调用写锁定函数,完成之后应调用写解锁函数。和spi_trylock()一样,write_trylock()也只是尝试获取读写自旋锁,不管成败都会立即返回。
读写自旋锁一般都这样使用:
rwlock_t lock; /*定义rwlock */
rwlock_init(&lock); /*初始化rwlock */

/*读时获取锁
read_lock(&lock);

read_unlock(&lock);

/写时获取锁/
write_lock_irqsave(&lock, flags);

write_unlock_irqrestore(&lock, flags);

7.5.4.顺序锁
顺序锁(seqlock)是对读写锁的一种优化,若使用顺序锁,读写单元不会被写执行单元堵塞,也就是说,读执行单元在写执行单元对顺序锁对被顺序锁保护的共享资源进行写操作仍然可以继续读,写执行单元也不需要等待所有读执行单元完成才去进进行写操作。但写操作与写操作仍然是互斥的,即如果有写单元在进行写操作,其他写操作必须自旋在那里,直到写执行单元释放了互斥锁。
在Linux内核中,写执行单元涉及到的顺序锁操作如下
/获取顺序锁/
void write_seqlock(seqlock_t *sl);
int write_tryseqlock(seqlock_t *sl);
write_seqlock_irqsave(lock, flags);
write_seqlock_irq(lock);
write_seqlock_bh(lock);

write_seqlock_irqsave() = local_irq_save()+ write_seqlock();
write_seqlock_irq() = local_irq_save() + write_seqlock();
Write_seqlock_bh() = local_bh_disable() + write_seqlock();

/释放写顺序锁/
void write_sequnlock(seqlock_t *s1);
write_sequnlock_irqrestore(lock, flags);
write_sequnlock_irq(lock);
write_sequnlock_bh(lock);

/使用写顺序锁/
write_seqlock(&seqlock_a);
…/写操作代码/
write_sequnlock(&seqlock_a);

/读顺序锁开始/
unsigned read_seqbegin(const seqlock_t *s1);
read_seqbegin_irqsave(lock, flags)

/重读/
int read_seqretry(const seqlock_t *s1, unsigned iv);
read_seqretry_irqrestore(lock, iv, flags)
read_seqretry_irqrestore() = read_seqretry() + local_irq_restore()

7.6.信号量(semaphore)
用于保护临界区的一种常用方法,它的使用方式和自旋锁类似。与自旋锁相同,只有得到信号量的进程才能执行临界区代码。但是,与自旋锁不同的是,当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。
工作方式:如果一个任务试图获得一个不可用(被占用)的信号量时,信号量会将其推进一个等待队列,令其睡眠,这时,CPU重获自由转去执行其他代码。当信号量可用(被释放),处于等待队列的那个任务被唤醒,并获得该信号量。
·拿不到就切换进程,有调度开销,CPU利用率高
·锁定期间可用睡觉,不用于中断上下文

信号量相关的操作
/定义信号量/
struct semaphore sem;
/初始化信号量/
void sema_init (struct semaphore *sem, int val); /初始值为sem = val/
void int_MUTEX (struct semaphore *sem); /*互斥信号量 sem = 1;
void init_MUTEX_LOCKED (struct semaphore *sem) /*同步信号量 sem = 0
DECLARE_MUTEX(mount_sem); /定义信号量/
DEXLARE_MUTEX_LOCKED(name) /定义初始值为0,用于同步/

/获得信号量/
void down(struct semaphore *sem); /导致睡眠,不能被信号打断,不能在中断上下文使用/
int down_interruptible(struct semaphore * sem); /*导致睡眠,能被信号打断,信号导致函 数返回非零/
int down_trylock(struct semaphore *sem) /*获得信号量返回0,否则返回非0,不会导致调用 *者睡眠,可在中断上下文使用 */
/释放信号量/
void up(struct semaphore *sem); /释放信号量sem,唤醒等待者/

/使用信号量/
DECLARE_MUTEX(mount_sem); /定义信号量/
down(&mount_sem); /获取信号量,保护临界区/

critical section /临界区/

up(&mount_sem); /释放信号量/

进程当中用于并发互斥和资源的计数。
相当于自旋锁、信号量会睡眠,仅能用于进程中
#include <linux/semaphore.h>
static DEFINE_SEMAPHORE(demo_semlock); //定义一个初始值为一的信号量

static int demo_open(struct inode *inode, struct file file)
{
If (down(&demo_semlock)) /
获得打开锁 /
return -EBUSY; /
设备忙

printk(“demo open \n”);
return 0;
}

static int demo_release(struct inode *inode, struct file file)
{
up(&demo_semlock); /
释放打开锁 */
printk(“demo release\n”);
return 0;
}

7.7.互斥体
互斥体是专门用来做互斥,和二元信号量相似
static struct mutex lock; /*定义mutex */
mutex_init(& lock); /初始化mutex/

mutex_lock(& lock); /*获取mutex /
…/
临界资源 */
mutex_unlock(& lock); /*释放mutex */

互斥体和自旋锁选用的3项原则
(1)当锁不能被获取时,使用互斥体的开销是进程上下文切换时间,使用自旋锁的开销是等待获取自旋锁(由临界区执行时间决定),若临界区比较小,宜使用自旋锁,若临界区很大,应使用互斥体。
(2)互斥体所保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护这样代码的临界区。因为堵塞意味着进行进程的切换,如果线程被切换,如果进程被切换出去后,另外一个进程企图获取本自旋锁,死锁就会发生。
(3)互斥体存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断下使用,则在互斥体和自旋锁之间只能选择自旋锁了。

7.8.完成量
是一种比信号量更好的同步机制,它用于一个执行单元等待另一个执行单元执行完某事
struct completion my_completion; /定义完成量/
init_completion(&my_completion); /初始化完成量/
DECLARE_COMPLETION(my_completion); /定义并初始化完成量/
void wait_for_completion(struct completion *c); /唤醒一个等待的执行单元/
void complete(struct completion *c); /唤醒等待这个完成量的所有执行单元/
void complete_all(struct completion *c) /唤醒等待这个完成量的所有执行单元/

7.9.总结
自旋锁和互斥体应用最为广泛,自旋锁会导致死循环,因此在锁期间要求的临界区小,互斥体允许临界区堵塞。可以试用于临界区大的情况

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值