297 lines
12 KiB
Markdown
297 lines
12 KiB
Markdown
# 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 临界区执行
|
||
|
||
正常执行时,用户态代码:
|
||
|
||
1. 将临界区描述符地址写入 `rseq->rseq_cs`
|
||
2. 读取 `rseq->cpu_id` 获取当前 CPU
|
||
3. 使用该 CPU ID 访问 per-CPU 数据
|
||
4. 完成操作后,清除 `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
|
||
|
||
```c
|
||
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 的典型使用模式:
|
||
|
||
```c
|
||
// 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](https://man7.org/linux/man-pages/man2/rseq.2.html)
|
||
- [LWN: Restartable sequences](https://lwn.net/Articles/697979/)
|
||
- Linux 6.6.21 kernel/rseq.c
|