现在的位置: 首页 > 综合 > 正文

自旋锁及其衍生的锁,值得看看

2013年08月06日 ⁄ 综合 ⁄ 共 11635字 ⁄ 字号 评论关闭

自旋锁

自旋锁(spinlock)是用在多个CPU系统中的锁机制,当一个CPU正访问自旋锁保护的临界区时,临界区将被锁上,其他需要访问此临界区的CPU只能忙等待,直到前面的CPU已访问完临界区,将临界区开锁。自旋锁上锁后让等待线程进行忙等待而不是睡眠阻塞,而信号量是让等待线程睡眠阻塞。自旋锁的忙等待浪费了处理器的时间,但时间通常很短,在1毫秒以下。

自旋锁用于多个CPU系统中,在单处理器系统中,自旋锁不起锁的作用,只是禁止或启用内核抢占。在自旋锁忙等待期间,内核抢占机制还是有效的,等待自旋锁释放的线程可能被更高优先级的线程抢占CPU。

自旋锁基于共享变量。一个线程通过给共享变量设置一个值来获取锁,其他等待线程查询共享变量是否为0来确定锁现是否可用,然后在忙等待的循环中"自旋"直到锁可用为止。

通用自旋锁

自旋锁的状态值为1表示解锁状态,说明有1个资源可用;0或负值表示加锁状态,0说明可用资源数为0。Linux内核为通用自旋锁提供了API函数初始化、测试和设置自旋锁。API函数功能说明如表5。

表5 通用自旋锁API函数功能说明

宏定义 功能说明
spin_lock_init(lock) 初始化自旋锁,将自旋锁设置为1,表示有一个资源可用。
spin_is_locked(lock) 如果自旋锁被置为1(未锁),返回0,否则返回1。
spin_unlock_wait(lock) 等待直到自旋锁解锁(为1),返回0;否则返回1。
spin_trylock(lock) 尝试锁上自旋锁(置0),如果原来锁的值为1,返回1,否则返回0。
spin_lock(lock) 循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。
spin_unlock(lock) 将自旋锁解锁(置为1)。
spin_lock_irqsave(lock, flags) 循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。关中断,将状态寄存器值存入flags。
spin_unlock_irqrestore(lock, flags) 将自旋锁解锁(置为1)。开中断,将状态寄存器值从flags存入状态寄存器。
spin_lock_irq(lock) 循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。关中断。
spin_unlock_irq(lock) 将自旋锁解锁(置为1)。开中断。
spin_unlock_bh(lock) 将自旋锁解锁(置为1)。开启底半部的执行。
spin_lock_bh(lock) 循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。阻止软中断的底半部的执行。

下面用一个使用自旋锁锁住链表的样例,代码列出如下(在arch/x386/mm/pgtable.c中):

spinlock_t pgd_lock = SPIN_LOCK_UNLOCKED; //锁初始化 void pgd_dtor(void *pgd, kmem_cache_t *cache,unsigned long unused) { unsigned long flags; //能从中断上下文中被调用   spin_lock_irqsave(&pgd_lock, flags);//加锁 pgd_list_del(pgd); spin_unlock_irqrestore(&pgd_lock,flags);//解锁 }

自旋锁用结构spinlock_t描述,在include/linux/spinlock.h中有类型 spinlock_t定义,列出如下:

typedef struct { raw_spinlock_t raw_lock; #ifdef CONFIG_GENERIC_LOCKBREAK /*引入另一个自旋锁*/unsigned int break_lock; #endif #ifdef CONFIG_DEBUG_SPINLOCK /*用于调试自旋锁*/ unsigned int magic,owner_cpu; void *owner; #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map; /*映射lock实例到lock-class对象 #endif } spinlock_t;

由于自旋锁的性能严重地影响着操作系统的性能,Linux内核提供了Lock-class和Lockdep跟踪自旋锁的使用对象和锁的状态,并可从/proc文件系统查询自旋锁的状态信息。自旋锁的调试通过配置项CONFIG_DEBUG_*项打开。

对于对称多处理器系统(SMP),slock为一个int数据类型,对于单个处理器系统,slock定义为空。SMP的slock定义列出如下(在include/linux/spinlock_types.h):

typedef struct { volatile unsigned int slock; } raw_spinlock_t;

自旋锁的实现机制类型,下面仅分析自旋锁API函数spin_lock_init、spin_lock_irqsave和spin_unlock_irqrestore。

(1)spin_lock_init

函数spin_lock_init将自旋锁状态值设置为1,表示未锁状态。其列出如下(在include/linux/spinlock.h中):

# define spin_lock_init(lock)	 / do { *(lock) = SPIN_LOCK_UNLOCKED; } while (0)

宏__SPIN_LOCK_UNLOCKED列出如下(在include/linux/spinlock_types.h中):

# define __SPIN_LOCK_UNLOCKED(lockname) / (spinlock_t)	{	.raw_lock = __RAW_SPIN_LOCK_UNLOCKED,	/ SPIN_DEP_MAP_INIT(lockname) }   #define __RAW_SPIN_LOCK_UNLOCKED	{ 1 }

(2)函数spin_lock_irqsave

  函数spin_lock_irqsave等待直到自旋锁解锁,即自旋锁值为1,它还关闭本地处理器上的中断。其列出如下(在include/linux/spinlock.h中):

#define spin_lock_irqsave(lock, flags)	flags = _spin_lock_irqsave(lock)

函数spin_lock_irqsave分析如下(在kernel/spinlock.c中):

unsigned long __lockfunc _spin_lock_irqsave(spinlock_t *lock) { unsigned long flags;   local_irq_save(flags); //将状态寄存器的值写入flags保存 preempt_disable(); //关闭内核抢占,内核抢占锁加1 spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);   #ifdef CONFIG_LOCKDEP LOCK_CONTENDED(lock,_raw_spin_trylock, _raw_spin_lock); #else _raw_spin_lock_flags(lock, &flags); #endif return flags;}

宏定义local_irq_save保存了状态寄存器的内容到x中,同时关中断。这个宏定义列出如下:

#define local_irq_save(x)	__asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x): /* no input */ :"memory")

上述语句中,指令pushfl将当前处理器的状态寄存器的内容压入堆栈保护。指令popl %0 将状态寄存器的内容存入x中,其中%0这里指x。

函数_raw_spin_lock_flags空操作等待直到自旋锁的值为1,表示有资源可用,就跳出循环等待,准备执行本函数后面的操作。其列出如下:

# define _raw_spin_lock_flags(lock, flags) / __raw_spin_lock_flags(&(lock)->raw_lock, *(flags�

函数__raw_spin_lock_flags列出如下(在include/asm-x86/spinlock.h中):

#define __raw_spin_lock_flags(lock, flags) __raw_spin_lock(lock) static __always_inline void__raw_spin_lock(raw_spinlock_t *lock) { int inc = 0x00010000; int tmp; /*指令前缀lock用来锁住内存控制器,不让其他处理器访问,保证指令执行的原子性*/ asm volatile("lock ; xaddl %0, %1/n" // lock->slock=lock->slock+inc "movzwl %w0, %2/n/t" //tmp=inc "shrl $16, %0/n/t" //inc >> 16 后,inc=1"1:/t" "cmpl %0, %2/n/t" //比较inc与lock->slock "je 2f/n/t" //如果inc与lock->slock相等,跳转到2"rep ; nop/n/t" //空操作 "movzwl %1, %2/n/t" //tmp=lock->slock /* 这里不需要读内存屏障指令lfence,因为装载是排序的*/ "jmp 1b/n" //跳转到1 "2:" : "+Q" (inc), "+m" (lock->slock), "=r" (tmp) : :"memory", "cc"); }

(3)函数spin_unlock_irqrestore

宏定义spin_unlock_irqrestore是解锁,开中断,并把flags值存入到状态寄存器中,这个宏定义分析如下:

#define spin_unlock_irqrestore(lock, flags)	_spin_unlock_irqrestore(lock, flags)

函数_spin_unlock_irqrestore列出如下(在kernel/spinlock.c中):

void __lockfunc _spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) {spin_release(&lock->dep_map, 1, _RET_IP_); _raw_spin_unlock(lock); //解锁 local_irq_restore(flags);//开中断,将flag的值存入状态寄存器 preempt_enable(); //开启内核抢占 }   # define _raw_spin_unlock(lock)	 __raw_spin_unlock(&(lock)->raw_lock)

函数__raw_spin_unlock将自旋锁状态值加1,表示有1个资源可用,从而释放自旋锁,其列出如下(在include/asm-x86/spinlock.h中):

static __always_inline void __raw_spin_unlock(raw_spinlock_t *lock) { asmvolatile(UNLOCK_LOCK_PREFIX "incw %0" // lock->slock= lock->slock +1 : "+m" (lock->slock) : :"memory", "cc"); }

读/写自旋锁

"读/写自旋锁"用来解决读者/写者问题。如果有多个线程(进程、中断处理程序、底半部例程)以只读的方式访问一个临界区数据,读/写自旋锁允许多个线程同时读取数据。如果一个线程需要对临界区数据进行写操作,它必须获取写锁,只有在没有读者或写者进行操作时,写者才独占临界区数据进行写操作。读操作时需要获取读锁,写操作时需要获取写锁。

Linux内核为读/写自旋锁提供了操作API函数初始化、测试和设置自旋锁。API函数功能说明如表5。

表5 读/写自旋锁API函数功能说明

宏定义 功能说明
rwlock_init(lock) 初始化自旋锁值为0x01000000(未锁)。
read_lock(lock) 加读者锁,即将读者计数加1。
read_lock_irqsave(lock, flags) 加读者锁,即将读者计数加1。并且关中断,存储状态标识到flags中。
read_lock_irq(lock) 加读者锁,即将读者计数加1。并且关中断。
read_unlock(lock) 解读者锁,即将读者计数减1。
read_unlock_irqrestore(lock, flags) 解读者锁,即将读者计数减1。并且开中断,将状态标识从flags读到状态寄存器中。
read_unlock_irq(lock) 解读者锁,即将读者计数减1。并且开中断。
write_lock(lock) 加写者锁,即将写者锁置0。
write_lock_irqrestore(lock, flags) 加写者锁,即将写者锁置0。并且关中断,存储状态标识到flags中。
write_lock_irq(lock) 加写者锁,即将写者锁置0。并且关中断。
write_unlock(lock) 解写者锁,即将写者锁置1。
write_unlock_irqrestore(lock, flags) 解写者锁,即将写者锁置1。并且开中断,将状态标识从flags读到状态寄存器中。
write_unlock_irq(lock) 解写者锁,即将写者锁置1。并且开中断。

用户使用读/写自旋锁,应先自旋锁的状态值初始化为锁初始化为RW_LOCK_BIAS,即0x01000000,表示为未锁状态。

读/写自旋锁用结构rwlock_t描述,它的主要成员为锁状态值变量lock,结构rwlock_t列出如下(在include/linux/spinlock_types.h中):

typedef struct { raw_rwlock_t raw_lock; …… } rwlock_t;   typedef struct { unsigned int lock; }raw_rwlock_t;

在结构raw_rwlock_t中,读/写自旋锁状态变量lock为32位,它分为2个部分,0~23位是一个24位计数器,表示对临界数据区进行并发读操作的线程数,线程数以补码形式存入计数器;第24位为表示"未锁"的状态位,在没有线程读或写临界区时,设置为1,否则,设置为0。

如果自旋锁设置了"未锁"状态且无读者,那么lock值为0x01000000;如果写者已获得自旋锁且无读者,则未锁状态位清0,lock值为0x00000000。如果有一个、2个或多个线程获取锁对临界数据区进行读操作,则lock值为0x00ffffff、0x00fffffe等(第24位清0表示未锁,第0~23位为读者个数的补码)。

下面说明读/写自旋锁API函数的实现:

(1)函数rwlock_init

函数rwlock_init将读/写自旋锁状态值设为0x01000000,其列出如下(在include/linux/spinlock.h中):

# define rwlock_init(lock)	 / do { *(lock) = RW_LOCK_UNLOCKED; } while (0)   #define RW_LOCK_UNLOCKED	__RW_LOCK_UNLOCKED(old_style_rw_init) #define __RW_LOCK_UNLOCKED(lockname) / (rwlock_t)	{	.raw_lock = __RAW_RW_LOCK_UNLOCKED,	/ RW_DEP_MAP_INIT(lockname) }   #define __RAW_RW_LOCK_UNLOCKED	 { RW_LOCK_BIAS } #define RW_LOCK_BIAS	 0x01000000

(2)函数read_lock和read_unlock

函数read_lock用于加读者锁,函数read_unlock用于解读者锁,两函数需要配对使用。下面分别进行说明:

    函数read_lock

    读/写自旋锁lock空闲值为0x01000000,当有一个读者进行读操作时,它加读者锁,执行运算lock=lock-1,lock值为0x00ffffff;当接着有第二个读者进行读操作时,可以进行并发的读,再执行运算lock=lock-1,lock值为0x00fffffe;依此类推,可支持多个读者同时读操作。

    如果在读操作正进行(如:有2个读者正进行操作,lock值为0x00fffffe)时,有一个写者请求写操作时,写操作必须等待读者全部完成操作,每个读者完成操作时,执行运算lock=lock+1,当2个读者的操作完成后,lock值为0x01000000,表示写锁空闲,可以进行写操作或并发的读操作。

    如果一个写操作正进行时,执行运算lock=lock-0x01000000,lock值为0x00000000,表示写者锁已加锁,另一个写者无法对临界区数据进行访问。此时,如果有一个读者进行读操作请求时,执行运算lock=lock-1,结果为负数,则状态寄存器符号位置为1,加读者锁失败,将lock还原(lock=lock+1),读者循环等待,直到写操作完成(即lock值为0x01000000)时。

    写操作完成时,lock值为0x01000000,表示写锁空闲,可以进行写操作或并发的读操作。这时,正等待的读者执行运算lock=lock-1,结果为0x00ffffff,则状态寄存器符号位置为0,跳出加读者锁的等待循环,加锁成功,读者进行读操作。

    函数read_lock关闭内核抢占,加读者锁,即将读者数增加1,其列出如下(在include/linux/spinlock.h中):

    #define read_lock(lock)	 _read_lock(lock)

    函数_read_lock列出如下(在kernel/spinlock.c中):

    void __lockfunc _read_lock(rwlock_t *lock) { preempt_disable(); //关闭内核抢占rwlock_acquire_read(&lock->dep_map, 0, 0, _RET_IP_); /*用于自旋锁调试*/ /*下面语句相当于_raw_read_lock(lock)*/ LOCK_CONTENDED(lock, _raw_read_trylock, _raw_read_lock); } # define _raw_read_lock(rwlock)	 __raw_read_lock(&(rwlock)->raw_lock)

    函数__raw_read_lock增加读锁,即锁状态值rw减1,由于读者计数以补码形式存放在锁状态值中,因此,减1表示读者计数增加1。其列出如下(在include/asm-x86/spinglock.h中):

    static inline void __raw_read_lock(raw_rwlock_t *rw) { asm volatile(LOCK_PREFIX " subl $1,(%0)/n/t"//*rw=*rw-1 "jns 1f/n" //如果符号位为0,跳转到1 "call __read_lock_failed/n/t" "1:/n" ::LOCK_PTR_REG(rw) : "memory"); }

    函数__read_lock_failed进行加读者锁失败后的循环等待操作。加读者锁失败,说明有一个写者正在写操作,因此,锁状态值为*rw=0x00000000,函数__raw_read_lock在执行*rw=*rw-1后,rw值为0xffffffff,即传入函数__read_lock_failed的rw值为0xffffffff。

    函数__read_lock_failed执行*rw=*rw+1后,锁状态值为*rw=0x00000000,然后,进入循环等待状态,直到,写者完成写操作后将锁状态值*rw置为0x01000000。这时,函数__read_lock_failed才跳出循环等待状态,加读者锁成功。

    函数__read_lock_failed列出如下(在include/asm-x86/lib/rwlock_64.h中):

    /* rdi指向rwlock_t */ ENTRY(__read_lock_failed) CFI_STARTPROC //即:#define CFI_STARTPROC.cfi_startproc LOCK_PREFIX incl (%rdi) // *rw=*rw+1,值为0x00000000 1:	rep //循环等待*rw值被写者修改为0x01000000 nop cmpl $1,(%rdi) // *rw-1 js 1b //如果符号位为1,表明*rw值还为0x00000000,跳转到1进行循环等待 LOCK_PREFIX /* 运行到这里,说明写者操作完成,*rw值为0x01000000 */ decl (%rdi) //执行加读者锁操作*rw=*rw-1 js __read_lock_failed//如果符号位为1,表明*rw值为0x00000000,跳转到函数开头进行循环等待 ret CFI_ENDPROC //即:#define CFI_ENDPROC .cfi_endproc END(__read_lock_failed)

    由于汇编语言程序无法产生帧信息,由用户手动添加指示语句。上述代码中,指示语句.cfi_startproc用于调试时的调用帧信息处理,在每个函数的开始处使用,它在.eh_frame中生成一个条目,初始化一些内部数据结构,并发出构架依赖的初始CFI(Call Frame Information)指令。在函数结束处使用.cfi_endproc关闭该功能。

      函数read_unlock

      函数read_unlock开读者锁,即将锁状态值减1,由于读者计数以补码形式存放在锁状态值中,因此,加1表示读者计数减1。其列出如下

      # define read_unlock(lock) / do {__raw_read_unlock(&(lock)->raw_lock); __release(lock); } while (0)# define __release(x)	__context__(x,-1) static inline void __raw_read_unlock(raw_rwlock_t *rw) { /* rw->lock= rw->lock +1*/ asm volatile(LOCK_PREFIX "incl %0" :"+m" (rw->lock) : : "memory"); }

      (3)函数write_lock和write_unlock

      函数write_lock和write_unlock分别加写者锁和解写者锁,分别说明如下:

        函数write_lock

        只有在没有读者或写者对临界区数据进行操作时,加写者锁才会成功,即:只有锁状态值lock值为0x01000000时,写者加锁才能成功,执行运行lock=lock-0x01000000运算。

        当有读者或写者操作临界区数据时,lock值为比0x01000000小的正数,如果值为0x00000000表示有一个写者正在写操作,如果值为0x00ffffff,表示有1个读者在进行读操作,如果值为0x00fffffe,表示有2个读者在进行读操作,依此类推。此时,写者只能循环等待,直到lock值为0x01000000。

        函数write_lock关闭内核抢占,加写者锁,其列出如下(在include/linux/spinlock.h中):

        #define write_lock(lock)	 _write_lock(lock)

        函数_write_lock列出如下(在kernel/spinlock.c中):

        void __lockfunc _write_lock(rwlock_t *lock) { preempt_disable(); /*关闭内核抢占*/rwlock_acquire(&lock->dep_map, 0, 0, _RET_IP_); /*用于自旋锁调试*/ /*下面语句相当于_raw_write_lock(lock)*/ LOCK_CONTENDED(lock, _raw_write_trylock, _raw_write_lock); }   # define _raw_write_lock(rwlock)	__raw_write_lock(&(rwlock)->raw_lock)   static inline void__raw_write_lock(raw_rwlock_t *rw) { asm volatile(LOCK_PREFIX " subl %1,(%0)/n/t" /* RW_LOCK_BIAS-rw*/ /* 如果没有读者或写者,*rw为0x01000000,RW_LOCK_BIAS-rw为0 */ "jz 1f/n" /*值为0,跳转到1*/"call __write_lock_failed/n/t" /*加写者锁失败*/ "1:/n" /* RW_LOCK_BIAS定义为0x01000000*/::LOCK_PTR_REG (rw), "i" (RW_LOCK_BIAS) : "memory"); }

        运行函数__write_lock_failed时,说明加写者锁失败。如果加写者锁失败,说明有读者或写者正在访问临界区数据,*rw值为一个小于0x01000000的正数。此时,函数__write_lock_failed循环等待直到,读者或写者完成操作,锁变为空闲,即*rw值为0x01000000。

        函数__write_lock_failed列出如下(在include/asm-x86/lib/rwlock_64.h中):

        /* rdi:	pointer to rwlock_t */ ENTRY(__write_lock_failed) CFI_STARTPROC /*用于调试时将调用帧信息写入 LOCK_PREFIX addl $RW_LOCK_BIAS,(%rdi) // *rw=*rw+$RW_LOCK_BIAS,还原为尝试加锁前的状态值 1:	repnop cmpl $RW_LOCK_BIAS,(%rdi) //比较结果 = *rw-$RW_LOCK_BIAS jne 1b //比较结果不为0,说明有写者或读者在访问临界区,跳转到1进行循环等待 LOCK_PREFIX //锁内存管理器,确保原子操作 /*运行到这里,说明锁空闲,*rw值为0x010000,执行加写者锁操作*/ subl $RW_LOCK_BIAS,(%rdi) //*rw=*rw-RW_LOCK_BIAS jnz__write_lock_failed /*如果*rw不为0,说明加写者锁失败,跳转到函数头循环等待*/ ret CFI_ENDPROCEND(__write_lock_failed)

        函数write_unlock

        函数write_unlock在写者操作完后解写者锁,读/写自旋锁变为空闲,锁状态值lock变为: 0x00000000+0x01000000。以后,读者或写者可以访问临界区数据了。

        函数write_unlock列出如下:

        # define write_unlock(lock) / do {__raw_write_unlock(&(lock)->raw_lock); __release(lock); } while (0)

        函数_write_unlock列出如下(在kernel/spinlock.c中):

        void __lockfunc _write_unlock(rwlock_t *lock) { rwlock_release(&lock->dep_map, 1, _RET_IP_);_raw_write_unlock(lock); preempt_enable()

        抱歉!评论已关闭.