码迷,mamicode.com
首页 > 其他好文 > 详细

并发与竞态

时间:2015-03-16 06:30:42      阅读:180      评论:0      收藏:0      [点我收藏+]

标签:linux驱动

linux驱动——并发和竟态

序——正在运行的多个用户空间程序可能以一个令人惊讶的组合方式访问我们的代码。SMP系统甚至可能在不同的处理器上同时执行我们的代码。内核代码是可抢占的,因此我们的驱动程序代码可在任何时候丢失对处理器的独占。


信号量(semaphore)的实现:

有一对函数,通常成为P和V,加锁用P,解锁用V。在任何时刻只能有单个执行上下文拥有时,这种模式一个信号量也称谓“互斥体(mutex)”。Linux内核中几乎所有信号量均用于互斥。

内核代码必须包括<asm/semaphore.h>

相关类型是struct semaphore

声明和初始化:

直接创建和使用下列函数初始化
void sema_init(struct semaphore *sem, int val);

val为信号量初始值


使用宏声明一个互斥体

DECLARE_MUTEX(name);

DECLARE_MUTEX_LOCKED(name);

第一个初始化为1,第二个初始化为0。在允许线程访问前必须显式解锁。


如果互斥体必须在运行时被初始化(例如在动态分配互斥体的情况下),应使用下列函数之一:

void init_MUTEX(struct semaphore *sem);

void init_MUTEX_LOCKED(struct semaphore *sem);

在Linux世界,P函数被称为down,或这个名字的变种。它指的是减小了信号量的值,也许会将调用者置于休眠状态。然后等待信号量变得可用,之后授予调用者对被保护资源的访问。以下是down的三个版本:
void down(struct semaphore *sem);

int down_interruptible(struct semaphore *sem);

int down_trylock(struct semphore *sem);

down函数会一直等下去,down_interruptible允许等待在某个信号量上的用户空间进程可被用户中断,被中断时返回一个非零值并不拥有锁,down_trylock永不会休眠,拿不到锁就理解返回一个非]零值。

V操作是up:

void up(struct semaphore *sem);

操作返回不同返回值后,内核的高层代码会有不同的反应



读取者/写入者信号量:

rwsem(reader/writer semaphore,读取者/写入者信号量)。<linux/rwsem.h>,struct rw_semaphore。

必须显式初始化:

void init_rwsem(struct rw_semaphore *sem);

只读访问可用的接口如下:
void down_read(struct rw_semaphore *sem);

void down_read_trylock(struct rw_semaphore *sem);

void up_read(struct rw_semaphore *sem);

写入者的接口:
void down_write(struct rw_semaphore *sem);

int down_write_trylock(struct rw_semaphore *sem);

void up_write(struct semaphore *sem);

void downgrade_write(struct rw_semaphore *sem);

当某个快速改变获得了写入者锁,而其后是更长时间的只读访问,可以在结束修改后调用downgrade_write,来允许其他读取者的访问。

写入者拥有更高优先级,在所有写入者完成其工作之前,不会允许读取者获得访问,所以最好在很少需要写访问且写入者只会短期拥有信号量的时候使用rwsem。



completion

内核编程中常见的一种模式是,在当前进程之外初始化某个活动,然后等待该活动的结束,这个活动可能是,创建一个新的内核线程或者新的用户空间进程、对一个已有进程的请求,或者某种类型的硬件动作,等等。可用如下代码:
struct semaphore sem;

init_MUTEX_LOCKED(&sem);

start_external_task(&sem);

down(&sem);

当外部任务完成其工作时,将调用up(&sem)。

但信号量并不是使用这种情况最好的工具。在通常的使用中,试图锁定某个信号量的代码会发现该信号量几乎总是可用的。而如果存在针对该信号量的严重竞争,性能将会受到影响,这时,需要重新审视锁定机制,因此,信号量对“可用”情况已做了大量优化。然而,如果像上面那样使用信号量在完成任务时进行通信,则调用down的线程几乎总是要等待,这样性能也同样会受到影响。如果信号量在这种情况下声明为自动变量,则也可能受某个(难对付)竞态的影响。在某些情况下,信号量可能在调用up的进程完成其相关任务前消失。

上述考虑导致了completion接口的出现。是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成。为了使用completion,代码必须包含<linux/completion.h>。可以利用下面的接口创建:

DECLARE_COMPLETION(my_completion);

或者动态创建和初始化:
strcut complection my_completion;

init_complection(&my_completion);


等待complection可调用:

void wait_for_completion(struct complection *c);

该函数执行一个非中断的等待。如果代码调用了wait_for_completion且没有人会完成该人物,则会将产生一个不可杀的进程。

可由以下函数触发completion事件:
void completion(struct completion *c);

void completion_all(struct completion *c);

completion只会唤醒一个等待线程,complete_all允许唤醒所有等待线程。

一个completion通常是一个单次(one-shot)设备,它只会被使用一次然后就被丢弃了。但是仔细处理,completion可以被重复使用,如果没有使用completion_all,则我们可以重复使用一个completion结构,只要那个将要触发的事件是明确而不含糊的。但是如果使用了complete_all,则必须重复使用该结构之前重新初始化它,有快速初始化的函数:

INIT_COMPLETION(struct completion *c));


complete机制的典型使用是模块退出时的内核线程终止。在这种原型中,某些驱动程序的内部工作由一个内核线程在while(1)循环中完成,当内核准备清除该模块时,exit函数会告诉线程退出并等待completion。为实现这个目的,内核包含了可用于这种线程的一个特殊函数:
void complete_and_exit(struct completion *c, long retval);


自旋锁:

自旋锁(“spinlock”)。可在不能休眠的代码中使用,一个自旋锁是一个互斥设备,它只能有两个值:“锁定”和“解锁”“测试并设置”的操作必须以原子方式完成,即使有多个线程在给定时间自旋,也只有一个线程可获得该锁。当存在自旋锁时,等待执行忙循环的处理器做不了任何有用的工作。


自旋锁API:

<linux/spinlock.h>,类型:spinlock_t类型。自旋锁的初始化可在编译时通过下面的代码完成:

spinlock_t my_lock=SPIN_LOCK_UNLOCKED;

或在运行时调用下面的函数:

void spin_lock_init(spinlock_t *lock);

在进入临界区之前,但嘛必须调用下面的函数获得需要的锁:

void spin_lock(spinlock_t *lock);

释放锁:

void spin_lock(spinlock_t *lock);

使用自旋锁的规则:

避免在获得自旋锁或休眠,或许会影响性能或造成死锁。适用于自旋锁的核心规则是:任何拥有自旋锁的代码都必须是原子的。他不能休眠,事实上,他不能以任何原因放弃处理器,除了服务中断以外(某些情况下此时也不能放弃处理器)

内核抢占的情况由自旋锁代码本身处理。任何时候,只有内核代码拥有自旋锁,在相关处理器上的抢占就会被禁止。甚至在单处理器系统上,也必须以同样的方式禁止抢占以避免竞态。这就是为什么我们不打算在多处理器系统上运行自己的代码,却仍然要正确的处理锁定的原因。

许多内核函数可以休眠,所以必须自己注意每个调用的函数。

自旋锁必须在可能的最短时间内拥有。

自旋锁函数:

锁定一个自旋锁的函数实际有4个:
void spin_lock(spinlock_t *lock);

void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);

void spin_lock_irq(spinlock_t *lock);

void spin_lock_bh(spinlock_t *lock);

spin_lock_irqsave会在获得自旋锁之前禁止中断(只在本地处理器上),先前的中断状态保存在flags中。如果我们能够确保没有任何其他代码禁止本地处理器的中断(或者换句话说,我们能够确保在释放自旋锁时应该启用中断),则可以使用spin_lock_irq,而无需跟踪标志。最后spin_lock_bh在获得锁之前禁止软件中断,但是会让硬件中断保持打开。

一个自旋锁,它可以被运行在(硬件或软件)中断上下文的代码获得,则必须使用某个禁止中断的spin_lock形式,因为使用其他的锁定函数迟早会导致系统死锁。如果我们不会在硬件中断处理例程中访问自旋锁,但可能在软件中断,(例如,以tasklet的形式运行的代码)中访问,则应该使用spin_lock_bh,以便在安全地避免死锁的同时还能服务硬件中断。


释放自旋锁方法:              严格对应

void spin_unlock(sinlock_t *lock);

void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);

void spin_unlock_irq(spinlock_t *lock);

void spin_unlock_bh(spinlock_t *lock);

每个spin_unlock变种都会撤销对应的spin_lock函数所做的工作,传递到spin_unlock_irqrestrore的参数flag必须是对应的那个,必须在同一个函数中调用spin_lock_irqsave和spin_unlock_irqrestore。否则代码可能在某些架构上出现问题。

如下非阻塞的自旋锁操作:

int spin_trylock(spinlock_t *lock);

int spin_trylock_bh(spinlock_t *lock);

禁止中断的没有try版。



读取者/写入者自旋锁:

rwlock_t类型,<linux/spinlcok.h>中定义。

两种声明和定义方式:

rwlock_t my_rwlock = RW_LOCK_UNLOCKED; //Static way


rwlock_t my_rwlock;

rwlock_init (&my_rwlock); //Dynamic way


void read_lock(rwlock_t *lock);

void read_lock_irqsave(rwlock_t *lock, unsigned long falgs);

void read_lock_irq(rwlock_t *lock);

void read_lock_bh(rwlock_t *lock);

void read_unlock(rwlock_t *lock);

void read_unlock_irqsave(rwlock_t *lock, unsigned long falgs);

void read_unlock_irq(rwlock_t *lock);

void read_unlock_bh(rwlock_t *lock);

read的没有try


void write_lock(rwlock_t *lock);

void write_lock_irqsave(rwlock_t *lock, unsigned long falgs);

void write_lock_irq(rwlock_t *lock);

void write_lock_bh(rwlock_t *lock);

void write_trylock(rwlock_t *lock);

void write_unlock(rwlock_t *lock);

void write_unlock_irqsave(rwlock_t *lock, unsigned long falgs);

void write_unlock_irq(rwlock_t *lock);

void write_unlock_bh(rwlock_t *lock);



使用锁的注意要点:
必须前期做好安排;

获得某个锁的函数决不能调用试图获得这个锁的其他函数,系统会挂起;

有时必须编写有锁和无锁两个版本的函数,而且要做好标记;

在需要获得多个锁时,始终以同样的顺序获得这些锁;

避免多个锁,先获得局部锁在获得核心锁,先获得信号量再获得自旋锁;


如果我们怀疑竞争锁导致性能下降,可以使用lockmeter工具,这个补丁可度量内核花费在锁上的时间。



不用锁的方法:

循环缓冲区:
通用的循环缓冲区实现<linux/kfifo.h>

原子变量:
内核提供一种院子的整数类型atomic_t定义在<asm/atomic.h>

两种初始化方式:

void atomic_set(atomic_t *v, int i);

atomic_t v=ATOMIC_INIT(0);

int atomic_read(atomic_t *v);

返回v的当前值

void atomic_add(int i, atomic_t *v);

累加i,无返回值

void atomic_sub(int i, atomic_t *v);

减去i,无返回值

void atomic_inc(atomic_t *v);

void atomic_dec(atomic_t *v);

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,则返回true。

注意不存在 atomic_add_and_tedt函数。

int atomic_add_negative(int i, atomic_t *v);

将整数变量i累加到v,返回值结果为负true,否则为false。


int atomic_add_return(int i, atomic_t *v);

int atomic_sub_return(int i, atomic_t *v);

int atomic_inc_return(int i, atomic_t *v);

int atomic_dec_return(int i, atomic_t *v);

有返回值


atomic_t数据项必须由上述函数访问, 否则出错。


位操作:

原子位操作,在<linux/bitops.h>中声明。

可用操作:

void set_bit(nr, void *addr);

设置addr指向的数据项的第nr为

void clear_bit(nr, void *addr);

清除

void change_bit(nr, void *addr);

切换

test_bit(nr, void *addr);

该函数是唯一一个不必以原子方式实现的,它仅仅返回指定位的当前值

int test_and_set_bit(nr, void *addr);

int test_and_clear_bit(nr, void *addr);

int test_and_change_bit(nr, void *addr);

改变并返回先前值。


seqlock

2.6内核包含两个新的机制,可提供对共享资源的快速访问、免锁访问。当要保护的资源很小、很简单、会很频繁被访问而且写入访问很少发生且必须快速时,就可以使用seqlock。

本质上讲,seqlock会允许读取者对资源的自由访问,但需要读取者检查是否和写入者发生冲突,当这种冲突发生时,就需要重试对资源的访问。seqlock通常不能用于保护包含指针的数据结构,因为在写入者修改该数据结构的同时,读取者可能会追随一个无效的指针。


<linux/seqlock.h> seqlock_t类型

初始化方法有两种:

seqlock_t lock1=SEQLOCK_UNLOCKED;

seqlock_t lock2;

seqlock_init(&lock2);

读取访问通过获得一个(无符号)整数顺序值而进入临界区。在退出时,该顺序值会和当前比较,如果不相等,则必须重试读取访问。其结果是,读取者代码会如下编写:

unsigned int seq;

do{

seq=read_seqbegin(&the_lock);

//完成相应工作

}while raed_seqretry(&the_lock,seq);

如果在中断处理例程中使用seqlock,则应该使用IRQ安全版本:

unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);

int read_seqretry_irqrestore(seqlock_r *lock, unsigned int seq, unsigned long flags);

写入者必须在进入由seqlock保护的临界区时获得一个互斥锁。为此需调用下面的函数:

void write_seqlock(seqlock_t *lock);

写入锁使用自旋锁实现,因此自旋锁的常见限制也使用于写入锁

void wirite_sequnlock(seqlock_t *lock);

常见自旋锁的变种都可以使用:

void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags);

void write_seqlock_irq(seqlock_t *lock);

void write_seqlock_bh(seqlock_t *lock);


void write_sequnlock_irqsave(seqlock_t *lock, unsigned long flags);

void write_sequnlock_irq(seqlock_t *lock);

void write_sequnlock_bh(seqlock_t *lock);

如果write_tryseqlock可以获得自旋锁,它也会返回非零值。


读写-复制-更新:

(read-copy-update,RCU),很有名但很少在驱动程序中使用。它针对经常发生读取而很少写入的情形做了优化。被保护的资源应该通过指针访问,而对这些资源的引用必须仅由原子代码拥有,在需要修改该数据结构时,写入线程首先复制,然后修改副本,之后用新的版本替代相关指针。当内核确定老版本没有其他引用时,就可释放老的版本。

比较复杂,建议参考头文件来了解接口

<linux/rcupdata.h>


本文出自 “重剑无锋” 博客,请务必保留此出处http://qianyang.blog.51cto.com/7130735/1620571

并发与竞态

标签:linux驱动

原文地址:http://qianyang.blog.51cto.com/7130735/1620571

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!