以文本方式查看主题 - 曙海教育集团论坛 (http://peixun0.cn/bbs/index.asp) -- Linux驱动开发 (http://peixun0.cn/bbs/list.asp?boardid=33) ---- Linux驱动程序开发 - 内核同步技术 (http://peixun0.cn/bbs/dispbbs.asp?boardid=33&id=1704) |
||||||||||||||||||||||||||||||
-- 作者:wangxinxin -- 发布时间:2010-11-24 9:20:31 -- Linux驱动程序开发 - 内核同步技术 序言 就像我们在操作系统里学习的那样,如果多个程序(进程或线程)同时访问临界区数据就会发生竞争。存在竞争条件的程序会产生不可预料的结果。消除竞争的方法一般就是同步的访问临界区数据(原子访问)。Linux内核提供了多种技术用来实现内核同步操作。下面我们就分别介绍。 内核同步技术 Linux内核是多进程、多线程的操作系统,它提供了相当完整的内核同步方法。作为一个总结,我们先列出内核同步方法列表,这样我们可以从总体上对内核同步技术有个了解,然后我们这分别对每个同步技术做详细介绍。
锁机制是一种广泛使用的同步技术,Linux内核中最常见的锁就是自旋锁(spin lock)。自旋锁被设计工作在多个处理器上(SMP),它只能被一个CPU上的一个进程(线程)所持有。它也可以工作在支持抢占的单处理器上。如果另一个进程或线程试图获取一个被持有的自旋锁,那么它就会在该锁上自旋(循环的执行一小段代码)直到该锁被释放。从这个意义上说,自旋锁是忙等待的,这就会特别浪费处理器的时间,因此自旋锁不应该被长时间持有。对于单处理器并且不可抢占的内核来说,自旋锁什么也不作。
需要强调的是,自旋锁别设计用于多处理器的同步机制,对于单处理器,内核在编译时不会引入自旋锁机制,对于可抢占的内核,它仅仅被用于设置内核的抢占机制是否开启的一个开关,也就是说加锁和解锁实际变成了禁止或开启内核抢占功能。如果内核不支持抢占,那么自旋锁根本就不会编译到内核中。 内核中使用spinlock_t类型来表示自旋锁,它定义在<linux/spinlock_types.h>:
对于不支持SMP的内核来说,struct raw_spinlock_t什么也没有,是一个空结构。对于支持多处理器的内核来说,struct raw_spinlock_t定义为
slock表示了自旋锁的状态,“1”表示自旋锁处于解锁状态(UNLOCK),“0”表示自旋锁处于上锁状态(LOCKED)。
break_lock表示当前是否由进程在等待自旋锁,显然,它只有在支持抢占的SMP内核上才起作用。 自旋锁的实现是一个复杂的过程,说它复杂不是因为需要多少代码或逻辑来实现它,其实它的实现代码很少。自旋锁的实现跟体系结构关系密切,核心代码基本也是由汇编语言写成,与体协结构相关的核心代码都放在相关的<asm/>目录下,比如<asm/spinlock.h>。对于我们驱动程序开发人员来说,我们没有必要了解这么spinlock的内部细节,如果你对它感兴趣,请参考阅读Linux内核源代码。对于我们驱动的spinlock接口,我们只需包括<linux/spinlock.h>头文件。在我们详细的介绍spinlock的API之前,我们先来看看自旋锁的一个基本使用格式:
从使用上来说,spinlock的API还很简单的,一般我们会用的的API如下表,其实它们都是定义在<linux/spinlock.h>中的宏接口,真正的实现在<asm/spinlock.h>中
spinlock有两种初始化形式,一种是静态初始化,一种是动态初始化。对于静态的spinlock对象,我们用 SPIN_LOCK_UNLOCKED来初始化,它是一个宏。当然,我们也可以把声明spinlock和初始化它放在一起做,这就是 DEFINE_SPINLOCK宏的工作,因此,下面的两行代码是等价的。
spin_lock_init 函数一般用来初始化动态创建的spinlock_t对象,它的参数是一个指向spinlock_t对象的指针。当然,它也可以初始化一个静态的没有初始化的spinlock_t对象。
内核提供了三个函数用于获取一个自旋锁。
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:释放自旋锁,并恢复保存的本地中断状态。
如果临界区保护的数据是可读可写的,那么只要没有写操作,对于读是可以支持并发操作的。对于这种只要求写操作是互斥的需求,如果还是使用自旋锁显然是无法满足这个要求(对于读操作实在是太浪费了)。为此内核提供了另一种锁-读写自旋锁,读自旋锁也叫共享自旋锁,写自旋锁也叫排他自旋锁。
读写自旋锁的使用也普通自旋锁的使用很类似,首先要初始化读写自旋锁对象:
在读操作代码里对共享数据获取读自旋锁:
在写操作代码里为共享数据获取写自旋锁:
需要注意的是,如果有大量的写操作,会使写操作自旋在写自旋锁上而处于写饥饿状态(等待读自旋锁的全部释放),因为读自旋锁会自由的获取读自旋锁。
读写自旋锁的函数类似于普通自旋锁,这里就不一一介绍了,我们把它列在下面的表中。
信号量,或旗标,就是我们在操作系统里学习的经典的P/V原语操作。
P:如果信号量值大于0,则递减信号量的值,程序继续执行,否则,睡眠等待信号量大于0。 V:递增信号量的值,如果递增的信号量的值大于0,则唤醒等待的进程。 信号量的值确定了同时可以有多少个进程可以同时进入临界区,如果信号量的初始值始1,这信号量就是互斥信号量(MUTEX)。对于大于1的非0值信号量,也可称为计数信号量(counting semaphore)。对于一般的驱动程序使用的信号量都是互斥信号量。 |