DragonOS/docs/kernel/ipc/rseq.md

12 KiB
Raw Permalink Blame 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_ipabort_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 字节)
  • flags0 表示注册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. 参考资料