内核中防止竞争状态的手段

1、什么是竞争状态,之前在应用编程的学习中已经提到过。

竞争状态就是在多进程环境下,多个进程同时抢占系统资源(内存、CPU、文件IO),竞争状态对OS来说是很危险的,此时OS如果没处理好就会造成意想不到的结果。

写程序当然不希望程序运行的结果不确定,所以我们写程序时要尽量消灭竞争状态。操作系统给我们提供了一系列的消灭竟态的机制,我们需要做的是在合适的地方使

用合适的方法来消灭竟态。这是之前在应用编程中说的,但是在内核态中同样存在竞争状态,所以今天来说说Linux内核中提供的用来消灭竞争状态的方法。

内核中消灭竞争状态的方法:互斥锁、信号量、原子访问、自旋锁

首先说明本次使用的内核版本是: Linux 2.6.35.7

一些概念:临界段、互斥锁、死锁

同步:多CPU、多任务、中断

2、互斥锁

互斥也就是相互排斥的意思,排他性,在编程中引入互斥是为了保证公共访问对象在同一时间(这个同一时间指的是宏观上的同一时间)内只能被一个线程

进行访问操作。当有一个线程在访问这个对象的时候,其他的线程是无法对他进行访问的。他的实现原理就是通过互斥锁来实现的,当线程访问公共对象时,在访问之前

他需要先去获取访问这个对象的锁,如果此时无法获取锁,则表示此时已经有其他的线程正在访问这个对象,那么他就如进入等待队列当中,进入阻塞状态。

其实这个过程就好比是上厕所(只有一个厕所),当厕所没人的时候你就直接进去把门关上,其他人想要上厕所就得先把门打开,发现门打不开,说明里面就有人,只能等待。

下面相关的函数定义和声明分别在: kernelmutex.c  和  includelinuxmutex.h

(1)定义互斥锁: DEFINE_MUTEX(mutex);          定义 + 初始化

                      struct mutex mutexname;  mutex_init(mutexname);    先定义,之后再初始化    

(2)给互斥锁上锁: mutex_lock(struct mutex *lock);       这种方式进入休眠状态不会被打断,一直等待条件满足

                        int __must_check mutex_lock_interruptible(struct mutex *lock);   这种方式可以被中断返回,退出等待队列

                        int __must_check mutex_lock_killable(struct mutex *lock);     这种方式可以被杀掉

                        int mutex_trylock(struct mutex *lock);        这种方式会尝试上锁,不管成功与否都不会阻塞等待    

(3)解锁:   mutex_unlock(struct mutex *lock);           

整个互斥锁的使用形式如下:

先定义一个互斥锁: DEFINE_MUTEX(mutex);

在需要被保护的地方加上锁:  mutex_lock(struct mutex *lock);    或者其他的函数

当访问完成之后需要解锁,这个一定不要忘了:  mutex_unlock(struct mutex *lock);

3、信号量

互斥锁其实是一种特殊的信号量,互斥锁表示只有一个线程能够同时访问某个对象,而信号量则可以定义同时访问对象的线程数。也就是说互斥锁是只能同时访问一个对象

的信号量。下面函数定义和申明所在文件:  includelinuxsemaphore.h

需要注意的是:互斥锁的出现先对于信号量来说比较晚,所以他的实现上更加的优秀,所以使用时如果可以请尽量使用互斥锁,而不要用信号量。

(1)信号量的定义:  

             宏:     DECLARE_MUTEX(name);       直接定义一个信号量name,初始化为1,表示空闲状态。一般这个用的比较多

                       struct semaphore sem;   init_MUTEX(sem);                  需要自己先定义一个变量,在调用宏初始化

                       struct semaphore sem;    init_MUTEX_LOCKED(sem);    跟上面的区别在于定义之后初始化为0,表示忙状态,所以在访问之前必须先释放锁

             函数:  void sema_init(struct semaphore *sem, int val);    val表示能够同时访问被保护对象的线程数量,上面2个宏内部就是调用了这个函数实现的

(2)信号量上锁:  

                      down(struct semaphore *sem);        将信号量计数值count减1,如果在执行down函数前,计数值已经为0,则表示不能在进行访问了,只能进入阻塞休眠,等待

                      int __must_check down_interruptible(struct semaphore *sem);  和上面的区别在于,阻塞可以被打断,例如信号,直接退出等待队列

                      int __must_check down_killable(struct semaphore *sem);    在阻塞等待的时可以被杀掉

                      int __must_check down_trylock(struct semaphore *sem);     尝试上锁,不管成功与否都直接返回,而不进入阻塞等待队列

                      int __must_check down_timeout(struct semaphore *sem, long jiffies);     可以设置等待超时时间,如果等待时间超过规定的时间,则直接退出等待队列

(3)信号量解锁: 

                      up(struct semaphore *sem);            解锁信号量,将信号量计数值count加1

使用方法跟互斥锁差不多。

4、自旋锁

自旋锁最初是为了在SMP系统中实现保护临界区而设计的,自旋锁的实现是为了保护一段短小的临界区操作代码,保证这个临界区的操作是原子的,从而避免并发的竞争冒险。

因为在SMP多核心处理器中可以在微观上同时执行多个进程,当这几个进程同时对临界保护区的代码进行访问的时候,这就发生了竞争状态。为了避免这种情况的出现,可以

通过内核实现的自旋锁来避免,他的原理就是:当进程访问临界保护区代码时,跟上面的信号量和互斥锁一样需要先去获取锁,只有成功获取到锁的进程才能够对代码进行访问

,并且访问是原子性的,必须当这个临界区代码执行完毕解锁之后才能恢复。没有获取到锁的进程并不会进入休眠阻塞的状态,而是会一直循环查询锁状态,这就是自旋的意思

这点与信号量(互斥锁)是不同的。

上面说的是SMP处理器的情况,在单核心非抢占内核系统(抢占的意思就是高优先级的线程可以优先抢占CPU)中,自旋锁所做的操作时空操作。在单核心抢占内核系统下,自旋

锁仅仅是当作一个设置内核抢占的开关,当获取到的线程进入临界区代码中时,此时就把内核抢占系统关闭了,同时也会禁止中断,所以此时除了正在执行他自己的代码外没有其

他的线程在动作了。

自旋锁是循环检测“忙等”,即等待时内核无事可做(除了浪费时间),进程在CPU上保持运行,所以它保护的临界区必须小,

且操作过程必须短。不过,自旋锁通常非常方便,因为很多内核资源只锁1毫秒的时间片段,所以等待自旋锁的释放不会消耗太多CPU的时间。

(1)自旋锁和信号量的使用要点

  1): 自旋锁不能递归

  2): 自旋锁可以用在中断上下文(信号量不可以,因为可能睡眠),但是在中断上下文中获取自旋锁之前要先禁用本地中断(本CPU自己的中断),因为中断是不参与进程调度的,

       如果我们的CPU运行在中断中时丢失了CPU,那么将再也不能会在这个中断的断点处了。

  3): 自旋锁的核心要求是:拥有自旋锁的代码必须不能睡眠,要一直持有CPU直到释放自旋锁。

  4): 信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用

       。如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共享资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在

       中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自

       旋锁只有在内核可抢占或SMP(多处理器)的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。

(1)初始化自旋锁:

              宏:     struct spinlock_t lock;       spin_lock_init(&lock);      初始化一个自旋锁,将自旋锁设置为1,表示资源可用

(2)自旋锁上锁:

             函数:  spin_lock(spinlock_t *lock);         循环等待直到自旋锁解锁(置为1),之后将自旋锁锁上(置为0)

                        spin_lock_bh(spinlock_t *lock);    循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。阻止软中断的底半部的执行。

                        int spin_trylock(spinlock_t *lock);  尝试锁上自旋锁(把它置为0),不管成功与否,都直接返回,而不循环等待

                       spin_lock_irq(spinlock_t *lock);     循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。关闭中断

             宏:    spin_lock_irqsave(lock, flags);       循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。关中断,将状态寄存器值存入flags。

(3)自旋锁解锁:

             函数: spin_unlock(spinlock_t *lock);               将自旋锁解锁(置为1)

                       spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);      将自旋锁解锁(置为1)。开中断,将状态寄存器值从flags存入状态寄存器。

                       spin_unlock_irq(spinlock_t *lock);      将自旋锁解锁(置为1)。开中断。

                       spin_unlock_bh(spinlock_t *lock);     将自旋锁解锁(置为1)。开启底半部的执行。

(4)其他的一些辅助函数:

                      int spin_is_locked(spinlock_t *lock);          如果自旋锁被锁上了,返回0,否则返回1

                     spin_unlock_wait(spinlock_t *lock);       等待直到自旋锁解锁(为1),返回0;否则返回1。 

(5)自旋锁与信号量或者互斥锁的使用

需求                                 建议的加锁方式

低开销加锁                        优先使用自旋锁

短期锁定                           优先使用自旋锁

长期锁定                           优先使用信号量或者是互斥锁

中断上下文加锁                  优先使用自旋锁

如果需要进入休眠、调度     优先使用信号量或者是互斥锁

5、原子操作

这部分我之后再补充

参考:http://blog.csdn.net/tommy_wxie/article/details/7283307

         http://blog.chinaunix.net/uid-26990992-id-3264808.html

                       

 

 

原文地址:https://www.cnblogs.com/deng-tao/p/6045127.html