内核同步之读写锁rwlock_t
本文讲述的是基于linux 4.7.1版本的。
我们先来看读写锁的结构体定义:
include/linux/rwlock_types.h
这里只考虑raw_lock一个成员。
arch/arm/include/asm/spinlock_types.h
arch_rwlock_t只有一个成员lock.
下面,我们来介绍相关的API。
1. rwlock_init()
这个用来初始化rwlock.
include/linux/rwlock.h
include/linux/rwlock_types.h
arch/arm/include/asm/spinlock_types.h
从上述代码实现来看,rwlock_init(lock)就相当于lock->raw_lock.lock = 0;
2. read_lock()
这个API是占用读锁的。
include/linux/rwlock.h
include/linux/rwlock_api_smp.h
148行也是先禁内核抢占,这样,当前cpu就不会切换到其他进程了,只有当前进程和中断程序。
150行先调用do_raw_read_trylock(),如果失败(返回0),再调用do_raw_read_lock()。
include/linux/lockdep.h
我们先来看do_raw_read_trylock().
include/linux/rwlock.h
arch/arm/include/asm/spinlock.h
259-262行,将rw->lock + 1,看是否>=0(strexpl表示261行的操作结果大于等于0时才执行strex操作);如果rw->lock + 1>=0; 则将rw->lock +=1,并写入rw->lock,表示读锁占有成功,strexpl失败会使得while(1)来重新执行259-262的过程。成功占有读锁后,返回0.
如果rw->lock + 1 < 0,则认为写锁被占有(当写锁被占有时,rw->lock被设置为最小的负数0x80000000), 则返回1.
0x80000000是最小的负数,值为-2,147,483,648。
当do_raw_read_trylock()失败时,说明写锁被占用了,则调用do_raw_read_lock()
include/linux/rwlock.h
arch/arm/include/asm/spinlock.h
218-221行,判断rw->lock+1是否>=0,若>=0,表示当前没有写锁占有,则将rw->lock +=1.如果,当前有写锁占有,则219行adds的结果<0,rsbpls不执行(pl表示>=0)。rsbpls用于判断220行的strex是否成功。成功时,%0 = 0-%1=0,失败时,%0=0-%1=-1.
WFE(“mi”)会执行wfemi(wfemi表示负数执行wfe,即写锁存在的情况下会wfe), WFE等待写锁占有者释放锁。
222行, %0 = 0 - %1, 即当strex失败时,%1为1,%0为-1,223行就跳转到1处重新来一遍。
故当没有写锁strex失败时, 223行会执行跳转,重新来一次。当有写锁时WFE, 在被SEV唤醒后,由于之前219行adds的结果小于0,故222行的rsbpls不执行,直接执行223行跳转,重新来一遍。
2. read_unlock()
include/linux/rwlock.h
include/linux/rwlock_api_smp.h
__raw_read_unlock()调用do_raw_read_unlock()之后使能了内核抢占。
include/linux/rwlock.h
arch/arm/include/asm/spinlock.h
arch_read_unlock()比较简单了,就是将rw->lock -=1, 如果当前是最后一把读锁,则SEV,即发送event,用于唤醒可能在wfe的读写锁等待。SEV可以唤醒所有的WFE。
3. write_lock()
include/linux/rwlock.h
include/linux/rwlock_api_smp.h
__raw_write_lock()也是先禁内核抢占,然后调用do_raw_write_trylock(),如果失败(返回0), 表示当前有读锁或则写锁占用,则调用do_raw_write_lock()。
先来看do_raw_write_trylock()。
include/linux/rwlock.h
arch/arm/include/asm/spinlock.h
arch_write_trylock()判断rw->lock是否为0,为0则表示没有读锁或写锁,可以上写锁,就将rw->lock设置为最小的负数0x80000000, 表示写锁占有;如果rw->lock不为0,则表示有读锁或写锁存在,返回0,表示trylock()失败,后面就去调用do_raw_write_lock()了。
trylock()成功时,返回1.
当trylock()失败时,我们接着看do_raw_write_lock().
include/linux/rwlock.h
arch/arm/include/asm/spinlock.h
147-149行,判断rw->lock是否为0,如果不为0,则表示有读锁或者写锁存在,则WFE等待锁释放。当锁被释放后(读锁在最后一把锁unlock才会发SEV),由于之前eq不成立,strexeq不执行,故152行会跳转到147行重新来一遍。假如此时rw->lock是0,即不存在有读锁和写锁,则将rw->lock设置为最小的负数0x80000000.
4. write_unlock()
include/linux/rwlock.h
include/linux/rwlock_api_smp.h
__raw_write_unlock()在调用do_raw_write_unlock()释放锁之后,调用preempt_enable()使能了内核抢占。
我们接着看do_raw_write_unlock().
include/linux/rwlock.h
arch/arm/include/asm/spinlock.h
arch_write_unlock()比较简单,就是将rw->lock设置为0,然后SEV,唤醒等待WFE的锁操作。
至此,我们讲完了所有的API: rwlock_init(), read_lock(), read_unlock(), write_lock(), write_unlock()。
这里,我先提出一个问题:当已有写锁被占有时,其他的尝试读锁和写锁的操作都在WFE等待,如果此时写锁占有者释放了锁,即发送了SEV,那么其他锁操作请求者谁先占用呢?
我的理解是: 当SEV发出后,唤醒了所有的WFE等待的锁操作请求者,至于后面谁先占用锁,就看谁”手快”了,读写锁没有自旋锁spinlock的FIFO。
和spinlock类似地,这几个读写API并不能用于中断或中断和进程共享中。当使用上述lock的API函数后,在unlock之前,如果发生了中断,且中断中也尝试去获取这个锁,那么这就死锁了。要在中断中使用,则需要使用中断版本的API:read_lock_irq()/read_unlock_irq(), write_lock_irq()/write_unlock_irq()