序言就像我们在操作系统里学习的那样,如果多个程序(进程或线程)同时访问临界区数据就会发生竞争。存在竞争条件的程序会产生不可预料的结果。消除竞争的方法一般就是同步的访问临界区数据(原子访问)。Linux内核提供了多种技术用来实现内核同步操作。下面我们就分别介绍。
内核同步技术Linux内核是多进程、多线程的操作系统,它提供了相当完整的内核同步方法。作为一个总结,我们先列出内核同步方法列表,这样我们可以从总体上对内核同步技术有个了解,然后我们这分别对每个同步技术做详细介绍。
同步技术 |
同步技术描述 |
自旋锁 |
|
读写自旋锁 |
|
信号量 |
|
读写信号量
|
|
原子操作
|
|
内存屏障 |
|
完成变量
|
|
大内核锁 |
|
seq锁
|
|
锁机制是一种广泛使用的同步技术,Linux内核中最常见的锁就是自旋锁(spin lock)。自旋锁被设计工作在多个处理器上(SMP),它只能被一个CPU上的一个进程(线程)所持有。它也可以工作在支持抢占的单处理器上。如果另一个进程或线程试图获取一个被持有的自旋锁,那么它就会在该锁上自旋(循环的执行一小段代码)直到该锁被释放。从这个意义上说,自旋锁是忙等待的,这就会特别浪费处理器的时间,因此自旋锁不应该被长时间持有。对于单处理器并且不可抢占的内核来说,自旋锁什么也不作。
需要强调的是,自旋锁别设计用于多处理器的同步机制,对于单处理器,内核在编译时不会引入自旋锁机制,对于可抢占的内核,它仅仅被用于设置内核的抢占机制是否开启的一个开关,也就是说加锁和解锁实际变成了禁止或开启内核抢占功能。如果内核不支持抢占,那么自旋锁根本就不会编译到内核中。
内核中使用spinlock_t类型来表示自旋锁,它定义在<linux/spinlock_types.h>:
typedef struct { raw_spinlock_t raw_lock; #if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP) unsigned int break_lock; #endif } spinlock_t; |
对于不支持SMP的内核来说,struct raw_spinlock_t什么也没有,是一个空结构。对于支持多处理器的内核来说,struct raw_spinlock_t定义为
typedef struct { unsigned int slock; } raw_spinlock_t; |
slock表示了自旋锁的状态,“1”表示自旋锁处于解锁状态(UNLOCK),“0”表示自旋锁处于上锁状态(LOCKED)。
break_lock表示当前是否由进程在等待自旋锁,显然,它只有在支持抢占的SMP内核上才起作用。
自旋锁的实现是一个复杂的过程,说它复杂不是因为需要多少代码或逻辑来实现它,其实它的实现代码很少。自旋锁的实现跟体系结构关系密切,核心代码基本也是由汇编语言写成,与体协结构相关的核心代码都放在相关的<asm/>目录下,比如<asm/spinlock.h>。对于我们驱动程序开发人员来说,我们没有必要了解这么spinlock的内部细节,如果你对它感兴趣,请参考阅读Linux内核源代码。对于我们驱动的spinlock接口,我们只需包括<linux/spinlock.h>头文件。在我们详细的介绍spinlock的API之前,我们先来看看自旋锁的一个基本使用格式:
#include <linux/spinlock.h> spinlock_t lock = SPIN_LOCK_UNLOCKED;
spin_lock(&lock); .... spin_unlock(&lock); |
从使用上来说,spinlock的API还很简单的,一般我们会用的的API如下表,其实它们都是定义在<linux/spinlock.h>中的宏接口,真正的实现在<asm/spinlock.h>中
#include <linux/spinlock.h> SPIN_LOCK_UNLOCKED DEFINE_SPINLOCK spin_lock_init( spinlock_t *) spin_lock(spinlock_t *) spin_unlock(spinlock_t *) spin_lock_irq(spinlock_t *) spin_unlock_irq(spinlock_t *) spin_lock_irqsace(spinlock_t *,unsigned long flags) spin_unlock_irqsace(spinlock_t *, unsigned long flags) spin_trylock(spinlock_t *) spin_is_locked(spinlock_t *) |
spinlock有两种初始化形式,一种是静态初始化,一种是动态初始化。对于静态的spinlock对象,我们用 SPIN_LOCK_UNLOCKED来初始化,它是一个宏。当然,我们也可以把声明spinlock和初始化它放在一起做,这就是 DEFINE_SPINLOCK宏的工作,因此,下面的两行代码是等价的。
DEFINE_SPINLOCK (lock); spinlock_t lock = SPIN_LOCK_UNLOCKED; |
spin_lock_init 函数一般用来初始化动态创建的spinlock_t对象,它的参数是一个指向spinlock_t对象的指针。当然,它也可以初始化一个静态的没有初始化的spinlock_t对象。
spinlock_t *lock ...... spin_lock_init(lock); |
内核提供了三个函数用于获取一个自旋锁。
spin_lock:获取指定的自旋锁。
spin_lock_irq:禁止本地中断并获取自旋锁。
spin_lock_irqsace:保存本地中断状态,禁止本地中断并获取自旋锁,返回本地中断状态。
自旋锁是可以使用在中断处理程序中的,这时需要使用具有关闭本地中断功能的函数,我们推荐使用 spin_lock_irqsave,因为它会保存加锁前的中断标志,这样就会正确恢复解锁时的中断标志。如果spin_lock_irq在加锁时中断是关闭的,那么在解锁时就会错误的开启中断。
另外两个同自旋锁获取相关的函数是:
spin_trylock():尝试获取自旋锁,如果获取失败则立即返回非0值,否则返回0。
spin_is_locked():判断指定的自旋锁是否已经被获取了。如果是则返回非0,否则,返回0。
同获取锁相对应,内核提供了三个相对的函数来释放自旋锁。
spin_unlock:释放指定的自旋锁。
spin_unlock_irq:释放自旋锁并激活本地中断。
spin_unlock_irqsave:释放自旋锁,并恢复保存的本地中断状态。
如果临界区保护的数据是可读可写的,那么只要没有写操作,对于读是可以支持并发操作的。对于这种只要求写操作是互斥的需求,如果还是使用自旋锁显然是无法满足这个要求(对于读操作实在是太浪费了)。为此内核提供了另一种锁-读写自旋锁,读自旋锁也叫共享自旋锁,写自旋锁也叫排他自旋锁。
读写自旋锁的使用也普通自旋锁的使用很类似,首先要初始化读写自旋锁对象:
// 静态初始化 rwlock_t rwlock = RW_LOCK_UNLOCKED; //动态初始化 rwlock_t *rwlock; ... rw_lock_init(rwlock); |
在读操作代码里对共享数据获取读自旋锁:
read_lock(&rwlock); ... read_unlock(&rwlock); |
在写操作代码里为共享数据获取写自旋锁:
write_lock(&rwlock); ... write_unlock(&rwlock); |
需要注意的是,如果有大量的写操作,会使写操作自旋在写自旋锁上而处于写饥饿状态(等待读自旋锁的全部释放),因为读自旋锁会自由的获取读自旋锁。
读写自旋锁的函数类似于普通自旋锁,这里就不一一介绍了,我们把它列在下面的表中。
RW_LOCK_UNLOCKED rw_lock_init(rwlock_t *) read_lock(rwlock_t *) read_unlock(rwlock_t *) read_lock_irq(rwlock_t *) read_unlock_irq(rwlock_t *) read_lock_irqsave(rwlock_t *, unsigned long) read_unlock_irqsave(rwlock_t *, unsigned long) write_lock(rwlock_t *) write_unlock(rwlock_t *) write_lock_irq(rwlock_t *) write_unlock_irq(rwlock_t *) write_lock_irqsave(rwlock_t *, unsigned long) write_unlock_irqsave(rwlock_t *, unsigned long) rw_is_locked(rwlock_t *) |
信号量,或旗标,就是我们在操作系统里学习的经典的P/V原语操作。
P:如果信号量值大于0,则递减信号量的值,程序继续执行,否则,睡眠等待信号量大于0。
V:递增信号量的值,如果递增的信号量的值大于0,则唤醒等待的进程。
信号量的值确定了同时可以有多少个进程可以同时进入临界区,如果信号量的初始值始1,这信号量就是互斥信号量(MUTEX)。对于大于1的非0值信号量,也可称为计数信号量(counting semaphore)。对于一般的驱动程序使用的信号量都是互斥信号量。