DragonOS/docs/kernel/ipc/rseq.md

297 lines
12 KiB
Markdown
Raw Permalink Normal View History

# Restartable Sequences (rseq) 机制
## 1. 概述
Restartable Sequencesrseq可重启序列是一种用户态与内核协作的机制用于实现高效的 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