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
|