全部博文(668)
分类:
2008-05-31 09:35:08
在这篇文章中,我将会介绍 Kernel 提供用来使用 spinlock 的 function。除此之外,我还会告诉各位,为何在 SMP 的环境里,使用 spinlock 会比将所有 CPU 的中断 disable 这个方法来的有效率,我也会告诉各位如何针对不同的使用需求,使 spinlock 的 cost 再降低,进而使系统的效能更好...
前言
在 Linux Kernel 里有着许多重要的资料结构,这些资料在作业系统的运作中扮演着举足轻重的角色。然而,Linux 是个多任务的作业系统,也就是在同一时间里可以同时有许多的行程在执行,所以,很有可能某个行程在依序读取 inode list,同时却又有另一个在 inode list 里加入新的 inode,这会造成什幺情形呢?这会造成 inode list 的不稳定。所以,在 Kernel 里,我们需要一个机制,可以使得当我们在修改某个重要的资料结构时,不能被中断,即使被中断了,这个资料结构由于还没修改完,别的行程也都不能去读取和修改它。Linux Kernel提供了 spinlock 这个机制可以使我们做到这样的功能。
有的人会想到当我们在修改某个重要的资料结构时,将中断都 disable 掉就好了,等修改完了再将中断 enable 不就得了,何必还要再提供一个 spinlock 来做同样的事。在 uni-processor 的环境底下,的确是如此。所谓 uni-processor 就是指只有一个 CPU 的电脑,但是在SMP的环境下就不是这幺一回事了。
我们知道现在 Linux 已经有支持 SMP,也就是可以使用多颗 CPU 来加快系统的速度,如果当我们在修改重要的资料结构时,将执行修改工作的 CPU 中断 disable 掉的话,只有目前的这个 CPU 的执行不会被中断,在 SMP 环境下,还有别的 CPU 正同时运作,如果别的 CPU 也去修改这个资料结构的话,就会造成同时有两个 CPU 在修改它,不稳定性就会产生。解决方法是将全部的 CPU 中断都 disable 掉,等修改完之后,再全部都 enable 起来。但是这样的做法其 cost 会很大,整个系统的效能会 down 下来。因此,Linux Kernel 才会提供 spinlock 这样的机制,它不会将全部 CPU 的中断 disable 掉,所以效率比上述的方法好,但同时却又能确保资料的稳定性,不会有某个行程在修改它,另外又有一个行程在读取或修改它的情形发生。
在这篇文章中,我将会介绍 Kernel 提供用来使用 spinlock 的 function。除此之外,我还会告诉各位,为何在 SMP 的环境里,使用 spinlock 会比将所有 CPU 的中断 disable 这个方法来的有效率,我也会告诉各位如何针对不同的使用需求,使 spinlock 的 cost 再降低,进而使系统的效能更好。
spinlock的资料结构
spinlock 的资料结构在 Linux底下是以 spinlock_t 来表示的,在 SMP 和 UP 环境底下两者的栏位有一些差异,其实在 UP 底下 spinlock_t 可以说是一个空的结构,空就是空的,为何要说"可以说是空的"呢?这是因为 gcc 版本的问题,gcc 在 2.8 版以前结构的内容必须不能是空的,而在 2.8 版之后就可以,所以在 UP 环境底下,会根据 gcc 的版本而设定不同的 spinlock_t 结构栏位,但基本上,在 UP 环境底下,是根本不会用到 spinlock_t 结构里的栏位的,详情请见以下诸节即可了解。
由于 spinlock 主要是用在SMP的环境底下,所以,以下我们就只针对在SMP环境底下的 spinlock_t 结构来讨论,它的结构内容是这样子的:
typedef struct { volatile unsigned int lock; } spinlock t; |
使用 spinlock
spinlock t xxx lock = SPIN_LOCK_UNLOCKED; unsigned long flags; spin lock irqsave (&xxx lock, flags) ...critical section... spin unlock irqrestore (&xxx lock, flags) |
在 这个档案里定义了 spin_lock_irqsave() 及 spin_lock_irqrestore() 这两个 function。
#define spin_lock_irqsave(lock,flags) do while (0) #define spin_unlock_irqrestore(lock,flags) do while (0) |
#define local_irq_save(x) __asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x): /* no input */ :"memory") #define local_irq_restore(x) __asm__ __volatile__("pushl %0 ; popfl" /* no output */ :"g" (x):"memory") |
#ifdef __SMP__ #include #else /* !SMP */ ....... #endif |
UP 环境下的 Implementation
我们先来看看在 UP 的环境下, spin_lock(lock) 会变成什幺样子。
#define spin_lock(x) (void)lock #define spin_unlock(x) do {} while(0) |
spinlock_t xxx_lock = SPIN_LOCK_UNLOCKED; unsigned long flags; local_save_flags(flags); cli(); ... critical section ... local_restore_flags(flags); |
SMP 环境下的 Implementation
在 SMP 的环境底下, spin_lock() 和 spin_unlock() 这两个函式的原始码是放在 中。
extern inline void spin_lock(spinlock_t *plock) { __asm__ __volatile__( spin_lock_string :"=m" (__dummy_lock(plock))); } |
extern inline void spin_lock(spinlock_t *plock) { 1: lock ; btsl ,plock; jc 2f; .section .text.lock,"ax" 2: testb ,plock; rep;nop; jne 2b; jmp 1b; .previous } |
#define SPIN_LOCK_UNLOCKED (spinlock_t) |
看完了 spin_lock(),再来看 spin_unlock() 就会发觉简单多了。
#define spin_unlock(lock) __asm__ __volatile__( spin_unlock_string :"=m" (__dummy_lock(lock))) |
spin_unlock(plock) { lock; btrl , plock; } |
看到这里,各位应该可以了解 spinlock 的运作方式及其基本的使用方法了,接下来,我要跟各位介绍 spinlock 的另一种小小的变型,叫 read-write spinlock。
第二种的使用方式
有些资料结构是这样子的,我们希望有人在修改它的内容时,别人都不能读取或修改它,但是当没有人在修改它时,可以同时有很多人去读取它的内容。我们称这样的 spinlock 为 read-write spinlock。 Kernel 为它定义了 rwlock_t,放在 里。使用方式是这样子的。
rwlock_t xxx_lock = RW_LOCK_UNLOCKED; unsigned long flags; read_lock_irqsave(&xxx_lock, flags); ... critical section that only reads the info ... read_unlock_irqrestore(&xxx_lock, flags); write_lock_irqsave(&xxx_lock, flags); ... read and write exclusive access to the info ... write_unlock_irqrestore(&xxx_lock, flags); |
我们来看看read这组函式的原始码是怎幺样子的:
#define read_lock_irqsave(lock, flags) do { local_irq_save(flags); read_lock(lock); } while (0) #define read_unlock_irqrestore(lock, flags) do { read_unlock(lock); local_irq_restore(flags); } while (0) |
我们再来看看 read_lock() 与 read_unlock() 这两个函式,在 UP 环境底下是这个样子的:
#define read_lock(lock) (void)(lock) /* Not "unused variable". */ #define read_unlock(lock) do while(0) |
至于 SMP 底下 rwlock 的实作方式我就不再赘述,基本上它们的实作方式都是差不多的,只有一点要特别说的是,由于 rwlock 可以容许多个 reader,但却只能有一个 writer,所以,它不会只用到 rwlock_t.lock 的第 0 个 bit 而已。事实上,rwlock_t.lock 是个 32bit 的 unsigned int 型别的变数,因此,它用第 0 到 30 个 bit 当作 reader 的 counter,而第 31 个 bit 则是用来给 writer 使用的。当第 31 个 bit 为 1 时,表示目前 rwlock 被 writer 锁住,此时前 30 个 bit 都应该是 0,表示此时没有任何的 reader。因此,可以推断 rwlock 同一时间最多可以有 2 的 30 次方个 reader。
第三种使用 spinlock 的方式
我们可以看到以上两种的使用机制都是以 disable 中断的方式来做的,虽然 disable 中断很简单,只要一个指令就行了,但事实上,这个指令的 cost 对 CPU 来讲是蛮大的。所以, Kernel 还提供另一组的函式,它不 disable 中断,所以,它的执行速度会比上面两种来得有效率一些。但是,上帝是公平的,它让你速度快,相对的它也提供的某些限制。这个限制就是就如果你确定 interrupt handler 不会用到这个受保护的资料结构时,那你就可以考虑用这一组的函式, 以加快程序的执行。其实,这一组函式我们已经在上面见过了。
spin_lock(&lock); ... spin_unlock(&lock); |
spin_lock(&lock); .... <------ interrupt spin_lock(&lock); ... spin_unlock(&lock) |
混合使用
针对 rwlock_t 这组函式除了上面提到的用法外,事实上,还是可以混合 spin_lock() 及 spin_unlock() 来使用的。由于 rwlock_t 可以允许多个 reader,所以如果在 interrupt handler 中只会读取受保护的资料结构,而不会去修改它的话,那我们可以使用 spin_lock() 这组函式,但是当行程要修改资料结构时,还是得呼叫 write_spin_lock() 及 write_spin_unlock() 这组的函式。这样既可以增加执行的效率,又可以确保重要资料结构的稳定性了。
结论
虽然在 UP 的环境中,保护重要的资料结构只要呼叫 cli() 和 sti() 就好,但是随着 SMP 技术的成熟,相信 SMP 系统会逐渐的增加,为了让自己写的程序具有可移植性,善用 spinlock 这项机制相信会为未来省下相当修改程序码的功夫。