Linux rwlock读写锁arch_read_lock与ticket锁对比

Linux rwlock读写锁arch_read_lock与ticket锁对比
Linux rwlock读写锁arch_read_lock与ticket锁对比Linux内核中的rwlock提供了读写分离的同步语义:读者之间不互斥,写者独占.其实现同样经过多层抽象,rwlock_t - arch_rwlock_t.底层最关键的对比在于:传统rwlock使用ticket锁来保证公平性,而arch_read_lock/core读者实现则面临写者饥饿问题.一、rwlock的核心数据结构ctypedef struct {arch_rwlock_t raw_lock;#ifdef CONFIG_DEBUG_SPINLOCKunsigned int magic, owner_cpu;void *owner;#endif#ifdef CONFIG_LOCKDEPstruct lockdep_map dep_map;#endif} rwlock_t;ARM64平台的arch_rwlock_t基于排队锁(queue rwlock):ctypedef struct {union {u32 __lock;struct {u8 wrpend; /* 写者挂起标志: 0表示无写者等待, 1表示有写者等待 */u8 waiters; /* 等待者计数 (包括读者和写者) */u16 readers; /* 活跃读者计数, 0xFFFF表示有写者持有 */};};} arch_rwlock_t;x86平台的传统arch_rwlock_t:ctypedef struct {u32 rwlock; /* 高16位: 读者计数; 低16位: 写者标志 */} arch_rwlock_t;二、arch_read_lock实现分析ARM64的读者锁获取:cstatic inline void arch_read_lock(arch_rwlock_t *lock){unsigned int tmp;asm volatile(1: ldaxr %w0, %1\n tbnz %w0, #24, 2f\n /* 检查wrpend位,有写者挂起则跳转 */ add %w0, %w0, #1\n /* 读者计数1 */ stxr %w0, %w0, %1\n cbnz %w0, 1b\n /* 存储失败则重试 */ dmb ishld\n /* 数据内存屏障 */ ret\n2: /* 写者挂起,等待 */\n ldar %w0, %1\n tbz %w0, #24, 1b\n /* wrpend清零则重试读获取 */ wfe\n b 2b\n: r (tmp), Q (lock-__lock):: memory);}x86平台的arch_read_lock:cstatic inline void arch_read_lock(arch_rwlock_t *lock){/* 尝试原子减1 (rwlock初始值0x01000000, 减1后为0x00FFFFFF表示读锁) */asm volatile(1: lock; decl %0\n jns 3f\n2: rep; nop\n cmpl $0, %0\n js 2b\n jmp 1b\n3:: m (lock-rwlock):: memory, cc);}三、arch_write_lock实现分析ARM64的写者锁获取:cstatic inline void arch_write_lock(arch_rwlock_t *lock){unsigned int tmp;asm volatile(1: mov %w0, #0x00010000\n /* wrpend1, waiters递增 */ ldaxr %w1, %2\n add %w0, %w0, %w1, lsr #16\n /* waiters域递增 */ stxr %w0, %w0, %2\n cbnz %w0, 1b\n/* 等待所有读者释放 */2: dmb ishld\n ldr %w0, %2\n and %w0, %w0, #0xFFFF\n /* 取readers域 */ cbnz %w0, 2b\n /* 仍有读者则等待 */ ret\n: r (tmp), r (lock-wrpend), Q (lock-__lock):: memory);}四、ticket锁形式的rwlock某些架构(如旧版x86)使用了ticket锁思想来实现读写锁.每个锁维护两个计数器:当前服务号和服务号上限.cstruct ticket_rwlock {union {u32 headtail;struct {u16 head; /* 当前服务的票号 */u16 tail; /* 下一个可用的票号 */};};u16 readers; /* 读者计数, 分开存储以避免与写者票号冲突 */};ticket锁形式的读写锁操作:cstatic inline void ticket_write_lock(struct ticket_rwlock *lock){u16 my_ticket xadd(lock-tail, 2); /* 获取一个偶数的写者票号 *//* 等待轮到自己的票号 */while (lock-head ! my_ticket)cpu_relax();/* 等待所有读者释放 */while (lock-readers)cpu_relax();smp_mb();}static inline void ticket_read_lock(struct ticket_rwlock *lock){/* 读者不需要票号队列 */atomic_inc(lock-readers);smp_mb();}static inline void ticket_read_unlock(struct ticket_rwlock *lock){smp_mb();atomic_dec(lock-readers);}五、公平性差异分析传统rwlock(非ticket版)存在严重的写者饥饿问题:如果读者持续到来,写者可能永远无法获取锁.c/* 写者饥饿场景 */CPU0: read_lock(rwlock); /* 持有读锁 */CPU1: read_lock(rwlock); /* 同时获得读锁 */CPU2: write_lock(rwlock); /* 等待所有读者释放 */CPU3: read_lock(rwlock); /* CPU0释放后, CPU3进入, 写者依然等待 *//* 写者CPU2永远等下去... */解决写者饥饿的两个方案:1. Queue rwlock: 当前ARM64使用的方案,通过wrpend位标记等待的写者,新读者看到wrpend后不再获取锁.2. ticket rwlock: 写者获取一个票号,所有操作按票号FIFO执行,新读者即使没有写者等待也必须排队.六、性能对比关键点- 读者冲突场景: 传统rwlock读者之间通过原子增/减操作,缓存行 bouncing 开销大- queue rwlock读者路径: 需要检查wrpend位,多一次分支预测- ticket rwlock读者路径: 完全无需排队,但写者饥饿问题严重- ARM64 LSE指令集: 支持LDAPR等宽松语义指令,可降低读者路径延迟七、qrwlock的改进 - 公平读写锁内核引入了queued_read_lock()来替代旧的arch_read_lock:cstatic inline void queued_read_lock(struct qrwlock *lock){/* 快速路径: 无写者竞争时直接自增读者计数 */if (likely(atomic_inc_not_zero(lock-cnts)))return;/* 慢路径: 有写者竞争, 需要排队 */queued_read_lock_slowpath(lock);}static inline void queued_write_lock(struct qrwlock *lock){/* 尝试获取写锁: 从0xFFFFFFFE变为(0xFFFFFFFE | _QW_LOCKED) */if (likely(atomic_cmpxchg(lock-cnts, 0, _QW_LOCKED) 0))return;queued_write_lock_slowpath(lock);}这种设计的核心优势:读者快速路径在无竞争时仅需一条原子操作,慢路径使用MCS锁队列避免缓存行bouncing.八、使用建议- 读远多于写的场景(如路由表查找): 使用rwlock,但需注意写者饥饿- 读写均衡场景: 优先使用带公平性的qrwlock- 对实时性有要求: 使用RT内核中的rwlock(基于rt_mutex实现),完全避免饥饿- 读者性能极致要求: 考虑RCU或percpu-rw-semaphore替代rwlock