并发和同步是操作系统内核设计的核心问题,也是 Android 系统每一个层次——从 Binder 驱动到 ART 虚拟机——都不可回避的主题。本文以 Android/Linux 为背景,深入分析 spinlock、mutex、RCU、atomic 操作和 futex 这五种经典同步机制,并结合 Binder 驱动和 ART 中的实际应用进行剖析。
一、原子操作(Atomic Operations)
1.1 基本原理
原子操作是最底层的同步原语,由 CPU 指令(如 x86 的 LOCK CMPXCHG,ARM 的 LDREX/STREX)保证操作的不可分割性。
// include/linux/atomic.h 和 arch/arm64/include/asm/atomic.h |
1.2 在 Binder 驱动中的应用
Binder 驱动中的引用计数大量使用原子操作:
// drivers/android/binder.c |
使用原子操作避免了对多进程共享的 binder_node 引用计数的锁争用。
二、自旋锁(Spinlock)
2.1 基本原理
Spinlock 是最简单的锁实现。当锁被持有时,其他 CPU 核心在其上”自旋”(不断轮询)直到锁被释放。适用于临界区极短的场景。
// include/linux/spinlock.h |
2.2 Binder 驱动中的 Spinlock 分层
Binder 驱动使用复杂的锁层级来避免死锁。最外层是 binder_procs_lock(保护全局 binder_procs 列表),内层是进程级别的锁,最内层是 binder_node 级别的锁。
// drivers/android/binder.c |
2.3 Spinlock 在用户空间的使用
Android 的 pthread 实现(bionic libc)中,pthread_mutex_t 的快速路径使用原子操作,慢速路径才进入 futex。pthread_spinlock_t 则是纯粹的用户空间 spinlock。
三、互斥锁(Mutex)
3.1 基本原理
Mutex 允许锁持有者睡眠(与 spinlock 不同),适用于临界区较长的场景。在 Linux 内核中通过 mutex 结构实现:
// include/linux/mutex.h |
3.2 Binder 驱动中的 Mutex
// drivers/android/binder.c |
四、RCU(Read-Copy-Update)
4.1 基本原理
RCU 是一种为”读写比例极大”的场景优化的同步机制。基本思想是:读者无锁访问数据(无需获取任何锁),写者先复制数据、修改副本、然后原子地更新指针。
// include/linux/rcupdate.h |
4.2 Binder 驱动中的 RCU
Binder 驱动在全局 binder_procs 哈希表的查找中使用 RCU。查找 binder_proc 是高频只读操作,而添加/删除 binder_proc 相对很少:
// drivers/android/binder.c |
五、Futex(Fast Userspace Mutex)
5.1 基本原理
Futex 是 Linux 提供的最重要的用户空间同步系统调用。它在无竞争时完全在用户空间运行(原子操作),只在有竞争时才陷入内核:
// include/uapi/linux/futex.h |
5.2 ART 中 Object.wait/notify 的 Futex 实现
ART 虚拟机使用 futex 来实现 Java 的 Object.wait() / Object.notify():
// art/runtime/base/mutex.cc |
ART 的 Monitor(对应 Java synchronized 关键字的实现)同样依赖 futex。Monitor::MonitorEnter 在无竞争时走快速路径(CAS),有竞争时通过 futex 睡眠等待;Monitor::Notify 通过 futex wake 唤醒等待者:
// art/runtime/monitor.cc |
六、Binder 驱动的锁层级设计
Binder 驱动中有一个严格的锁获取顺序,防止死锁:
binder_procs_lock (全局互斥锁) |
所有代码路径必须按这个顺序获取锁。Binder 驱动在开发初期曾面临严重的死锁问题,后续通过严格的 lockdep 注解和代码审查解决了这些问题。
// drivers/android/binder.c |
七、核心面试题
Q1:spinlock 和 mutex 的核心区别是什么?什么场景用哪个?
核心区别:spinlock 等待时 CPU 忙等(不释放 CPU),mutex 等待时线程进入睡眠(释放 CPU)。因此:spinlock 适用于临界区极小(微秒级)且不能睡眠的场景(如中断处理程序),mutex 适用于临界区可能较长、允许睡眠的场景。关键判断:如果临界区中有可能睡眠的操作(如内存分配 GFP_KERNEL、copy_from_user),必须用 mutex 而不能用 spinlock(否则可能死锁)。
Q2:ART 为什么选择基于 futex 实现锁而不是仅依赖 pthread_mutex_t?
ART 需要更精细地控制锁行为:(1) 性能——pthread_mutex 的函数调用开销在某些 ARM 芯片上较大;ART 通过在用户空间用原子变量 CAS 可以避免大量情况下的系统调用。(2) 线程状态管理——ART 需要知道线程是否在等待锁(用于线程 suspend、GC safepoint 等),这需要与运行时深度集成。(3) Monitor 膨胀(thin lock → fat lock)需要精确控制底层 futex 操作,普通 pthread_mutex 无法支持。
Q3:RCU 中的 synchronize_rcu 是如何知道所有读者都完成的?为什么说 RCU 读者是无锁的?
RCU 利用 Linux 内核的抢占调度:rcu_read_lock() 实际只做 preempt_disable(),禁止内核抢占;rcu_read_unlock() 做 preempt_enable()。synchronize_rcu() 等待所有 CPU 至少经历一次上下文切换(即一个 grace period),这保证了所有在 rcu_read_lock/unlock 区间内的读者都已离开临界区。读者无锁是因为它仅禁用了抢占,没有获取任何锁——这意味着读者零争用开销,非常适合 Binder 驱动中 binder_procs 的查找这种高频只读场景。
AOSP 核心路径参考:
drivers/android/binder.c— Binder 驱动锁层级art/runtime/base/mutex.cc— ART Mutex(基于 futex)art/runtime/monitor.cc— ART Monitor(synchronized 实现)include/linux/spinlock.h— Linux spinlockinclude/linux/mutex.h— Linux mutexinclude/linux/rcupdate.h— Linux RCUinclude/uapi/linux/futex.h— Futex 系统调用接口



