读写锁rwlock

    xiaoxiao2025-10-19  10

    读写锁引入

    在前面小节分析了spin_lock的实现,可以知道spin_lock只允许一个thread进入临界区,而且对进入临界区中的操作不做细分。但是在实际中,对临界区的操作分为读和写。如果按照spin_lock的实现,当多个read thread都想进入临界区读取的时候,这时候只有一个read thread进入到临界区,这样效率和性能明显下降。所以就针对某些操作read thread占绝大多数的情况下,提出了读写锁的概念。

    读写锁的基本原理

    加锁操作

    假设当前临界区没有任何进程,这时候read进程或者write进程都可以进来,但是只能是其一如果当前临界区只有一个read进程,这时候任意的read进程都可以进入,但是write进程不能进入如果当前临界区只有一个write进程,这时候任何read/write进程都无法进入。只能自旋等待如果当前当前临界区有好多个read进程,同时read进程依然还会进入,这时候进入的write进程只能等待。直到临界区一个read进程都没有,才可进入

    解锁操作

    如果在read进程离开临界区的时候,需要根据情况决定write进程是否需要进入。只有当临界区没有read进程了,write进程方可进入。如果在write进程离开临界区的时候,无论write进程或者read进程都可进入临界区,因为write进程是排它的。

    读写锁的定义

    typedef struct { arch_rwlock_t raw_lock; } rwlock_t; typedef struct { volatile unsigned int lock; } arch_rwlock_t;可以看到读写锁与spin_lock的定义最终相同,只是名字不同罢了。

    读写锁API

    加锁API

    read_lock(lock)/write_lock(lock)                                                               #获取指定的锁 read_trylock(lock)/write_trylock(lock)                                                       #尝试获取锁,如果失败不spin,直接返回 read_lock_irq(lock)/write_lock_irq(lock)                                                   #获取指定的锁,同时关掉本地cpu中断 read_lock_irqsave(lock, flags)/write_lock_irqsave(lock, flags)                #保存本地cpu的irq标志,然后关掉cpu中断,获取指定锁read_lock_bh(lock)/read_lock_bh(lock)                                                   #获取指定的锁,同时关掉中断下半部(bottom half)

    解锁API

    read_unlock(lock)/write_unlock(lock)                                                      #释放指定的锁 read_unlock_irq(lock)/write_unlock_irq(lock)                                          #释放指定的锁,同时使能cpu中断 read_unlock_irqrestore/write_unlock_irqrestore                                     #释放锁,同时使能cpu中断,恢复cpu的标识 read_unlock_bh/write_unlock_bh                                                            #释放锁,同时使能cpu中断的下半部

    读写锁的实现

    写入者加锁操作:

    /* * Write lock implementation. * * Write locks set bit 31. Unlocking, is done by writing 0 since the lock is * exclusively held. * * The memory barriers are implicit with the load-acquire and store-release * instructions. */ 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"); } 通过注释: write操作的上锁操作是给bit31写1, 解锁操作就是给bit31写0 " sevl\n" "1: wfe\n"使cpu进入低功耗模式 2: ldaxr %w0, %1\n读取锁的值,赋值给tmp变量 cbnz %w0, 1b如果tmp的值不为0, 跳转到标号1重新执行。不等于0说明有read/write进程正在持有锁,所以需要进入低功耗等待。 stxr %w0, %w2, %1将锁的bit31设置为1, 然后将设置结果放入tmp中。 cbnz %w0, 2b如果tmp的值不为0,说明上条指令执行失败,跳转到标号2继续执行。 可以看到,对于wirte操作,只要临界区有read/write进程存在,就需要自旋等待,直到临界区没有任何进程存在。

    写入者解锁操作:

    static inline void arch_write_unlock(arch_rwlock_t *rw) { asm volatile( " stlr %w1, %0\n" : "=Q" (rw->lock) : "r" (0) : "memory"); }写操作很简单,就是将锁的值全部清为0而已。

    读取者加锁操作:

    /* * Read lock implementation. * * It exclusively loads the lock value, increments it and stores the new value * back if positive and the CPU still exclusively owns the location. If the * value is negative, the lock is already held. * * During unlocking there may be multiple active read locks but no write lock. * * The memory barriers are implicit with the load-acquire and store-release * instructions. */ 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"); }读取者进入临界区先要判断是否有write进程在临界区,如果有必须自旋。如果没有,则可以进入临界区。 2: ldaxr %w0, %2读取锁的值,赋值给tmp变量。 add %w0, %w0, #1将tmp的值加1, 然后将结果放入tmp中。 tbnz %w0, #31, 1b判断tmp[31]是否等于0,不等于0也就是说write进程在临界区,需要自旋等待,跳到标号1继续。 stxr %w1, %w0, %2将tmp的值复制给lock,然后将结果放入tmp2中。 cbnz %w1, 2b判断tmp2是否等于0,不等于0就跳到标号2继续。 可以看到read操作需要先判断临界区是否有write进程存在,如果有就需要自旋。

    读取者解锁操作:

    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即可。 1: ldxr %w0, %2读取锁的值,复制给tmp sub %w0, %w0, #1将tmp的值减去1,同时将结果放入到tmp中 stlxr %w1, %w0, %2将tmp的值复制给lock,然后将结果存放到tmp2 cbnz %w1, 1b如果tmp2的值不为0,就跳转到标号1继续执行。

    小节

    从上面的定义可知,lock的是一个unsigned int的32位数。 0-32bit用来表示read thread counter, 31bit用来表示write therad counter。 这样设计是因为write进程每次进入临界区只能有一个,所以一个bit就可以。剩余的31bit位全部给read therad使用。 从概率上将,当一个进程试图去写时,成功获得锁的几率要远小于读进程概率。所以在一个读写相互依赖的系统中,这种设计会导致读取者饥饿,也就是没有数据可读。所以读写锁使用的系统就是读操作占用绝大多数,这样读写操作就比以前的spin lock大大提升效率和性能。
    转载请注明原文地址: https://ju.6miu.com/read-1303312.html
    最新回复(0)