1、什么是竞争状态,之前在应用编程的学习中已经提到过。
竞争状态就是在多进程环境下,多个进程同时抢占系统资源(内存、CPU、文件IO),竞争状态对OS来说是很危险的,此时OS如果没处理好就会造成意想不到的结果。
写程序当然不希望程序运行的结果不确定,所以我们写程序时要尽量消灭竞争状态。操作系统给我们提供了一系列的消灭竟态的机制,我们需要做的是在合适的地方使
用合适的方法来消灭竟态。这是之前在应用编程中说的,但是在内核态中同样存在竞争状态,所以今天来说说Linux内核中提供的用来消灭竞争状态的方法。
内核中消灭竞争状态的方法:互斥锁、信号量、原子访问、自旋锁
首先说明本次使用的内核版本是: Linux 2.6.35.7
一些概念:临界段、互斥锁、死锁
同步:多CPU、多任务、中断
2、互斥锁
互斥也就是相互排斥的意思,排他性,在编程中引入互斥是为了保证公共访问对象在同一时间(这个同一时间指的是宏观上的同一时间)内只能被一个线程
进行访问操作。当有一个线程在访问这个对象的时候,其他的线程是无法对他进行访问的。他的实现原理就是通过互斥锁来实现的,当线程访问公共对象时,在访问之前
他需要先去获取访问这个对象的锁,如果此时无法获取锁,则表示此时已经有其他的线程正在访问这个对象,那么他就如进入等待队列当中,进入阻塞状态。
其实这个过程就好比是上厕所(只有一个厕所),当厕所没人的时候你就直接进去把门关上,其他人想要上厕所就得先把门打开,发现门打不开,说明里面就有人,只能等待。
下面相关的函数定义和声明分别在: kernel\mutex.c 和 include\linux\mutex.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、信号量
互斥锁其实是一种特殊的信号量,互斥锁表示只有一个线程能够同时访问某个对象,而信号量则可以定义同时访问对象的线程数。也就是说互斥锁是只能同时访问一个对象
的信号量。下面函数定义和申明所在文件: include\linux\semaphore.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