http://blog.sina.com.cn/s/blog_4cc4ab450100afe9.html
序言
就像我们在操作系统里学习的那样,如果多个程序(进程或线程)同时访问临界区数据就会发生竞争。存在竞争条件的程序会产生不可预料的结果。消除竞争的方法一般就是同步的访问临界区数据(原子访问)。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)。对于一般的驱动程序使用的信号量都是互斥信号量。
类似于自旋锁,信号量的实现也与体系结构密切相关,具体的实现定义在<asm/semaphore.h>头文件中,对于x86_32系统来说,它的定义如下:
struct semaphore { atomic_t count; int sleepers; wait_queue_head_t wait; }; |
信号量的初始值count是atomic_t类型的,这是一个原子操作类型,它也是一个内核同步技术,可见信号量是基于原子操作的。我们会在后面原子操作部分对原子操作做详细介绍。
信号量的使用类似于自旋锁,包括创建、获取和释放。我们还是来先展示信号量的基本使用形式:
static DECLARE_MUTEX(my_sem); ......
if (down_interruptible(&my_sem))
{ return -ERESTARTSYS; } ...... up(&my_sem) |
Linux内核中的信号量函数接口如下:
static DECLARE_SEMAPHORE_GENERIC(name, count); static DECLARE_MUTEX(name); seam_init(struct semaphore *, int); init_MUTEX(struct semaphore *); init_MUTEX_LOCKED(struct semaphore *) down_interruptible(struct semaphore *); down(struct semaphore *) down_trylock(struct semaphore *) up(struct semaphore *) |
信号量的初始化包括静态初始化和动态初始化。静态初始化用于静态的声明并初始化信号量。
static DECLARE_SEMAPHORE_GENERIC(name, count); static DECLARE_MUTEX(name); |
对于动态声明或创建的信号量,可以使用如下函数进行初始化:
seam_init(sem, count); init_MUTEX(sem); init_MUTEX_LOCKED(struct semaphore *) |
显然,带有MUTEX的函数始初始化互斥信号量。LOCKED则初始化信号量为锁状态。
信号量初始化完成后我们就可以使用它了
down_interruptible(struct semaphore *); down(struct semaphore *) down_trylock(struct semaphore *) up(struct semaphore *) |
down函数会尝试获取指定的信号量,如果信号量已经被使用了,则进程进入不可中断的睡眠状态。down_interruptible则会使进程进入可中断的睡眠状态。关于进程状态的详细细节,我们在内核的进程管理里在做详细介绍。
down_trylock尝试获取信号量, 如果获取成功则返回0,失败则会立即返回非0。
当退出临界区时使用up函数释放信号量,如果信号量上的睡眠队列不为空,则唤醒其中一个等待进程。
类似于自旋锁,信号量也有读写信号量。读写信号量API定义在<linux/rwsem.h>头文件中,它的定义其实也是体系结构相关的,因此具体实现定义在<asm/rwsem.h>头文件中,以下是x86的例子:
struct rw_semaphore { signed long count; spinlock_t wait_lock; struct list_head wait_list; }; |
首先要说明的是所有的读写信号量都是互斥信号量。读锁是共享锁,就是同时允许多个读进程持有该信号量,但写锁是独占锁,同时只能有一个写锁持有该互斥信号量。显然,写锁是排他的,包括排斥读锁。由于写锁是共享锁,它允许多个读进程持有该锁,只要没有进程持有写锁,它就始终会成功持有该锁,因此这会造成写进程写饥饿状态。
在使用读写信号量前先要初始化,就像你所想到的,它在使用上几乎与读写自旋锁一致。先来看看读写信号量的创建和初始化:
// 静态初始化 static DECLARE_RWSEM(rwsem_name); // 动态初始化 static struct rw_semaphore rw_sem; init_rwsem(&rw_sem); |
读进程获取信号量保护临界区数据:
down_read(&rw_sem); ... up_read(&rw_sem); |
写进程获取信号量保护临界区数据:
down_write(&rw_sem); ... up_write(&rw_sem); |
更多的读写信号量API请参考下表:
#include <linux/rwsem.h> DECLARE_RWSET(name); init_rwsem(struct rw_semaphore *); void down_read(struct rw_semaphore *sem); void down_write(struct rw_semaphore *sem); void up_read(struct rw_semaphore *sem); int down_read_trylock(struct rw_semaphore *sem); int down_write_trylock(struct rw_semaphore *sem); void downgrade_write(struct rw_semaphore *sem); void up_write(struct rw_semaphore *sem); |
同自旋锁一样,down_read_trylock和down_write_trylock会尝试着获取信号量,如果获取成功则返回1,否则返回0。奇怪为什么返回值与信号量的对应函数相反,使用是一定要小心这点。
后记
由于文本大小的限制,这里只能介绍这些技术了。自旋锁和信号量都是常用的内核技术,下面我们总结一下自旋锁和信号量的特点,然后说明什么时候使用自旋锁,什么时候使用信号量。
自旋锁主要是应用于SMP(CONFIG_SMP)的系统上的,为了能使代码工作在非SMP系统上,请慎重选择使用的自旋锁函数。
信号量是睡眠的而自旋锁是非睡眠的,因此在不能睡眠的条件下请使用自旋锁(如中断上下文)。如果持锁期间需要睡眠请使用信号量
由于自旋锁是忙等待,很浪费CPU时间,因此如果是短期持锁可以使用自旋锁。如果是长期持锁请使用信号量。