linux内核并发和竟态 (解决竟态的5种方式屏蔽中断,原子操作,自旋锁,信号量,互斥体,)

linux内核并发和竟态 :

并发(Concurrency) 指的是多个执行单元同时、 并行被执行, 而并发的执行单元对共享资源(硬件资源和软件上的全局变量、 静态变量等) 的访问则很容易导致竞态(Race Conditions)

linux内核并发和竟态 (解决竟态的5种方式屏蔽中断,原子操作,自旋锁,信号量,互斥体,)

引起竟态的方式:

                1 对称多处理器(SMP) 的多个CPU

              linux内核并发和竟态 (解决竟态的5种方式屏蔽中断,原子操作,自旋锁,信号量,互斥体,)   

               2 单CPU内进程与抢占它的进程

                   一个进程在内核执行的时候可能耗完了自己的时间片(timeslice) , 也可能被另一个高优先级进程打断, 进程与抢占它的进程访问共享资源的情况类似于SMP的多个CPU

                    3 中断(硬中断、 软中断、 Tasklet、 底半部) 与进程之间(中断和更高级的中断)

                  中断可以打断正在执行的进程, 如果中断服务程序访问进程正在访问的资源, 则竞态也会发生。此外, 中断也有可能被新的更高优先级的中断打断, 因此, 多个中断之间本身也可能引起并发而导致竞态。 但是Linux 2.6.35之后,就取消了中断的嵌套  

解决竞态问题的途径:保证对共享资源的互斥访问, 所谓互斥访问是指一个执行单元在访问共享资源的时候, 其他的执行单元被禁止访问。

互斥的机制:屏蔽中断,原子操作,自旋锁,信号量,互斥体

屏蔽中断: 中断屏蔽只对单核cpu生效,中断屏保护的临界区非常的小在中断屏蔽期间不能够做耗时(mdelay)或者延时的操作,进程调度和异步I/O都依赖于中断,在中断屏蔽期间所有的中断无法得到处理,长时间屏蔽会导致系统奔溃,在驱动编程中不值得推荐。 中断屏蔽将使得中断与进程之间的并发不再发生, 而且,由于Linux内核的进程调度等操作都依赖中断来实现, 内核抢占进程之间的并发也得以避免 

中断屏蔽的使用方法为:

    local_irq_disable() /* 屏蔽中断 */

    . . .

    critical section /* 临界区 */

    . . .

    local_irq_enable() /* 开中断 */、

 

local_irq_disable() 和local_irq_enable() 都只能禁止和使能本CPU内的中断, 因此, 并不能解决SMP多CPU引发的竞态。 因此, 单独使用中断屏蔽通常不是一种值得推荐的避免竞态的方法(换句话说。驱动中使用local_irq_disable/enable() 通常意味着一个bug) , 它适合与下文将要介绍的自旋锁联合使用。

 

原子操作

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);//加锁每调用一次变量都会加1 所以调用完以后使用atomic_dec(v) 进行相减

int atomic_dec_and_test(atomic_t *v);//减上1之后和0比较,如果结果为0 ,表示获取锁成功了,否则失败了

int atomic_sub_and_test(int i, atomic_t *v); 

上述操作对原子变量执行自增、 自减和减操作后(注意没有加) , 测试其是否为0, 为0返回true, 否则返回false。

原子的使用方式:

                 atomic_t atm = ATOMIC_INIT(-1);

                atomic_inc_and_test(v) //加锁每调用一次变量都会加1 所以调用完以后使用atomic_dec(v) 进行相减

                .......................................................

               .........................................................

                //加上1之后和0比较,如果结果为0 ,表示获取锁成功了,否则失败了

                atomic_dec(v)             //解锁

 

                atomic_t atm = ATOMIC_INIT(1);

                atomic_dec_and_test(v) //加锁

                //减上1之后和0比较,如果结果为0 ,表示获取锁成功了,否则失败了

                atomic_inc(v);解锁

 

自旋锁:在多处理器之间设置一个全局变量V,表示锁。并定义当V=1时为锁定状态,V=0时为解锁状态自旋锁同步机制是针对多处理器设计的,属于忙等机制。自旋锁机制只允许唯一的一个执行路径持有自旋锁.

 同步应遵循的机制:

                    空闲让进

                    忙则等待

                    有限等待

                    让权等待(当进程不能进入自己的临界区时,应立即释放处理机,以免进程陷入“忙等”状态)

特点:

            1.自旋锁在上锁期间是需要消耗cpu资源的

            2.在同一个进程如果多次获取同一把锁此时cpu死锁(执行这个代码的cpu)

            3.自旋锁工作在中断上下文

            4.自旋锁在上锁期间不允许做耗时或者延时操作

                mdelay(10);  copy_to_user/copy_from_user

                schedule();(主动放弃cpu)

            5.自旋锁在上锁期间会关闭抢占

            6.自旋锁使用的最后一个重要规则是自旋锁必须一直是尽可能短时间的持有. 你持有一个锁越长, 另一个进程可能不得不自旋等待你释放它的时间越长, 它不得不完全自旋的机会越大. 长时间持有锁也阻止了当前处理器调度, 意味着高优先级进程 -- 真正应当能获得 CPU 的 -- 可能不得不等待.            

 

Linux中与自旋锁相关的操作主要有以下4种:

1.定义自旋锁

spinlock_t lock;

2.初始化自旋锁

spin_lock_init(lock)

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

3.获得自旋锁

spin_lock(lock)

该宏用于获得自旋锁lock, 如果能够立即获得锁, 它就马上返回, 否则, 它将在那里自旋, 直到该自旋锁的保持者释放。

spin_trylock(lock)

该宏尝试获得自旋锁lock, 如果能立即获得锁, 它获得锁并返回true, 否则立即返回false, 实际上不再在原地打转

4.释放自旋锁

spin_unlock(lock)

该宏释放自旋锁lock, 它与spin_trylockspin_lock配对使用。

 

普通自旋锁的接口函数:

           spin_lock_init(lock)  //声明自旋锁是,初始化为锁定状态

           spin_lock(lock)//锁定自旋锁,成功则返回,否则循环等待自旋锁变为空闲

           spin_unlock(lock) //释放自旋锁,重新设置为未锁定状态

           spin_is_locked(lock) //判断当前锁是否处于锁定状态。若是,返回1.

           spin_trylock(lock) //尝试锁定自旋锁lock,不成功则返回0,否则返回1

           spin_unlock_wait(lock) //循环等待,直到自旋锁lock变为可用状态。

           spin_can_lock(lock) //判断该自旋锁是否处于空闲状态                                                   

自旋锁的使用方法:

              自旋锁一般这样被使用:

               /* 定义一个自旋锁 */

               spinlock_t lock;

               spin_lock_init(&lock);

               spin_lock (&lock) ; /* 获取自旋锁, 保护临界区 */

               . . ./* 临界区 */

               spin_unlock (&lock) ; /* 解锁

自旋锁主要针对SMP或单CPU但内核可抢占的情况, 对于单CPU和内核不支持抢占的系统,对于内核支持抢占的系统. 自旋锁持有期间中内核的抢占将被禁止,多核SMP的情况下, 任何一个核拿到了自旋锁, 该核上的抢占调度也暂时禁止了, 但是没有禁止另外一个核的抢占调度。

 

注意:尽管用了自旋锁可以保证临界区不受别的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_lock() + 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()

在多核编程的时候, 如果进程和中断可能访问同一片临界资源, 我们一般需要在进程上下文中调用spin_lock_irqsave() /spin_unlock_irqrestore() , 在中断上下文中调用spin_lock() /spin_unlock() ,

特别注意:  自旋锁实际上是忙等锁, 当锁不可用时, CPU一直循环执行“测试并设置”该锁直到可用而取得该锁, CPU在等待自旋锁时不做任何有用的工作, 仅仅是等待。 因此, 只有在占用锁的时间极短的情况下, 使用自旋锁才是合理的。 当临界区很大, 或有共享设备的时候, 需要较长时间占用锁, 使用自旋锁会降低系统的性能

自旋锁可能导致系统死锁。 引发这个问题最常见的情况是递归使用一个自旋锁, 即如果一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁, 则该CPU将死锁。

在自旋锁锁定期间不能调用可能引起进程调度的函数。 如果进程获得自旋锁之后再阻塞, 如调用copy_from_user() 、

copy_to_user() 、 kmalloc() 和msleep() 等函数, 则可能导致内核的崩溃

 

信号量:遵循进程同步中的让权等待的原则,同步机制在进程无法获取到临界资源的情况下,立即释放处理器的使用权,并睡眠在所访问的临界资源上对列的等待,在临界资源被释放时,再唤醒阻塞在该临界资源上的进程。另外,信号量机制不会禁用内核态抢占,所有持有信号量的进程一样可以抢占这意味着信号量机制不会给系统的响应能力,实时能力带来负面的影响

信号量的思想:信号量设计思想:

       除了初始化之外,信号量只能通过两个原子操作P()和V()访问,也称为down()和up(),down()原子操作通过

信号量的计数器减1,来请求获得一个信号量。如果操作后结果是0或者大于0,获得信号量锁,进入临界区.如果操作后进入临界区后结果是负数,任务会放入等待队列,处理器执行其他任务;对临界资源访问完毕后,可以调用原子操作up()来释放信号量,该操作后增加信号量的计数器。

当一个进程获取到锁之后,另外一个进程也想获取这把锁,此时第二个进程休眠状态。

普通信号量的接口函数:

            sema_init(sem,val)  //初始化信号量计数 器的值为val

            int_MUTEX(sem) //初始化信号量为一个互斥信号量

            down(sem)   //锁定信号量,若不成功,则睡眠在等待队列上

            up(sem) //释放信号量,并唤醒等待队列上的进程

            DOWN操作:linux内核中,对信号量的DOWN操作有如下几种:

            void down(struct semaphore *sem); //不可中断 进入睡眠状态的进程不能被信号打断

            int down_interruptible(struct semaphore *sem);//可中断 进入睡眠状态的进程能被信号打断,

            // 信号也会导致该函数返回, 这时候函数的返回值非0。

            int down_killable(struct semaphore *sem);//睡眠的进程可以因为受到致命信号而被唤醒,中断获取信号量的操作。

            int down_trylock(struct semaphore *sem);,若无法获得则直接返//试图获取信号量回1而不睡眠。返回0则 表示获取到了信号量

            int down_timeout(struct semaphore *sem,long jiffies);//表示睡眠时间是有限制的,如果在jiffies指明的时间到期时仍然无法获得信号量,则将返回错误码

             在使用down_interruptible()获取信号量时,对返回值一般会进行检查,如果非0,通常立即返回-ERESTARTSYS, 如:

                     if (down_interruptible(&sem))

                      return -ERESTARTSYS;

信号量和自旋锁不同的是, 当获取不到信号量时, 进程不会原地打转而是进入休眠等待状态。 用作互斥时, 信号量一般这样被使用,

互斥体:

互斥体的使用方法:

mutex的使用方法和信号量用于互斥的场合完全一样:

struct mutex my_mutex; /* 定义 mutex */

mutex_init(&my_mutex); /* 初始化 mutex */

mutex_lock(&my_mutex); /* 获取 mutex */

... /* 临界资源 */

mutex_unlock(&my_mutex); /* 释放 mutex *

普通的函数接口:

                struct mutex lock; //互斥体

                mutex_init(&lock)  //初始化

                mutex_lock(struct mutex *lock) //上锁

                mutex_trylock(struct mutex *lock) //尝试获取锁,成功返回1,失败返回0

                mutex_unlock(struct mutex *lock) //解锁

5个互斥的机制总结:

               互斥体和自旋锁属于不同层次的互斥手段 自旋锁属于更底层的手段。

               互斥体是进程级的, 用于多个进程之间对资源的互斥, 虽然也是在内核中, 但是该内核执行路径是以进程的身份, 代表进程来争夺资源的。 如果竞争失败, 会发生进程上下文切换, 当前进程进入睡眠状态, CPU将运行其他进程。 鉴于进程上下文切换的开销也很大, 因此, 只有当进程占用资源时间较长时, 用互斥体才是较好的选           

              当所要保护的临界区访问时间比较短时, 用自旋锁是非常方便的, 因为它可节省上下文切换的时间。 但是CPU得不到自旋锁会在那里空转直到其他执行单元解锁为止, 所以要求锁不能在临界区里长时间停留, 否则会降低系统的效率。