驱动篇:并发(一)

驱动篇:并发(一)

Linux 设备驱动中必须解决的一个问题是多个进程对共享资源 (硬件资源和软件上的全局变量、静态变量等) 的并发访问,并发访问会导致竞态。Linux 提供了多种解决竞态问题的方式,这些方式适合不同的应用场景。

场景:
1.对称多处理器(SMP)的多个 CPU
2.单 CPU 内进程与抢占它的进程
3.中断(硬中断、软中断、Tasklet、底半部)与进程之间

上述并发的发生情况除了 SMP 是真正的并行以外,其他的都是“宏观并行、微观串行”的,但其引发的实质问题和 SMP 相似。解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。访问共享资源的代码区域称为临界区(critical sections),临界区需要以某种互斥机制加以保护。中断屏蔽、原子操作、自旋锁和信号量等是 Linux 设备驱动中可采用的互斥途径

一.中断屏蔽
中断屏蔽将使得中断与进程之间的并发不再发生,而且,由于 Linux 内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也就得以避免了。

local_irq_disable() //屏蔽中断
...
critical section //临界区
...
local_irq_enable() //开中断

由于 Linux 系统的异步 I/O、进程调度等很多重要操作都依赖于中断,中断对于内核的运行非常重要,在屏蔽中断期间所有的中断都无法得到处理,因此长时间屏蔽中断是很危险的,有可能造成数据丢失甚至系统崩溃。这就要求在屏蔽了中断之后,当前的内核执行路径应当尽快地执行完临界区的代码。local_irq_disable()和 local_irq_enable()都只能禁止和使能本 CPU 内的中断,因此,并不能解决 SMP 多 CPU 引发的竞态。因此,单独使用中断屏蔽通常不是一种值得推荐的避免竞态的方法,它适宜与自旋锁联合使用。与 local_irq_disable()不同的是,local_irq_save(flags)除了进行禁止中断的操作以外,还保存目前 CPU 的中断位信息,local_irq_restore(flags)进行的是与 local_irq_save(flags)相反的操作。
如果只是想禁止中断的底半部,应使用 local_bh_disable(),使能被 local_bh_disable()禁止的底半部应该调用 local_bh_enable()。
2.原子操作
原子操作指的是在执行过程中不会被别的代码路径所中断的操作。Linux 内核提供了一系列函数来实现内核中的原子操作,这些函数又分为两类,分别针对位和整型变量进行原子操作。它们的共同点是在任何情况下操作都是原子的,内核代码可以安全地调用它们而不被打断。位和整型变量原子操作都依赖底层CPU 的原子操作来实现,因此所有这些函数都与 CPU 架构密切相关。
整型原子操作
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_dec(atomic_t *v);//原子变量减少 1

5.操作并测试

int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
上述操作对原子变量执行自增、自减和减操作后(注意没有加)测试其是否为 0,0 则返回 true,否则返回 false

6.操作并返回

int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
上述操作对原子变量进行加/减和自增/自减操作,并返回新的值。

位原子操作
1.设置位

void set_bit(nr, void *addr);
上述操作设置 addr 地址的第 nr 位,所谓设置位即将位写为 1

2.清除位

void clear_bit(nr, void *addr);
上述操作清除 addr 地址的第 nr 位,所谓清除位即将位写为 0

3.改变位

void change_bit(nr, void *addr);
上述操作对 addr 地址的第 nr 位进行反置。

4.测试位

test_bit(nr, void *addr);
上述操作返回 addr 地址的第 nr 位。

5.测试并操作位

int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);
上述 test_and_xxx_bit ( nr, void *addr) 操作等同于执行 test_bit (nr, void *addr) 后
再执行 xxx_bit(nr, void *addr)

使用原子变量使设备只能被一个进程打开

 static atomic_t xxx_available = ATOMIC_INIT(1); /*定义原子变量*/
 static int xxx_open(struct inode *inode, struct file *filp)
{
 ...
 if (!atomic_dec_and_test(&xxx_available))
 {
atomic_inc(&xxx_available);
return - EBUSY; /*已经打开*/
 }
 ...
 return 0; /* 成功 */
 }
static int xxx_release(struct inode *inode, struct file *filp)
 {
 atomic_inc(&xxx_available); /* 释放设备 */
 return 0;
 }

二.自旋锁
自旋锁(spin lock)是一种对临界资源进行互斥访问的典型手段,其名称来源于它的工作方式。为了获得一个自旋锁,在某 CPU 上运行的代码需先执行一个原子操作,该操作测试并设置(test-and-set)某个内存变量,由于它是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋”,通俗地说就是“在原地打转”。当自旋锁的持有者通过重置该变量释放这个自旋锁后,某个等待的“测试并设置”操作向其调用者报告锁已释放。理解自旋锁最简单的方法是把它作为一个变量看待,该变量把一个临界区或者标记为“我当前在运行,请稍等一会”或者标记为“我当前不在运行,可以被使用” 。如果 A 执行单元首先进入例程,它将持有自旋锁;当 B 执行单元试图进入同一个例程时,将获知自旋锁已被持有,需等到 A 执行单元释放后才能进入。
1.定义自旋锁

spinlock_t spin;

2.初始化自旋锁

spin_lock_init(lock)
该宏用于动态初始化自旋锁 lock

3.获得自旋锁

spin_lock(lock)
该宏用于获得自旋锁 lock,如果能够立即获得锁,它就马上返回,否则,它将自
旋在那里,直到该自旋锁的保持者释放;
spin_trylock(lock)
该宏尝试获得自旋锁 lock,如果能立即获得锁,它获得锁并返回真,否则立即返
回假,实际上不再“在原地打转”;

4.释放自旋锁

spin_unlock(lock)
该宏释放自旋锁 lock,它与 spin_trylock 或 spin_lock 配对使用。
自旋锁一般这样被使用,如下所示:
//定义一个自旋锁
spinlock_t lock;
spin_lock_init(&lock);
spin_lock (&lock) ; //获取自旋锁,保护临界区
...//临界区
spin_unlock (&lock) ; //解锁

自旋锁主要针对 SMP 或单 CPU 但内核可抢占的情况,对于单 CPU 和内核不支持抢占的系统,自旋锁退化为空操作。在单 CPU 和内核可抢占的系统中,自旋锁持有期间内核的抢占将被禁止。由于内核可抢占的单 CPU 系统的行为实际很类似于 SMP系统,因此,在这样的单 CPU 系统中使用自旋锁仍十分必要。尽管用了自旋锁可以保证临界区不受别的 CPU 和本 CPU 内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候还可能受到中断和底半部(BH)的影响。为了防止这种影响,就需要用到自旋锁的衍生。spin_lock()/spin_unlock()是自旋锁机制的基础,它们和关中断 local_irq_ disable()/开中断 local_irq_enable()、关底半部local_bh_disable()/开底半部 local_bh_enable()、关中断并保存状态字 local_irq_save()/开中断并恢复状态 local_irq_restore()结合就形成了整套自旋锁机制,关系如下所示:

spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_unlock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()

只有在占用锁的时间极短的情况下, 使用自旋锁才是合理的。当临界区很大或有共享设备的时候,需要较长时间占用锁,使用自旋锁会降低系统的性能自旋锁可能导致系统死锁。引发这个问题最常见的情况是递归使用一个自旋锁,即如果一个已经拥有某个自旋锁的 CPU 想第二次获得这个自旋锁,则该 CPU 将死锁。此外,如果进程获得自旋锁之后再阻塞,也有可能导致死锁的发生。copy_from_user()、copy_to_user()和 kmalloc()等函数都有可能引起阻塞,因此在自旋锁的占用期间不能调用这些函数。

使用自旋锁使设备只能被一个进程打开

int xxx_count = 0;/*定义文件打开次数计数*/
static int xxx_open(struct inode *inode, struct file *filp)
{
...
spinlock(&xxx_lock);
if (xxx_count)/*已经打开*/
{
spin_unlock(&xxx_lock);
return - EBUSY;
}
xxx_count++;/*增加使用计数*/
spin_unlock(&xxx_lock);
...
return 0; /* 成功 */
}
static int xxx_release(struct inode *inode, struct file *filp)
{
...
spinlock(&xxx_lock);
xxx_count--; /*减少使用计数*/
spin_unlock(&xxx_lock);
return 0;
}

读写自旋锁是一种比自旋锁粒度更小的锁机制,它保留了“自旋”的概念,但是在写操作方面, 只能最多有一个写进程, 在读操作方面,同时可以有多个读执行单元。当然,读和写也不能同时进行。
锁的用途可以明确到分为读取和写入两个场景。比如,对于一个链表可能就要更新又要检索,当更新链表时,不能有其他代码并发的写链表或从链表中读取数据,写操作要求完全互斥。另一方面,当对其检索(读取)链表时,只要其他程序不对链表进行写操作就行了。只有没有写操作,多个并发的读操作都是安全的。可以通过读写锁搞定
这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。
1.定义和初始化读写自旋锁

rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* 静态初始化 */
rwlock_t my_rwlock;
rwlock_init(&my_rwlock); /* 动态初始化 */

2.读锁定

void read_lock(rwlock_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);

3.读解锁

void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);

在对共享资源进行读取之前,应该先调用读锁定函数,完成之后应调用读解锁函数。read_lock_irqsave()、read_lock_irq()和 read_lock_bh() 分别是 read_lock()分别与local_irq_save() 、 local_irq_disable() 和 local_bh_disable() 的 组 合 , 读 解 锁 函 数read_unlock_irqrestore()、read_unlock_ irq()、read_unlock_bh()的情况与此类似。
4.写锁定

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);
int write_trylock(rwlock_t *lock);

5.写解锁

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);

write_lock_irqsave() 、 write_lock_irq() 、 write_lock_bh() 分 别 是 write_lock() 与local_irq_save() 、 local_irq_disable() 和 local_bh_disable() 的 组 合 , 写 解 锁 函 数write_unlock_irqrestore()、write_unlock_irq()、write_unlock_bh()的情况与此类似。
读写自旋锁一般这样被使用,如下所示:

rwlock_t lock; //定义 rwlock
rwlock_init(&lock); //初始化 rwlock
//读时获取锁
read_lock(&lock);
... //临界资源
read_unlock(&lock);
//写时获取锁
write_lock_irqsave(&lock, flags);
... //临界资源
write_unlock_irqrestore(&lock, flags);

顺序锁(seqlock)是对读写锁的一种优化,若使用顺序锁,读执行单元绝不会被写执行单元阻塞,也就是说,读执行单元可以在写执行单元对被顺序锁保护的共享资源进行写操作时仍然可以继续读,而不必等待写执行单元完成写操作,写执行单元也不需要等待所有读执行单元完成读操作才去进行写操作。但是,写执行单元与写执行单元之间仍然是互斥的,即如果有写执行单元在进行写操作,其他写执行单元必须自旋在那里,直到写执行单元释放了顺序锁。如果读执行单元在读操作期间,写执行单元已经发生了写操作,那么,读执行单元必须重新读取数据,以便确保得到的数据是完整的。这种锁在读写同时进行的概率比较小时,性能是非常好的,而且它允许读写同时进行,因而更大地提高了并发性。顺序锁有一个限制,它必须要求被保护的共享资源不含有指针,因为写执行单元可能使得指针失效,但读执行单元如果正要访问该指针,将导致 Oops。
1.获得顺序锁

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() = loal_irq_save() + write_seqlock()
write_seqlock_irq() = local_irq_disable() + write_seqlock()
write_seqlock_bh() = local_bh_disable() + write_seqlock()

2.释放顺序锁

void write_sequnlock(seqlock_t *sl);
write_sequnlock_irqrestore(lock, flags)
write_sequnlock_irq(lock)
write_sequnlock_bh(lock)
其中:
write_sequnlock_irqrestore() = write_sequnlock() + local_irq_restore()
write_sequnlock_irq() = write_sequnlock() + local_irq_enable()
write_sequnlock_bh() = write_sequnlock() + local_bh_enable()
写执行单元使用顺序锁的模式如下:
write_seqlock(&seqlock_a);
...//写操作代码块
write_sequnlock(&seqlock_a);
因此,对写执行单元而言,它的使用与 spinlock 相同。
读执行单元涉及如下顺序锁操作。

1.读开始

unsigned read_seqbegin(const seqlock_t *sl);
read_seqbegin_irqsave(lock, flags)
读执行单元在对被顺序锁 s1 保护的共享资源进行访问前需要调用该函数, 该函数
仅返回顺序锁 s1 的当前顺序号。其中:
read_seqbegin_irqsave() = local_irq_save() + read_seqbegin()

2.重读

int read_seqretry(const seqlock_t *sl, unsigned iv);
read_seqretry_irqrestore(lock, iv, flags)
读执行单元在访问完被顺序锁 s1 保护的共享资源后需要调用该函数来检查, 在读
访问期间是否有写操作。如果有写操作,读执行单元就需要重新进行读操作。其中:
read_seqretry_irqrestore() = read_seqretry() + local_irq_restore()
读执行单元使用顺序锁的模式如下:
do {
seqnum = read_seqbegin(&seqlock_a);
//读操作代码块
...
} while (read_seqretry(&seqlock_a, seqnum));
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值