12 KiB
12 KiB
Restartable Sequences (rseq) 机制
1. 概述
Restartable Sequences(rseq,可重启序列)是一种用户态与内核协作的机制,用于实现高效的 per-CPU 数据访问。它允许用户态程序在不使用传统同步原语(如锁或原子操作)的情况下,安全地访问和修改 per-CPU 数据结构。
1.1 设计目标
rseq 的核心目标是提供一种乐观并发机制:
- 用户态代码可以假设自己不会被打断,直接操作 per-CPU 数据
- 如果确实被打断(抢占、信号等),内核负责将执行重定向到恢复路径
- 这种"要么完整执行,要么从头开始"的语义,避免了传统锁的开销
1.2 典型应用场景
- 内存分配器:tcmalloc、jemalloc 等使用 per-CPU 缓存加速分配
- 引用计数:per-CPU 引用计数可避免缓存行争用
- 统计计数器:per-CPU 计数器的无锁更新
- RCU 读侧临界区:快速获取当前 CPU 信息
2. 核心概念
2.1 临界区(Critical Section)
rseq 临界区是一段用户态代码,具有以下特征:
┌─────────────────────────────────────────────────────────────┐
│ rseq 临界区 │
│ │
│ start_ip ──► ┌─────────────────────────────────┐ │
│ │ 1. 读取 cpu_id │ │
│ │ 2. 使用 cpu_id 索引 per-CPU 数据 │ │
│ │ 3. 执行操作(读/改/写) │ │
│ │ 4. 提交点(commit point) │ │
│ end_ip ────► └─────────────────────────────────┘ │
│ │ │
│ │ 被打断时跳转 │
│ ▼ │
│ abort_ip ──► ┌─────────────────────────────────┐ │
│ │ 恢复/重试逻辑 │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
- start_ip:临界区起始地址
- post_commit_offset:从 start_ip 到提交点的偏移量
- abort_ip:中断恢复地址,必须位于临界区外
2.2 用户态数据结构
用户态需要在 TLS(线程本地存储)中维护一个 struct rseq 结构:
| 字段 | 大小 | 说明 |
|---|---|---|
| cpu_id_start | u32 | 进入临界区时的 CPU ID |
| cpu_id | u32 | 当前 CPU ID(内核更新) |
| rseq_cs | u64 | 指向当前临界区描述符的指针 |
| flags | u32 | 标志位(保留) |
| node_id | u32 | NUMA 节点 ID |
| mm_cid | u32 | 内存管理上下文 ID |
2.3 临界区描述符
struct rseq_cs 描述一个具体的临界区:
| 字段 | 大小 | 说明 |
|---|---|---|
| version | u32 | 版本号,必须为 0 |
| flags | u32 | 标志位 |
| start_ip | u64 | 临界区起始地址 |
| post_commit_offset | u64 | 临界区长度 |
| abort_ip | u64 | 中断恢复地址 |
3. 工作原理
3.1 注册流程
用户态 内核态
│ │
│ sys_rseq(rseq_ptr, len, 0, sig) │
│ ──────────────────────────────────────► │
│ │ 1. 验证参数
│ │ 2. 记录注册信息
│ │ 3. 设置 NEED_RSEQ 标志
│ │
│ 返回 0(成功) │
│ ◄────────────────────────────────────── │
│ │
3.2 临界区执行
正常执行时,用户态代码:
- 将临界区描述符地址写入
rseq->rseq_cs - 读取
rseq->cpu_id获取当前 CPU - 使用该 CPU ID 访问 per-CPU 数据
- 完成操作后,清除
rseq->rseq_cs
3.3 内核干预时机
内核在以下事件发生后,返回用户态前进行检查和修正:
┌──────────────────────────────────────────────────────────────┐
│ 触发 rseq 处理的事件 │
├──────────────────────────────────────────────────────────────┤
│ 抢占(Preemption) │
│ └─► 调度器切换进程时设置 PREEMPT 事件 │
│ │
│ 信号递送(Signal Delivery) │
│ └─► 设置信号帧前设置 SIGNAL 事件 │
│ │
│ CPU 迁移(Migration) │
│ └─► 进程被迁移到其他 CPU 时设置 MIGRATE 事件 │
└──────────────────────────────────────────────────────────────┘
3.4 返回用户态前的处理
当进程即将返回用户态时,内核执行以下步骤:
返回用户态前处理流程
│
▼
┌─────────────────┐
│ 检查 NEED_RSEQ │
│ 标志位 │
└────────┬────────┘
│ 已设置
▼
┌─────────────────┐
│ 读取 rseq_cs │
│ 指针 │
└────────┬────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
rseq_cs == 0 rseq_cs != 0
(不在临界区) (在临界区)
│ │
│ ▼
│ ┌───────────────┐
│ │ 当前 IP 在 │
│ │ 临界区内? │
│ └───────┬───────┘
│ 是 │ 否
│ ┌──────────┴──────────┐
│ ▼ ▼
│ ┌───────────────┐ ┌───────────────┐
│ │ 修改返回地址 │ │ 清除 rseq_cs │
│ │ 为 abort_ip │ │ (lazy clear) │
│ └───────────────┘ └───────────────┘
│ │ │
└──────────────┴─────────────────────┘
│
▼
┌─────────────────┐
│ 更新 cpu_id 等 │
│ TLS 字段 │
└─────────────────┘
│
▼
返回用户态
4. 安全机制
4.1 签名验证
注册时用户提供一个 32 位签名值(sig),内核在处理临界区时会验证:
- 读取
abort_ip - 4处的 4 字节 - 必须与注册时的签名匹配
- 防止恶意构造的临界区描述符
4.2 地址验证
内核对所有用户态地址进行严格验证:
start_ip、abort_ip必须在用户地址空间内start_ip + post_commit_offset不能溢出abort_ip必须在临界区外
4.3 错误处理
当检测到以下错误时,内核向进程发送 SIGSEGV:
- 用户内存访问失败
- 签名不匹配
- 地址验证失败
- 版本号不为 0
5. 与进程生命周期的集成
5.1 fork
- CLONE_VM(线程):子线程需要重新注册 rseq
- fork(进程):子进程继承父进程的 rseq 注册状态
5.2 execve
执行新程序时,rseq 注册状态被清除,新程序需要重新注册。
5.3 exit
进程退出时,rseq 状态随 PCB 一起释放,无需特殊处理。
6. 系统调用接口
sys_rseq
long sys_rseq(struct rseq *rseq, u32 rseq_len, int flags, u32 sig);
参数:
rseq:用户态 rseq 结构的地址rseq_len:结构长度(至少 32 字节)flags:0 表示注册,RSEQ_FLAG_UNREGISTER (1) 表示注销sig:签名值
返回值:
- 成功:0
- 失败:负的错误码
错误码:
| 错误码 | 说明 |
|---|---|
| EINVAL | 参数无效(长度、对齐、flags 等) |
| EPERM | 签名不匹配 |
| EBUSY | 已注册(重复注册相同参数) |
| EFAULT | 地址无效 |
7. 辅助向量(auxv)
内核通过 ELF 辅助向量向用户态传递 rseq 支持信息:
| 类型 | 值 | 说明 |
|---|---|---|
| AT_RSEQ_FEATURE_SIZE | 27 | rseq 结构大小(32) |
| AT_RSEQ_ALIGN | 28 | rseq 对齐要求(32) |
用户态库(如 glibc)使用这些信息来:
- 确定内核是否支持 rseq
- 正确分配和对齐 TLS 中的 rseq 结构
8. 使用示例
以下伪代码展示了 rseq 的典型使用模式:
// 1. 注册 rseq
struct rseq *rseq_ptr = &__rseq_abi; // TLS 中的 rseq 结构
syscall(SYS_rseq, rseq_ptr, sizeof(*rseq_ptr), 0, RSEQ_SIG);
// 2. 定义临界区描述符
struct rseq_cs cs = {
.version = 0,
.flags = 0,
.start_ip = (uintptr_t)&&start,
.post_commit_offset = (uintptr_t)&&commit - (uintptr_t)&&start,
.abort_ip = (uintptr_t)&&abort,
};
// 3. 执行临界区
retry:
rseq_ptr->rseq_cs = (uintptr_t)&cs;
start:
cpu = rseq_ptr->cpu_id;
// 使用 cpu 访问 per-CPU 数据
per_cpu_data[cpu].counter++;
commit:
rseq_ptr->rseq_cs = 0;
goto done;
abort:
// 签名(必须紧挨在 abort 标签前)
.int RSEQ_SIG
rseq_ptr->rseq_cs = 0;
goto retry;
done:
// 操作完成
9. 参考资料
- Linux rseq(2) man page
- LWN: Restartable sequences
- Linux 6.6.21 kernel/rseq.c