[toc]
include/linux/rwlock.h 读写锁(Read-Write Lock) 允许多读者并发访问的同步原语
历史与背景
这项技术是为了解决什么特定问题而诞生的?
读写锁(Read-Write Lock, rwlock
)是为了解决一个非常普遍的并发访问模式的性能问题:对共享数据的访问是“读多写少”。
在使用简单的互斥锁(mutex
)或自旋锁(spinlock
)时,任何时候都只允许一个任务进入临界区,无论它是读取还是写入。这对于读操作来说是过度限制的,因为读操作本身不会修改数据,允许多个读者同时读取是安全的。如果一个数据结构被内核中成百上千个地方频繁地读取,而只被少数几个地方偶尔写入,那么使用互斥锁会导致所有读者被迫串行执行,极大地降低了系统的并发性能。
rwlock
的诞生就是为了优化这种场景。它提供了一种更精细的锁定机制:
- 读锁(共享锁):允许多个读者同时持有读锁。
- 写锁(排他锁):与互斥锁一样,任何时候只允许一个写者持有写锁。
- 互斥规则:当有任何读者持有读锁时,写者必须等待;当有写者持有写锁时,所有读者和其他写者都必须等待。
它的发展经历了哪些重要的里程碑或版本迭代?
Linux内核中的读写锁实现为了平衡性能和公平性,经历了持续的演进。
- 区分自旋和睡眠版本:一个关键的里程碑是将读写锁机制分化为两种具体的实现:
rwlock_t
(include/linux/rwlock.h
):这是基于自旋锁的实现。获取不到锁的任务会自旋(忙等待),因此它可以在任何上下文(包括中断)中使用,但临界区必须极短且不能睡眠。rw_semaphore
(include/linux/rwsem.h
):这是基于睡眠锁的实现。获取不到锁的任务会睡眠,因此它只能在进程上下文中使用,但临界区可以较长并允许睡眠。
- 公平性策略的演进:早期的读写锁实现可能会倾向于读者(reader-biased),这会导致在读操作繁忙时,写者被“饿死”。现代Linux内核的
rwlock
实现经过了大量优化,采用了一种更公平的策略,通常会优先考虑等待中的写者,以防止写者饥饿。当一个写者在等待锁时,后续到来的新读者也会被阻塞,直到写者完成操作。 - 性能优化:与自旋锁类似,
rwlock_t
的底层实现也从简单的原子计数器演变为更复杂的、对SMP缓存更友好的设计,以减少在多核高竞争下的缓存行争用。
目前该技术的社区活跃度和主流应用情况如何?
读写锁是内核中最基础、最重要的同步原语之一。其代码非常核心和稳定。社区的活跃度主要体现在对其在不同架构和负载下的性能进行微调。
它被广泛应用于内核中符合“读多写少”模式的各种场景:
- 网络子系统:保护路由表、防火墙规则等。这些数据被每个网络包读取,但只在配置变更时写入。
- 文件系统:保护目录缓存(dcache)和inode缓存中的某些数据结构。
- 进程管理:著名的
tasklist_lock
就是一个读写锁,用于保护全局的进程列表。当ps
等命令遍历进程时,获取读锁;当创建或销毁进程时,获取写锁。
核心原理与设计
它的核心工作原理是什么?
rwlock_t
的核心是利用一个**原子变量(通常是一个32位整数)**来同时编码读者数量和写者状态。
状态编码:这个原子变量通常被分成两部分。例如,一个常见的实现是:
- 如果值为0,表示锁未被持有。
- 如果值为正数,表示读者的数量。
- 如果值为某个特殊的负数(或设置了某个高位bit),表示有一个写者持有锁。
获取读锁 (
read_lock
):- 快速路径:原子地给计数器加一。如果结果是正数,说明之前没有写者,成功获取读锁。
- 慢速路径:如果原子加一后的结果是负数(意味着有一个写者正在持有或等待锁),则需要回退操作(原子减一),然后进入一个自旋循环,等待写者释放锁。
获取写锁 (
write_lock
):- 快速路径:通过一次原子的比较并交换操作,尝试将计数器从0(未锁定)设置为“写入”状态。如果成功,获取写锁。
- 慢速路径:如果锁不为0(意味着有读者或另一个写者),则进入自旋循环,等待锁的计数值变为0。
释放读锁 (
read_unlock
):- 原子地给计数器减一。如果减一后计数器变为0,并且有写者在等待,则需要唤醒等待的写者。
释放写锁 (
write_unlock
):- 原子地将锁的状态设置回0(未锁定)。然后唤醒所有在等待的读者和写者。
它的主要优势体现在哪些方面?
- 提高并发性:对于读多写少的场景,它能极大地提高系统的吞吐量,因为它允许多个读操作并行执行。
- 清晰的语义:
read_lock
/write_lock
的API清晰地表达了开发者对临界区内操作的意图。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 更高的开销:与简单的自旋锁相比,读写锁的实现更复杂,其原子操作和逻辑判断也更多。在无竞争或写操作频繁的场景下,它的性能可能反而不如简单的自旋锁。
- 饥饿问题:尽管现代实现试图变得公平,但在极端的负载下,读者或写者仍然可能面临饥饿问题。
- 使用场景的判断:需要开发者准确地判断工作负载是否真的是“读多写少”,否则使用它可能是一种负优化。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
rwlock
是保护被频繁读取、但很少被修改的共享数据的首选方案。
- 保护配置数据:一个数据结构在初始化时被写入一次,之后在系统的整个运行期间只被读取。
- 保护缓存的查找结构:如上文提到的
dcache
、路由表等,查找操作(读)远多于插入/删除操作(写)。 tasklist_lock
:遍历系统中的所有进程是一个读操作,而创建/销毁进程是一个写操作。显然,前者发生的频率远高于后者。
是否有不推荐使用该技术的场景?为什么?
- 写操作频繁:如果读写操作的比例接近甚至超过1:1,那么使用
rwlock
的开销会使其性能劣于简单的spinlock
或mutex
。 - 临界区非常简单且竞争不激烈:如果只是保护一个整数的修改,且竞争很少,那么一个简单的
spinlock
通常更轻量、更高效。 - 当
seqlock
或RCU
是更好的选择时:- 如果写者的性能至关重要,绝对不能被读者阻塞,并且被保护的数据不含指针,那么
seqlock
是更好的选择。 - 如果读者的性能要求达到极致(完全无锁),并且可以接受写者付出复制数据和等待“宽限期”的代价,那么RCU是最终的解决方案。
- 如果写者的性能至关重要,绝对不能被读者阻塞,并且被保护的数据不含指针,那么
对比分析
请将其 与 其他相似技术 进行详细对比。
特性 | 读写锁 (RW_Lock) | 自旋锁/互斥锁 (Spinlock/Mutex) | 顺序锁 (Seqlock) | RCU (Read-Copy-Update) |
---|---|---|---|---|
并发模型 | 读者共享 / 写者互斥 | 完全互斥 | 写者优先,读者乐观验证 | 读者无锁 / 写者复制更新 |
读者开销(无争用) | 中等 (原子操作) | 中等 (原子操作) | 极低 (普通内存读) | 几乎为零 (普通内存读) |
读者行为(有争用) | 阻塞 (自旋或睡眠) | 阻塞 (自旋或睡眠) | 自旋重试 | 无阻塞 (看到旧数据) |
写者饥饿 | 可能 (现代实现已缓解) | 不适用 | 不会 | 不会 |
数据约束 | 无 | 无 | 不能含指针 | 指针管理复杂,需要遵循RCU规则 |
适用场景 | 读多写少的通用场景。 | 读写均衡,或写操作频繁。 | 读多、写极少且极快,写者性能关键。 | 读极多,读者性能要求极致,可容忍数据轻微延迟。 |
include/linux/rwlock_types.h
DEFINE_RWLOCK
1 |
|
include/linux/rwlock.h
write_lock
1 |
read_lock
1 |
write_unlock
1 |
read_unlock
1 |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 wdfk-prog的个人博客!
评论