- UID
- 1029342
- 性别
- 男
|
2.6.25内核实现了新的自旋锁,叫做ticket spin_lock,ticket就是排队的意思,就像看电影时拿着票有序入场一样,新的自旋锁不再是乱抢自旋锁了,而是有序地获得自旋锁,这样就消除了 一些不必要的混乱。这样做的初衷是什么呢?首先看看自旋锁的意义,它是一种十分高效的锁,得不到锁的时候不是睡眠而是自旋,自旋就是忙等,这样就免去了睡 眠-切换-唤醒-切换的开销,毕竟一个进程睡眠必然导致一次硬切换,而唤醒仍然需要一次硬切换。但是在忙等期间,cpu实际上没有做什么有意义的事情,这 是一点小小的弊端,但是任何事必须图一方面,和睡眠-切换-唤醒的开销比起来,这么做是十分有意义的。另外要明白的是,当前实现的自旋锁的争抢实体实际上 是cpu,而不是进程,在单cpu的非抢占内核上,自旋锁是不必要的,在单cpu的抢占内核,自旋锁实际就是禁用了抢占而已,只有在多cpu的环境下,自 旋锁作为锁的意义才真正存在,所以我们说是cpu获得了自旋锁而不是别的。要了解新自旋锁的意义还要了解老自旋锁的实现。老的自旋锁是一个整数,在cpu 企图获得锁的时候要将此数减一,然后看看是否为0,如果为0,那么就获得了锁,如果为负数就说明别的cpu比此cpu先获得了锁,然后此cpu进入忙等, 等待占有锁的cpu释放锁从而将锁置为1,然后它再次将其减一后与0比较。这么实现可以看出,一个cpu企图获得锁时所探测到的锁的值是负数并且绝对值越 大,那么就有越多的cpu在争抢这个自旋锁,这看起来没有问题,就像踢足球一样,那么多人抢一个球,但是足球有个人技术,团队合作在里面,有输有赢的结 果,而自旋锁就不一样了,所有的cpu要公平竞争,没有团队,没有个人技术,但是这个实现真的就很公平吗?如果不考虑硬件告诉缓存,那么它可能还是比较公 平的,但是几乎每个cpu都有硬件高速缓存,如果一个cpu得到了自旋锁那么信息就会被读入该cpu的缓存行,后来它释放了这个自旋锁,但是马上它又去争 抢同一个锁,这样这个cpu对于别的cpu就是不公平的,它再次得到自旋锁的可能性要比别的cpu要大,因此,老的自旋锁实现方案看似公平实际不公平,按 道理应该让等待时间最长的cpu获得自旋锁。要得到这样的效果,改进点已经很明确了,就是增加一个队列,这个队列的元素按照等待时间排队,数据结构也很简 单,增加一个list_head字段即可,如果这么实现当然比较好,比较规则,比较规范,但是不是那么艺术和高效,于是新的ticket自旋锁由运而生。
以上从理论角度阐述了老的自旋锁的实现,下面看一下具体的代码,主要是纯粹的获得锁和释放锁的代码,不再讨论开关抢占的内容:
- #define spin_lock_string \
"\n1:\t" \
"lock ; decb %0\n\t" \ //参数0实际上就是lock->slock,这里减1
"jns 3f\n" \ //若为0则结束,成功获得锁。
"2:\t" \ //否则,自旋忙等
"rep;nop\n\t" \
"cmpb $0,%0\n\t" \
"jle 2b\n\t" \
"jmp 1b\n" \
"3:\n\t"
#define spin_unlock_string \
"movb $1,%0" \ //将lock->slock的值设置为1
- :"=m" (lock->slock) : : "memory"
那 么ticket自旋锁是如何实现的呢?它实际上是一个16位的数,被分为高8位和低8位,高8位代表的是争抢者的顺序号,也就是说高8位从小到大的顺序正 是争抢者获得锁的顺序,低8位代表拥有者的信息。具体过程就是,一个cpu请求自旋锁的时候,首先递加自旋锁的高8位存入一个变量,然后比较是否和低8位 相等,若不等则忙等,忙等中不断比较先前存入的变量和自旋锁的低8位是否相等,一旦相等,则代表成功获得锁,结束争抢过程。注意,只有在一个拥有锁的 cpu释放锁的时候才会导致自旋锁的低8位加1。说了等于白说,还是看代码实现吧:
- static inline void __raw_spin_lock(__raw_spinlock_t *lock)
- {
short inc = 0x0100; - __asm__ __volatile__ (
- LOCK_PREFIX "xaddw %w0, %1\n"
//高8位加1,交换两参数,效果就是lock的高8位加1
"1:\t"
"cmpb %h0, %b0\n\t"
//比较高8位和低8位
"je 2f\n\t"
//相等就结束,成功获得锁
"rep ; nop\n\t"
//否则忙等
"movb %1, %b0\n\t"
/* don't need lfence here, because loads are in-order */
"jmp 1b\n"
"2:"
- :"+Q" (inc), "+m" (lock->slock)
- :
- :"memory", "cc");
- }
static inline void __raw_spin_unlock(__raw_spinlock_t *lock) - {
- __asm__ __volatile__(
- UNLOCK_LOCK_PREFIX "incb %0"
//我们看到自旋锁的低8位递增了1
- :"+m" (lock->slock)
- :
- :"memory", "cc");
- }
linux内核的文档上对于ticket自旋锁的解释可没有我这里这么随便,还给出图了呢?他们把自旋锁的高8位叫做next,而低8位叫做了owner,想领略一下大师的文笔就请访问:http://lwn.net/Articles/267968/
ticket自旋锁的实现是如此之简单使得你我都叹为观止,其实是个学过点数据结构和算法的都会想到使用队列来实现排队的自旋锁,却没想到linux用一 个16位的数就搞定了一切,我们难免有一些惭愧,还好,有一位老大和我们这些俗人是属于一队的,那就是微软,windows下的排队自旋锁就是用队列实现 的,而且实现极其复杂,再怎么说,他和我们这些俗人的想法一致,我们也就没有什么好惭愧的了。队列实现有队列实现的好处,比如便于扩展,而用16位的数实 现的排队自旋锁难免让人觉得有耍小聪明的味道,可是这就是linux,无数的小聪明积累而成的大师级的作品。下面就谈谈windows下的排队自旋锁的实 现,它的实现比linux早,但是linux在任何方面都是不甘落后的。
windows的每个cpu上都有一个标志,通过该标志,这些标志就是排队自旋锁的排队实体,当一个cpu要一个自旋锁时,就将此cpu的标志排队,系统 中每个自旋锁拥有一个队列,此cpu的标志要排入的队列就是它争抢的自旋锁的队列,注意是排入队尾,当有cpu释放了自旋锁的时候,下一个获得锁的cpu 就是释放锁的cpu标志所在的位置后面的第一个cpu,那么释放者是怎么把拥有权交给下一个等待者的呢?其实很简单,排队的不是一个标志吗?释放者只需要 将下一个cpu的标志设置为一个大家商量好的标志,该标志表明等待者可以拥有锁了,然后队列中的所有cpu都在其自身的标志上忙等就可以了,一旦检测到标 志代表它可以拥有锁了,那么忙等就结束了。这个实现有一个很大的优点就是忙等的点并不是全局的,而是局部的本cpu的一个标志,因此同步的开销就很小了, 而不像linux的实现,动不动就来个lock前缀用于锁总线。
另外linux的排队自旋锁实现还有一个弊端就是只能最多有2的8次方即256个cpu等待自旋锁,linux的解决办法就是提供补丁,该补丁我还没有见 过,因此不再讨论,而windows的实现方式却非常容易被扩展,毕竟对于排队相关的概念,队列是一种标准的实现方式。
上面阐述了排队自旋锁的相关实现,不管是linux还是windows,其引入的目的就是使系统更加高效,更加公平,当然还有一个很少有人在意的原因就是 为了更加有序,因为有序的东西是便于管理的,而一切错误的根源就是混乱和无序,操作系统内核的目的就是管理,它是以管理员身份存在的,因此有序也就成了它 的一个要求,内核很不喜欢无序的东西,因此才有了优先级,队列的概念。 |
|