DragonOS/docs/kernel/ipc/rseq.md

297 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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