Chinaunix首页 | 论坛 | 博客
  • 博客访问: 81101
  • 博文数量: 11
  • 博客积分: 1514
  • 博客等级: 上尉
  • 技术积分: 150
  • 用 户 组: 普通用户
  • 注册时间: 2009-09-19 19:10
文章分类
文章存档

2010年(11)

我的朋友

分类: 嵌入式

2010-07-08 00:31:54

    并发(concurrency)指的是多个执行单元同时、并行执行,而并发的执行单元对共享资源( 硬件资源和软件上的全局变量、静态变量等 )的访问则很容易导致竞态(race conditions)。
    并发产生竞态,竞态导致共享数据的非法访问。因为竞态是一种极端低可能性的事件,因此程序员往往会忽视竞态。但是在计算机世界中,百万分之一的事件可能每几秒就会发生,而其结果是灾难性的。竞态通常是作为对资源的共享访问结果而产生的。在设计自己的驱动程序时,第一个要记住的规则是:只要可能,就应该避免资源的共享。若没有并发访问,就不会有竞态。这种思想的最明显的应用是避免使用全局变量。但是,资源的共享是不可避免的

    下面让我们来学习下解决竞态的几种方法。

一、信号量与互斥体
    一个信号量(semaphore)本质上是一个整数值,它和一对函数联合使用,这一对函数通常称为P和V。希望进入临届区的进程将在相关信号量上调用P;如果信号量的值大于零,则该值会减小一,而进程可以继续。相反,如果信号量的值为零(或更小),进程必须等待知道其他人释放该信号。对信号量的解锁通过调用V完成;该函数增加信号量的值,并在必要时唤醒等待的进程。
    当信号量用于互斥时(即避免多个进程同是在一个临界区运行),信号量的值应初始化为1。这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一个信号量有事也称为一个“互斥体(mutex)”,它是互斥(mutual exclusion)的简称。Linux内核中几乎所有的信号量均用于互斥
    使用信号量,内核代码必须包含
直接创建信号量:

void sema_init(struct semaphore *sem, int val);         //  val赋予信号量初始值

    信号量通常被用于互斥模式,初始化方式:

1、用于声明和初始化互斥体的宏
DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);

2、在运行时初始化互斥体

void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);
/*带有“_LOCKED”的是将信号量初始化为0,其初始状态是锁定的,允许任何线程访问之前显式解锁该互斥体。没带的初始化为1。*/

    锁定和解锁信号量:

void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);

void up(struct semaphore *sem);

    down会将调用进程置于不可中断的休眠状态。但是是建立不可杀进程的好方法。通常不使用该方法;
   down_interruptible可被信号中断。需要格外小心,若操作被中断,该函数会返回非零值,而调用这不会拥有该信号量。对其正确使用需要始终检查返回值,并做出相应的响应;
    down_trylock不会休眠,且会在信号量不可用时立即返回;
    up即解锁,
任何拿到信号量的线程都必须通过一次(只有一次)对up的调用而释放该信号量。在出错时,要特别小心;若在拥有一个信号量时发生错误,必须在将错误状态返回前释放信号量。
 
   读取者/写入者信号量
   一些任务只需要读取受保护的数据结构,而其他的则须做出修改。允许多个并发的读取者是可能的,只是它们之中没有哪个要做修改。这样做可大大提高性能,因为
只读任务可并行完成它们的工作,而不需要等待其他读取者退出临界区。Linux内核为这种情形提供了一种特殊的信号量类型,称读取者/写入者信号量“rwsem”,使用时必须包括 。读取者/写入者信号量的数据类型是struct rw_semaphore
初始化:

void init_rwsem(struct rw_semaphore *sem);

 
只读接口:

void down_read(struct rw_semaphore *sem);             /* 只读并发访问,不可中断 */
int down_read_trylock(struct rw_semaphore *sem);     
/* 读取访问不等待 */
void up_read(struct rw_semaphore *sem);               /* 释放rwsem */

 
写入接口:

void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);

void downgrade_write(struct rw_semaphore *sem);

    对于downgrade_write,该函数用于把写者降级为读者,这有时是必要的。因为写者是排他性的,因此在写者保持读写信号量期间,任何读者或写者都将无法访问该读写信号量保护的共享资源,对于那些当前条件下不需要写访问的写者,降级为读者,使得等待访问的读者能够立刻访问,从而增加了并发性,提高了效率。

    一个 rwsem 允许一个写者或无限多个读者来拥有该信号量. 写者有优先权; 当某个写者试图进入临界区, 就不会允许读者进入直到写者完成了它的工作. 如果有大量的写者竞争该信号量,则这个实现可能导致读者“饿死”,即可能会长期拒绝读者访问。因此, rwsem 最好用在很少请求写的时候, 并且写者只占用短时间.

二、completion

    completion是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成。代码必须包含
    创建completion:
DECLARE_COMPLETION(my_completion);
    或者动态创建和初始化completion:
struct completion my_completion;
static inline void init_completion(&my_completion);
    等待completion:
void wait_for_completion(struct completion *c);
    触发completion事件:
void complete(struct completion *c);                /* 唤醒一个等待completion的线程 */
void complete_all(struct completion *c);            /* 唤醒所有等待completion的线程 */
    如果未使用completion_all,completion可重复使用;否则必须使用下面宏对其重新初始化:
INIT_COMPLETION(struct completion c);/
    completion的典型使用是模块退出时的内核线程终止。在这种远行中,某些驱动程序的内部工作有一个内核线程在while(1)循环中 完成。当内核准备清楚该模块时,exit函数会告诉该线程退出并等待completion。为此内核包含了用于这种线程的一个特殊函数:

void complete_and_exit(struct completion *c, long retval);


三、自旋锁
    自旋锁是一个互斥设备,它只能有两个值:“锁定”和“解锁”。它通常实现为某个整数之中的单个位。如果锁可用,则“锁定”位被设置,而代码继续进入临界区;相反,如果锁被其他人获得,刚代码进入忙循环,直到锁可用为止。
   适用于自旋锁的核心规则:
  (1)任何拥有自旋锁的代码都必须使原子的,除服务中断外(某些情况下也不能放弃CPU,如中断服务也要获得自旋锁。为了避免这种锁陷阱,需要在拥有自旋锁时禁止中断),不能放弃CPU(如休眠,休眠可发生在许多无法预期的地方)。否则CPU将有可能永远自旋下去(死机)。
  (2)拥有自旋锁的时间越短越好。
   自旋锁原语所需包含的文件是 ,以下是自旋锁的内核API:

   初始化spinlock:

spinlock_t my_lock = SPIN_LOCK_UNLOCKED;   /*  编译时初始化spinlock */
void spin_lock_init(spinlock_t *lock);     /*  运行时初始化spinlock */
/* 所有spinlock等待本质上是不可中断的,一旦调用spin_lock,在获得锁之前一直处于自旋状态*/

    获得spinlock:

void spin_lock(spinlock_t *lock);          /*  获得spinlock */
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); /* 在获得自旋锁之前禁止中断,先前的中断状态保存在flags中 */
void spin_lock_irq(spinlock_t *lock);      /*  能确保释放自旋锁时应该启用中断 */
void spin_lock_bh(spinlock_t *lock)        /* 
在获得之前禁止软件中断,保持硬件中断打开 */
   释放
spinlock:
void spin_unlock(spinlock_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);
   非阻塞的自旋锁操作:
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);

/* 以上两个函数在成功获得自旋锁时,返回非零值;否则返回零*/

 
   读取者/写入者自旋锁:
    内核提供提供自旋锁的读取者/写入者形式,与rwsem十分相似。这种锁允许任意数量的读取者同时进入临界区,但写入者必须互斥访问。其数据类型rwlock_t,在中定义。

    声明和初始化:

rwlock_t my_rwlock = RW_LOCK_UNLOCKED;       /*  静态方式 */
rwlock_t my_rwlock;                         
/*  动态方式 */
rwlock_init(&my_rwlock); 

    获取只读接口:
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);

   释放只读接口:
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);
    获得只写接口:
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);

    释放只写接口:
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);


    总结自旋锁和信号量:
    自旋锁和信号量"类似而不类",类似说的是他们功能上的相似性,"不类"指代他们在本质和实现机理上完全不相同,不属于一类。
  
"不类":自旋锁不会引起调用者睡眠,假如自旋锁已被别的执行单元保持,调用者就一直循环查看是否该自旋锁的保持者已释放了锁,"自旋"就是"在原地打转"。而信号量则引起调用者睡眠,他把进程从运行队列上拖出去,除非获得锁。
  
"类似"无论是信号量,还是自旋锁,在任何时刻,最多只能有一个保持者,即在任何时刻最多只能有一个执行单元获得锁。
  鉴于自旋锁和信号量的上述特点,一般而言,自旋锁适合于保持时间很短的情况,它能够在任何上下文使用;信号量适合于保持时间较长的情况,会只能在进程上下文使用。假如被保护的共享资源只在进程上下文访问,则能够以信号量来保护该共享资源,假如对共享资源的访问时间很短,自旋锁也是好的选择。但是,假如被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。


四、锁陷阱
     锁定模式必须在一开始就安排好,否则其后的改进将会非常困难。
     不明确规则:如果某个获得锁的函数要调用其他同样试图获取这个锁的函数,代码就会锁死。(不允许锁的拥有者第二次获得同个锁。)为了锁的正确工作,不得不编写一些函数,这些函数假定调用这已经获得了相关的锁。
     锁的顺序规则:再必须获取多个锁时,应始终以相同顺序获取。
     (1) 若必须获得一个局部锁和一个属于内核更中心位置的锁,应先获得局部锁。
     (2) 若我们拥有信号量和自旋锁的组合,必须先获得信号量。
     (3) 不得再拥有自旋锁时调用down。(可导致休眠)
     (4) 尽量避免需要多个锁的情况。
    细颗粒度和粗颗粒度的对比:应该在最初使用粗颗粒度的锁,除非有真正的原因相信竞争会导致问题。

五、锁之外的办法
 
  1、免锁算法
    经常用于免锁的生产者/消费者任务的数据结构之一是循环缓冲区。它在设备驱动程序中相当普遍,如以前移植的网卡驱动程序。内核里有一个通用的循环缓冲区的实现在
 
  2、原子变量
   完整的锁机制对一个简单的整数来讲显得浪费。内核提供了一种原子的整数类型,称为atomic_t,定义在原子变量操作是非常快的, 因为它们在任何可能时编译成一条单个机器指令。以下是其接口函数:

void atomic_set(atomic_t *v, int i);       /* 设置原子变量 v 为整数值 i */
atomic_t v = ATOMIC_INIT(0);               /* 编译时利用ATOMIC_INIT
初始化原子值 */

int atomic_read(atomic_t *v);              /* 返回 v 的当前值 */

void atomic_add(int i, atomic_t *v);       /* 将i累加到V指向的原子变量,返回值是void */
void atomic_sub(int i, atomic_t *v);       /* 从*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; 否则返回false. 注意不存在atomic_add_and_test函数 */

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(atomic_t *v);
int atomic_dec_return(atomic_t *v);
/* 像atomic_add 和其类似函数, 除此之外,它们会将新值返回给调用者 */

    记住:
    (1)atomic_t 数据项必须通过上述函数访问。 如果将原子变量传递给一个需要整数参数的函数,则会遇到编译错误。

    (2)只有原子变量的数目是原子的,
atomic_t 变量才能工作。需要多个atomic_t变量的操作仍然需要某种其他种类的加锁。
 
   3、位操作
     内核提供了一套可原子地修改或测试单个位的函数。原子位操作非常快, 因为它们使用单个机器指令来进行操作,且不需要禁止中断。这些函数依赖于具体的架构,在 中声明。同时这些函数数据类型也是依赖于具体架构的。 nr参数(描述要操作哪个位)在ARM体系中定义为unsigned int

void set_bit(nr, void *addr);           /* 设置addr指向的数据项的第nr位 */
void clear_bit(nr, void *addr);         /*
清除addr指向的数据项的第nr位 */

void change_bit(nr, void *addr);        /* 翻转nr位 */
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);

/* 像前面列出的函数一样具有原子化行为,除此之外,它们还返回这个位以前的值 */

   
    4、seqlock
    2.6内核包含了两个新机制,可提供对共享资源的快速、免锁访问。当要保护的资源很小、很简单、会频繁被访问、写入访问很少发生且必须快速时,就可以使用seqlock。seqlock允许读取者对资源的自由访问,但需要读取者检查和写入者发生冲突。seqlock通常不能用于包含有指针的数据结构。seqlock数据类型为seqlock_t,定义在 中。
    初始化两种方法:

1) seqlock_t lock1 = SEQLOCK_UNLOCKED;
2) seqlock_t lock2;
    seqlock_init(&lock2);

   读取访问通过在进入临界区入口获取一个(无符号的)整数序列来工作。 在退出时, 那个序列值与当前值比较; 如果不匹配, 读读取访问必须重试。读者代码形式如下: 

unsigned int seq;
do {
    seq = read_seqbegin(&the_lock);
   /*  完成需要做的工作  */
} while read_seqretry(&the_lock, seq);

    如果在一个中断处理中访问seqlock, 你应当使用IRQ安全的版本:

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

   写入者必须进入由seqlock保护的临界时获取一个互斥锁,需要调用下面的函数:

void write_seqlock(seqlock_t *lock);
void write_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_irqrestore(seqlock_t *lock, unsigned long flags);
void write_sequnlock_irq(seqlock_t *lock);
void write_sequnlock_bh(seqlock_t *lock);

    还有一个write_tryseqlock在它能够获得锁时返回非零.

    5、读取-复制-更新

    读取-复制-更新(RCU) 也是一个高级的互斥方法, 在合适的情况下也可获得高性能。它在驱动中的使用很少。

   RCU对它保护的数据做了一些限定。它针对经常发生读取而很少写入的情形做了优化。被保护的资源应该通过指针访问。而对这些资源的引用必须仅由原子代码拥有。在需要修改该数据结构时,写入线程首先提制,然后修改复本,之后用新的版本替代相关指针,这也是该算法名称的由来。

   在读取端,代码使用受RCU保护的数据结构时,必须将引用数据结构的代码包括在rcu_read_lock和rcu_read_unlock调用之间例如:

struct my_stuff *stuff;

rcu_read_lock();
stuff = find_the_stuff(args...);
do_something_with(stuff);
rcu_read_unlock();
    rcu_read_lock调用非常快,它会禁止内核抢占,但不会等待任何东西。用来检验读取“锁”的代码必须是原子的。

    修改受RCU保护的数据结构的代码必须通过分配一个struct rcu_head数据结构来获得清除用的回调函数,但不需要用什么方法来初始化这个结构。通常,这个结构内嵌在由RCU保护的大资源中。在修改完资源后,应该做如下调用:

void cal_rcu(struct rcu_head *head,void (*func)(void *arg),void *arg);
    在可安全释放该资源时,给定的func会被调用,传递到call_rcu的相同参数也会传递给这个函数。通常,func要做的唯一工作就是调用kfree。
   


阅读(1656) | 评论(0) | 转发(0) |
0

上一篇:LDD--调试技术

下一篇:LDD--高级字符驱动

给主人留下些什么吧!~~