在高并发系统中,每 CPU 计数器是统计热点路径事件的基础构件。传统实现依赖原子操作或锁保护,但原子指令的 lock 前缀在 x86 上会增加数十个时钟周期,且无论是否发生竞争都会付出代价。Linux 4.18 引入的 restartable sequences(rseq)提供了一种用户空间无锁方案,通过内核协作实现关键区段的原子性保证。本文聚焦每 CPU 计数器的具体实现,剖析关键区段边界设计与信号安全机制。
关键区段的三元组结构
rseq 的核心是一个由内核与用户空间协作定义的关键区段,由三个指令指针唯一确定:
- start_ip:关键区段起始地址,进入此区域后 rseq 开始跟踪
- post_commit_ip:提交指令的下一条指令地址,完成此点后关键区段结束
- abort_ip:中断发生时的跳转目标,内核将指令指针重置至此
这三个地址封装在 struct rseq_cs 中,通过 struct rseq 的 rseq_cs 字段向内核注册。当线程在 start_ip 与 post_commit_ip 之间遭遇信号、抢占或 CPU 迁移时,内核自动将执行流导向 abort_ip,用户空间在此处理重试逻辑。
关键约束在于:关键区段内禁止系统调用。若执行 syscall,内核会立即以 SIGSEGV 终止进程。这一设计强制要求关键区段保持极简,通常只包含内存加载、算术运算和最终存储。
每 CPU 计数器的实现路径
实现每 CPU 计数器需要协调 CPU 号获取与关键区段的原子性。rseq 在 struct rseq 中提供 cpu_id_start 和 cpu_id 字段,用户空间在关键区段开始前读取 cpu_id_start,关键区段内将其与 cpu_id 比较。若二者不一致,说明发生了 CPU 迁移,操作应中止重试。
典型的递增操作内联汇编结构如下:
- 准备阶段:通过
leaq将当前关键区段描述符地址载入寄存器,写入rseq_cs字段 - CPU 校验:比较
cpu_id_start与cpu_id,不匹配则跳转到 abort 路径 - 提交操作:执行
addq指令完成计数器递增,此为关键区段的唯一写入点 - 完成点:post_commit_ip 标记提交完成,此后即使中断也不会回滚
abort 路径位于关键区段外,负责清理并返回错误码,触发上层重试。这种设计将乐观路径(无中断)的成本降至最低 —— 仅需一次内存比较和一次加法指令,无需原子操作或锁竞争。
信号安全与回滚边界
信号安全是 rseq 设计的核心考量。当信号到达时,若线程正处于关键区段内,内核必须确保操作要么完整提交,要么完全回滚,不会出现中间状态。
内核通过指令指针重写实现这一点。信号处理前,内核检查当前 rseq_cs 是否指向活跃的关键区段,且程序计数器是否位于 start_ip 与 post_commit_ip 之间。若条件满足,内核将返回地址替换为 abort_ip,用户空间从 abort 路径继续执行而非信号处理完成后原位置恢复。
这一机制要求 abort handler 具备幂等性。对于计数器递增场景,abort handler 只需返回错误码,由上层逻辑重试整个操作。由于关键区段内尚未执行实际存储(或存储已被架构保证为原子单指令),重试不会导致数据损坏。
需要注意的是,rseq 提供 RSEQ_CS_FLAG_NO_RESTART_ON_SIGNAL 标志,允许关键区段在信号到达时不触发回滚。这在某些需要与信号处理协作的场景中有用,但会丧失关键区段的原子性保证,需谨慎使用。
工程化注意事项
编译器屏障与 volatile
编译器优化可能破坏 rseq 的假设。例如,cpu_id_start 必须在关键区段开始前只读取一次,若编译器将其缓存到寄存器并在关键区段内复用,而内核在此期间更新了该值,将导致 CPU 迁移检测失效。
必须使用 volatile 或编译器屏障确保内存访问顺序:
#define RSEQ_ACCESS_ONCE(x) (*(__volatile__ __typeof__(x) *)&(x))
int cpu = RSEQ_ACCESS_ONCE(__rseq_abi.cpu_id_start);
关键区段前后也应插入 barrier() 防止指令重排,确保 rseq_cs 的写入和清除按预期顺序执行。
调试陷阱
使用调试器单步执行 rseq 关键区段会导致无限循环。当调试器在关键区段内暂停并恢复执行时,内核可能判定为中断事件并触发 abort 路径,而调试器继续单步会再次进入关键区段,形成死循环。解决方法是使用 cpu_opv 系统调用(尚未广泛合并)或避免在关键区段内设置断点。
性能收益
根据 EfficiOS 的基准测试,rseq 相比 sched_getcpu() 配合原子操作的方案,在 x86 上实现 7.7 倍加速,ARM 上达 11 倍。对于每 CPU 缓冲区写入等场景,性能提升虽不如纯计数器显著(x86 1.2 倍,ARM 1.1 倍),但仍能消除原子操作带来的缓存一致性流量。
总结
rseq 通过内核 - 用户空间协作,将每 CPU 数据访问的同步成本降至单条指令级别。关键区段的三元组设计(start/commit/abort)明确了原子性边界,信号安全机制通过指令指针重写确保操作要么完成要么回滚。实现时需注意编译器优化屏障、禁止关键区段内系统调用,以及调试器单步执行的陷阱。对于高频计数和统计场景,rseq 提供了一种无需锁竞争且性能可预测的方案。
资料来源
- Restartable Sequences — The Linux Kernel documentation
- The 5-year journey to bring restartable sequences to Linux,EfficiOS
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。