您的位置:首页 > 运维架构 > Linux

Linux驱动程序开发005 - 内核同步技术

2012-04-10 09:27 211 查看
序言

就像我们在操作系统里学习的那样,如果多个程序(进程或线程)同时访问临界区数据就会发生竞争。存在竞争条件的程序会产生不可预料的结果。消除竞争的方法一般就是同步的访问临界区数据(原子访问)。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 *)
信号量(semaphore)

信号量,或旗标,就是我们在操作系统里学习的经典的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系统

自旋锁主要是应用于SMP(CONFIG_SMP)的系统上的,为了能使代码工作在非SMP系统上,请慎重选择使用的自旋锁函数。

睡眠

信号量是睡眠的而自旋锁是非睡眠的,因此在不能睡眠的条件下请使用自旋锁(如中断上下文)。如果持锁期间需要睡眠请使用信号量

持锁时间

由于自旋锁是忙等待,很浪费CPU时间,因此如果是短期持锁可以使用自旋锁。如果是长期持锁请使用信号量。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: