内核同步机制 - 读写锁 read_lock()/write_lock

  
读写锁的基本原理类似自旋锁,它区分读取和写入场景,允许多个读线程同时访问共享数据,而保持读-写和写-写互斥,适用频繁读取数据,而修改相对较少的场景;

1. 读写锁变量

内核使用rwlock_t表示读写锁变量,raw_lock成员是架构相关的,其它成员用于锁调试和死锁检查等:
typedef struct {
    arch_rwlock_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;
#endif
} rwlock_t;

raw_lock成员和架构相关,以arm为例,lock成员是一个无符号整形变量:
typedef struct {
    volatile unsigned int lock;
} arch_rwlock_t;

注意:使用volatile类型声明lock变量,目的在于消除编译器优化,优化器在用到这个变量时必须每次都小心的重新读取这个变量的值,而不是使用保存在寄存器里的备份;

2. 初始化读写锁

使用DEFINE_RWLOCK宏来初始化读写锁,锁变量的初值是 __ARCH_RW_LOCK_UNLOCKED ,0表示未上锁状态:
#define DEFINE_RWLOCK(x)    rwlock_t x = __RW_LOCK_UNLOCKED(x)

#define __RW_LOCK_UNLOCKED(lockname) \
    (rwlock_t) {.raw_lock = __ARCH_RW_LOCK_UNLOCKED,    \
                RW_DEP_MAP_INIT(lockname) }

#define __ARCH_RW_LOCK_UNLOCKED        { 0 }

3. 读写锁原理与底层实现

读写锁包括读取锁和写入锁,多个读线程可以同时访问共享数据;写线程必须等待所有读线程都释放锁以后,才能取得锁;同样的,读线程必须等待写线程释放锁后,才能取得锁;
也就是说读写锁要确保的是如下互斥关系:可以同时读,但是读-写,写-写都是互斥的;
这有点儿类似RCU机制,适合频繁读取数据,而写入操作相对比较少的场景,下面我们看看读写锁的实现原理:

锁变量的初值是 __ARCH_RW_LOCK_UNLOCKED,0表示未锁状态;
写入锁检查锁变量,如果是未锁状态,则为锁变量的第31位置位(0x80000000),否则等待锁变量被释放;释放写入锁,也就是清除锁变量的第31位;
读取锁则要检查锁变量的第31位是否置位,是则等待写入锁释放,然后给锁变量加1;释放读取锁时,锁变量减1;

读写锁的底层实现是平台依赖的,因为在读写锁变量的过程要保持原子操作,这需要平台提供支持,以ARM64为例,先来看看平台提供的同步原语:
LDAXR: Load-Acquire eXclusive Register,以独占方式读取内存中的值,并为该段内存设置独占标记;
LDAXR Xt, [Xn];
从寄存器Xn指向的内存中读取数据,并保存到寄存器Xt中,同时为Xn指向的内存区域设置独占标记;

STLXR: Store-Release eXclusive Register,更新内存中的值时,首先要检查独占标记,并以此来决定是否更新内存中的值;
STLXR Ws, Xt, [Xn];
检查Xn指向的内存区域是否设置了独占标记,如果设置了,则把寄存器Xt中的数据保存到Xn指向的内存,并将寄存器Ws设置为0,最后清除独占标记;
如果没有设置独占标记,则不会更新内存,且将寄存器Ws设置为1;

类似的同步原语还有LDXR/STXR, LDAR/STLR等:
LDXR/STXR: 以独占方式读取/更新内存值,但不会设置/清除独占标记;
LDAR/STLR: 设置/清除独占标记,但是不会检查独占标记;

下面分析读取锁和写入锁的底层实现,使用平台底层同步原语是C语言无法实现的功能,需要借助内联汇编:
static inline void arch_read_lock(arch_rwlock_t *rw)
{
    unsigned int tmp, tmp2;

    asm volatile(
    "      sevl\n"
    "1:    wfe\n"
    "2:    ldaxr    %w0, %2\n"
    "      add      %w0, %w0, #1\n"
    "      tbnz     %w0, #31, 1b\n"
    "      stxr     %w1, %w0, %2\n"
    "      cbnz     %w1, 2b\n"
    : "=&r" (tmp), "=&r" (tmp2), "+Q" (rw->lock)
    :
    : "memory");
}
sevl命令是set event locally的缩写,即在当前核上设置一个事件,在别人释放锁时,发cpu事件唤醒等待的核来抢占,需要配合wfe使用;
wfe命令即wait for event,等待事件到达,这会让cpu进入低功耗模式;
"=&r" (tmp), "=&r" (tmp2), "+Q" (rw->lock)是输出部,指定变量的存放位置,%w0是一个占位符,表示tmp变量,"w"表示使用32位寄存器,后面以此类推;
ldaxr命令以独占方式读取锁变量(rw->lock),并为锁变量所在的内存区域设置独占标记;
tbnz命令检查是否有写入锁,有则等待写入锁释放,否则更新锁变量的值,此时的锁变量值已经加1了;
注意这里仅是以独占方式更新锁变量,并没有清除独占标记,具体的清除操作要在释放读取锁时进行;

static inline void arch_read_unlock(arch_rwlock_t *rw)
{
    unsigned int tmp, tmp2;

    asm volatile(
    "1:    ldxr    %w0, %2\n"
    "      sub     %w0, %w0, #1\n"
    "      stlxr   %w1, %w0, %2\n"
    "      cbnz    %w1, 1b\n"
    : "=&r" (tmp), "=&r" (tmp2), "+Q" (rw->lock)
    :
    : "memory");
}

释放读取锁的过程就简单了,锁变量减1,然后更新锁变量,清除独占标记;

static inline void arch_write_lock(arch_rwlock_t *rw)
{
    unsigned int tmp;

    asm volatile(
    "      sevl\n"
    "1:    wfe\n"
    "2:    ldaxr    %w0, %1\n"
    "      cbnz     %w0, 1b\n"
    "      stxr     %w0, %w2, %1\n"
    "      cbnz     %w0, 2b\n"
    : "=&r" (tmp), "+Q" (rw->lock)
    : "r" (0x80000000)
    : "memory");
}


设置写入锁就是为锁变量的第31位置位,在此之前要检查所有读取锁是否已经释放,如果还有读取锁则进入等待状态;

4. 读写锁的操作

上面介绍的是读写锁的底层实现,当然内核是不会让我们直接调用这么底层的函数的,那样的话兼容性就太差了;
内核提供一系列的帮助函数,对底层实现进行封装,简化读写锁的操作;
透过这些操作我们也能看到基本的读写锁操作是禁止内核抢占的,扩展操作还可以禁止中断或中断底半部;
下面我们列出这些操作,但是不再进一步展开,基本就是增加关中断,关底半部;

#define read_trylock(lock)     __cond_lock(lock, _raw_read_trylock(lock))
#define write_trylock(lock)    __cond_lock(lock, _raw_write_trylock(lock))

#define write_lock(lock)       _raw_write_lock(lock)
#define read_lock(lock)        _raw_read_lock(lock)

#define read_lock_irq(lock)    _raw_read_lock_irq(lock)
#define read_lock_bh(lock)     _raw_read_lock_bh(lock)
#define write_lock_irq(lock)   _raw_write_lock_irq(lock)
#define write_lock_bh(lock)    _raw_write_lock_bh(lock)
#define read_unlock(lock)      _raw_read_unlock(lock)
#define write_unlock(lock)     _raw_write_unlock(lock)
#define read_unlock_irq(lock)  _raw_read_unlock_irq(lock)
#define write_unlock_irq(lock) _raw_write_unlock_irq(lock)

5. 读写锁与RCU锁

RCU锁是对读写锁的改进,同样是对读线程和写线程区别对待,只不过对待方式不同;
RCU中,读线程不需要使用锁,可以随时访问共享资源;
而写线程开销比较大,它需要先备份原始数据,然后在备份数据上修改,声明宽限期,确保在宽限期开始前的读线程都结束后才更新数据;

你可能感兴趣的:(kernel)