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_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 字节)

  • 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 的典型使用模式:

// 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. 参考资料