目录

CXL-RPC

面向无一致性跨机共享内存的高性能 RPC 框架

CXL-RPC 是一个面向 CXL 2.0 跨主机共享内存 的 C++20 RPC 原型系统。它针对当前真实 CXL 平台通常不具备跨主机硬件缓存一致性、也不能依赖跨节点 CAS/FAA 等原子读改写操作的现实约束,使用显式缓存维护、无原子 SPSC 队列和共享内存偏移量引用来完成请求传输、远程执行和大对象访问。

与把 CXL 内存当作不可缓存消息通道的方案不同,CXL-RPC 让请求和缓冲区在必要时保留在本地 CPU 缓存中,并在跨节点交接点进行显式写回与失效;与传统网络 RPC 相比,大对象可通过共享 CXL 缓冲区按引用传递,避免反复序列化和复制。

本项目是“中国研究生操作系统开源创新大赛”参赛作品,相关的说明书、答辩材料与演示视频位于仓库根目录。

目录

为什么需要 CXL-RPC

传统 RPC 即使建立在 TCP、RDMA 或用户态网络之上,请求通常仍要经历序列化、协议处理、网络传输、反序列化以及一次或多次数据复制。CXL 等跨机互连技术使多台主机能够映射同一片物理内存,为 RPC 绕开这条消息传递路径提供了新的基础。

不过,真实 CXL 2.0 平台的共享内存并不等于“多机 std::atomic 可用的普通共享内存”:跨主机缓存并不自动一致,且不能假设跨节点原子读改写可用于队列协调。因此,CXL-RPC 的设计目标是:

  • 在无跨机缓存一致性的条件下,仍然利用可缓存访问的效率;
  • 不依赖跨节点原子操作或跨节点中断通知;
  • 为内联小消息、常规大对象和已驻留 CXL 的复杂对象提供不同的数据路径;
  • 将连接建立、请求调度和 CXL 缓冲区分配收敛到运行时,降低应用层的同步负担。

核心设计

flowchart LR
    App[客户端应用] --> API[Requester API]
    API --> Conn[RPConnection\n每连接一条 SPSC 队列]
    Conn -->|16 B Data / 缓冲区描述符| CXL[(共享 CXL 内存)]
    CXL --> Exec[服务端 RPC 执行线程池]
    Exec --> Registry[RPC 注册表与业务处理器]
    Exec --> Alloc[页粒度 CXL 分配器\n元数据位于服务端 DRAM]
    Registry -->|响应 / 缓冲区引用| CXL
    CXL --> API

四个运行时模块

模块 职责 关键做法
通信 传输请求与响应 每条连接使用单生产者、单消费者(SPSC)era 队列;16 B 固定消息,4 个请求可占用一个 64 B 缓存行;批量写回和跨队列预取降低 CXL 访问开销。
连接管理 建立和扩容队列 第一条连接通过可安全重试的幂等握手建立;已有连接上通过 RPC 扩容,避免用跨机原子操作协调连接创建。
RPC 执行 轮询队列、调度和调用处理器 多个服务端执行线程按队列负载工作;同一连接中的请求保持顺序。响应槽位允许多条有响应请求同时在途,而不会让服务端等待客户端回收旧响应。
内存分配 管理大请求使用的共享缓冲区 服务端集中管理、页粒度分配;客户端通过 RPC 请求分配/释放,不直接修改分配器元数据。两层 bitmap 同时支持小页和连续大块。

跨节点可见性

CXL-RPC 在数据发布前通过 clwb 写回修改,在读取远端发布的数据前通过 clflushopt 失效可能过期的本地缓存。队列使用递增 era 判断槽位是否已被重新发布,避免为每条消息频繁维护跨机可见的队头、队尾指针。该协议只依赖普通读写与显式缓存维护,不依赖跨节点原子操作。

默认构建会启用缓存维护。仅用于排查或兼容性实验时,可设置 -DCXL_RPC_DISABLE_CACHE_OPERATIONS=ON 关闭缓存预取、失效和写回操作。

三种请求传递路径

路径 适用数据 传输方式 缓存同步责任
内联(inline) 参数总大小不超过 14 B 的定长小请求 参数直接编码进 16 B Data 消息 运行时维护队列缓存行。
缓冲(buffered) 字符串、KV、一般变长大对象 Data 中传递 CXL 缓冲区相对偏移和精确长度,业务对象位于外部共享缓冲区 运行时按照 BufferDescriptor 的范围自动写回/失效。
间接(indirect) 已分散存储在 CXL 上的链表、超大对象、局部更新对象 请求仅传递引用/描述对象,业务数据无需再次打包 应用按需写回变更区域,并通过 INVALIDATE_CXL_REGION 请求远端刷新对应范围。

功能与特性

  • 面向 CXL 2.0 的共享内存 RPC 运行时,使用单一公共头文件根 include/cxl_rpc/...
  • 16 B 固定共享内存消息 ABI,内含 14 B 参数流和响应槽位标识。
  • 同步调用与异步提交/接收接口;每个连接支持 1–64 个响应槽位和有响应请求在途。
  • 按需创建和销毁 RPConnection;每条连接保证顺序执行,调用方不应并发推进同一连接。
  • 内置 CXL 分配、释放、缓存刷新和区域失效 RPC;默认分配粒度为 4 KiB,可配置为更大粒度。
  • 支持页粒度 CXL 共享缓冲区分配、复用、释放,以及 2 MiB 对齐映射。
  • 提供 buffered KV 与 indirect KV 两条端到端示例路径、交互式 KV 客户端、单元测试和吞吐量基准。
  • 包含作者独立复现并维护的 CXLock 对比实现,便于运行分配器吞吐量对比。

快速开始

环境要求

当前实现面向 Linux/x86-64:底层映射使用 mmap,缓存维护路径使用 x86 指令。建议使用 GCC 或 Clang,并满足以下要求:

  • CMake ≥ 3.14
  • 支持 C++20 的编译器
  • POSIX 线程库
  • zlib 开发包
  • Bash、truncate 与 GNU timeout(用于仓库中的运行脚本)

以 Debian/Ubuntu 为例:

sudo apt-get update
sudo apt-get install -y build-essential cmake zlib1g-dev

构建

git clone <repository-url> cxl-rpc
cd cxl-rpc

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel

常用的 CMake 选项:

选项 默认值 含义
CXL_PAGE_SIZE_OFFSET 12 分配器 chunk 大小的以 2 为底对数;默认 2^12 = 4 KiB。这是客户端/服务端共享 ABI 的一部分,两端必须一致。
CXL_ALIGN_SIZE_OFFSET 21 CXL 映射对齐单位的以 2 为底对数;默认 2^21 = 2 MiB
CXL_RPC_PAD_IMMEDIATE_REQUEST_WRITEBACK OFF 将立即可见或产生响应的请求填充到一个缓存行后再写回。
CXL_RPC_DISABLE_CACHE_OPERATIONS OFF 关闭运行时缓存维护,仅适合调试或特定实验。
CXL_RPC_KV_DEMO_TRACE OFF 输出交互式 KV 演示的请求流程。

例如,为 KV 演示增加请求跟踪:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCXL_RPC_KV_DEMO_TRACE=ON
cmake --build build --parallel

先运行不需要 CXL 设备的单元测试

这些测试锁定共享内存消息 ABI、缓冲对象编码和 RPC 注册逻辑,不映射 DAX 设备:

ctest --test-dir build --output-on-failure

单机 KV 演示

仓库提供的脚本会创建一个 2 GiB 的临时稀疏文件,启动服务端并执行 buffered KV 客户端,退出时自动清理文件和进程。单机 DRAM/文件映射不等价于真实 CXL 跨主机缓存行为。

# buffered KV 的完整 PUT/GET 示例
bash scripts/run_buffered_kv_demo.sh

在真实 CXL 平台上运行

将服务器与客户端分别部署到能够映射同一块 CXL 共享内存的主机。两端必须使用同一套 CMake ABI 参数,且就以下映射配置保持一致:

  • dax_dev_path:双方可访问同一物理 CXL 内存的 DAX 设备路径;
  • begin_offset:映射起始偏移;
  • data_len:RPC 数据区长度;
  • 队列和数据区需要满足默认的 2 MiB 映射对齐要求。

先复制并分别编辑 configs/demo.json,为不同主机赋予不同的 node_id,并把 dax_dev_path 改为实际 DAX 设备。例如,服务端:

./build/server -path configs/server.json --dax_dev_path=/dev/dax0.0

交互式 KV 客户端:

./build/kv_client -path configs/client.json --dax_dev_path=/dev/dax0.0

server 会先映射 RPC 元数据区,再从 begin_offset + cxl_meta_size() 映射数据区。客户端和服务端需要映射同一地址范围;只改变某一侧的 data_lenbegin_offset、页大小构建参数或协议注册顺序都会破坏互操作性。

使用生产设备前,请先确认该平台的 DAX/CXL 配置、访问权限、内存归属与缓存维护指令支持情况。示例脚本使用的是临时文件,不能替代真实跨主机一致性与性能验证。

示例与基准测试

所有端到端脚本都假定可执行文件已构建在 build/ 下,并且会自行创建 2 GiB 临时后备文件。基准的默认预热时间是 2 秒、测量时间是 5 秒;可在对应 JSON 中修改,或使用命令行 --key=value 覆盖配置项。

命令 工作负载 输出
bash scripts/run_buffered_kv_demo.sh 完整 buffered KV PUT/GET 示例成功或失败状态
bash scripts/run_malloc_throughput.sh 每工作线程分配 1024 个 4 KiB chunk 后批量释放 按在途数量报告 CXL-RPC malloc throughput(Mops)
bash scripts/run_buffered_kv_throughput.sh 64 B 定长值的 buffered KV PUT/GET 按客户端线程数输出吞吐量汇总
bash scripts/run_indirect_kv_throughput.sh 64 B 定长值的 indirect KV PUT/GET 按客户端线程数输出吞吐量汇总
bash scripts/run_allocator_sweep.sh CXL-RPC 与 CXLock 的分配器吞吐矩阵 logs/allocator_sweep_<timestamp>/throughput.csv

KV 线程数扫掠由 JSON 的 throughput_thread_counts 控制。也可以在命令行快速缩小范围:

# 仅执行 4 个客户端线程、128 B value 的 buffered KV 用例
bash scripts/run_buffered_kv_throughput.sh \
  --thread_count=4 \
  --buffered_kv_value_size=128

# 执行自定义线程扫掠的 indirect KV 用例
bash scripts/run_indirect_kv_throughput.sh \
  --throughput_thread_counts='[1,2,4]' \
  --indirect_kv_value_size=128

run_allocator_sweep.sh 的矩阵由 scripts/configs/allocator_sweep.conf 描述。

交互式 KV 演示

启动服务端后,可在一个或多个终端运行 kv_client;每个进程拥有独立的连接:

# 终端 1
./build/server -path configs/demo.json --dax_dev_path=/path/to/shared-memory

# 终端 2(可启动多个)
./build/kv_client -path configs/demo.json --dax_dev_path=/path/to/shared-memory

如需查看每次请求的客户端/服务端流程,在构建时开启 CXL_RPC_KV_DEMO_TRACE

应用接口

公共 API 位于 include/cxl_rpc/api/Requester.hpp,核心的调用生命周期如下:

  1. 通过 loadConfig(argc, argv) 读取 JSON 配置和 --key=value 覆盖项。
  2. 调用 cxl_init() 映射并初始化客户端运行时。
  3. 客户端和服务端以固定且相同的请求 ID、响应模式注册 RPC;独立部署的程序应使用 cxl_register_rpc_at(),不要依赖注册先后顺序。
  4. 调用 cxl_create_connection(max_inflight_count) 获取连接。
  5. 使用 cxl_call() 同步调用,或使用 cxl_send_async()cxl_receive() 分离提交和接收。
  6. 释放应用缓冲区、销毁连接,并调用 cxl_destroy()

一个内联请求通过 DataWriter 向消息的 14 B 参数区写入定长、平凡可复制的数据。服务端使用 RpcRegistry 在工作线程启动前注册相同 ID 的处理器。

#include <cxl_rpc/api/Requester.hpp>
#include <cxl_rpc/support/config/Config.hpp>

loadConfig(argc, argv);
cxl_init();

constexpr uint8_t kMyRpc = 20;
if (!cxl_register_rpc_at(kMyRpc, /* needs_response = */ true)) {
    // 客户端/服务端的协议定义不一致
}

RPConnection* conn = cxl_create_connection(/* max_inflight_count = */ 2);
Data request;
request.set_reqstate(kMyRpc);
auto writer = request.writer();
writer.write(uint32_t{42});

if (conn != nullptr && cxl_call(conn, request)) {
    // request 现在携带服务端返回的 Data
}

cxl_destroy_connection(conn);
cxl_destroy();

重要约束:

  • 同一 RPConnection 不可由多个线程并发推进;需要并行提交时,为工作线程创建独立连接。
  • max_inflight_count 必须为 1–64。异步请求返回的 slot 在 cxl_receive() 前一直被占用。
  • 自定义请求 ID 使用低 6 bit,可用类型总数为 64;内置运行时请求已占用一部分编号。
  • Data 是精确的 16 B 共享内存 ABI;内联参数容量为 14 B。大对象应使用 BufferDescriptorRpcBufferWriter/RpcBufferReader 或项目中的 typed adapter。
  • 对有复杂对象或分散数据的请求,选择 buffered 或 indirect 路径,并遵循相应的缓存可见性约定。

大对象与 KV adapter

include/cxl_rpc/examples/BufferedKvClient.hpp 提供了 typed buffered KV API:

  • cxl_rpc_buffered_kv() / cxl_rpc_buffered_kv_get() 提交 PUT/GET;
  • cxl_rpc_receive_buffered_kv() 接收状态和值;
  • cxl_rpc_release_buffered_kv_buffer() 回收 adapter 管理的 CXL 缓冲区。

include/cxl_rpc/examples/IndirectKvClient.hpp 提供对应的 indirect KV API。前者把完整 KV body 置于一个缓冲区中,后者将实际 KV body 与引用它的 envelope 分离,适用于业务对象本来就驻留于 CXL 内存的情形。两种 adapter 在匹配的 receive 返回前都不能复用传入的缓冲区描述符。

配置说明

JSON 配置存放在 configs/。程序使用 -path <config.json> 指定配置文件,随后可使用 --key=value 覆盖其中字段。例如:

./build/malloc_throughput -path configs/malloc_throughput.json \
  --dax_dev_path=/dev/dax0.0 \
  --thread_count=8 \
  --max_qp_count=9 \
  --throughput_measure_seconds=30

常用字段如下:

字段 作用
dax_dev_path 共享 CXL/DAX 设备或测试后备文件路径。
begin_offsetdata_len 映射起点和 RPC 数据区大小;两端必须一致。
node_idnode_count 节点标识及节点总数;不同主机应配置不同 node_id
thread_count 客户端工作线程数(具体测试使用方式见测试程序)。
max_qp_count 服务端可管理的最大连接/队列数;至少覆盖并发客户端连接数。
server_worker_count 服务端正常执行线程数。
cxl_queue_depth 每条 CXL 队列的深度。
udp_buffer_slots_count 初次连接幂等握手使用的槽位数;名称源自历史实现,并不表示使用 UDP 网络传输。
throughput_warmup_secondsthroughput_measure_seconds 基准预热和测量时长。
throughput_thread_counts buffered/indirect KV 脚本的客户端线程数扫掠。
malloc_throughput_inflight_counts 分配器基准中每连接的异步在途数扫掠。

工程结构

.
├── include/cxl_rpc/            # 公共头文件与模块接口
│   ├── api/                    # Requester 客户端 API
│   ├── runtime/                # 内存、传输与 RPC 抽象
│   ├── examples/               # Buffered/Indirect KV typed API
│   └── platform/               # DAX 映射与模拟设备抽象
├── src/
│   ├── api/                    # 客户端请求提交与响应接收
│   ├── runtime/                # 分配器、队列和 RPC 运行时
│   ├── server/                 # 服务端 daemon、调度与 KV handler
│   └── examples/               # KV client adapter 实现
├── examples/buffered_kv/       # 完整及交互式 KV 示例
├── tests/                      # 单元、集成和吞吐基准测试
├── configs/                    # 可直接使用的 JSON 配置
├── scripts/                    # 端到端运行、KV 和分配器扫掠脚本
├── cxlock/                     # 作者复现并维护的 CXLock 对比实现
├── third_party/                # nlohmann/json 与 HdrHistogram_c
├── 项目说明书.docx             # 完整设计、测试和限制说明
├── 作品介绍 PPT.pptx           # 项目答辩材料
└── 演示视频.mp4                # 演示视频

主要构建目标包括:

目标 用途
server RPC 服务端,注册运行时 RPC 与 KV 示例服务。
allocator_correctness 页分配器正确性测试程序。
malloc_throughput 小 RPC/分配器吞吐基准。
buffered_kv_throughputindirect_kv_throughput 两条大对象路径的 KV 吞吐基准。
buffered_kv_demokv_client 完整和交互式 KV 示例。
wire_format_testbuffered_object_testrpc_registry_test 不依赖 DAX 设备的单元测试。

实验结果与边界

项目说明书记录了在 4 节点 CXL 2.0 平台及 DRAM 验证环境上的实验。以下数字是特定硬件、线程配置、工作负载和比较基线下的结果,不应视为对任意平台或业务负载的通用承诺:

  • 在 3 个客户端节点、共 192 个客户端线程的混合队列传输测试中,CXL-RPC 使用 4 个执行线程,相较使用 12 个执行线程的 CXLock 基线,说明书报告吞吐提升 3.2×
  • 在同一规模的纯单向传输测试中,CXL-RPC 使用约 1/2 的服务端计算资源,说明书报告吞吐达到基线的 75×
  • 对 16 B 请求,发送端将写回批量从 1 个请求增至 4 个请求时,说明书报告约 12× 的写回吞吐提升;跨队列预取相对无预取方案可达约
  • 分配器和 KV 的 RPC 路径实验用于验证请求执行、响应回收、buffered 与 indirect 路径的功能和相对趋势。说明书明确指出其中 DRAM 环境的绝对性能不能等同于真实 CXL 平台表现。

更完整的架构图、测试方法和结果讨论请参阅 项目说明书.docx作品介绍 PPT.pptx;可运行演示位于 演示视频.mp4

依赖与致谢

CXL-RPC 的核心运行时、传输协议、连接管理、分配器与示例代码均在本仓库中实现。cxlock/ 中的 CXLock 是根据相关设计资料独立复现、整理并维护的对比实现,与 cxl_rpc_runtime 独立构建。项目使用以下第三方组件:

关于
138.3 MB
邀请码
    Gitlink(确实开源)
  • 加入我们
  • 官网邮箱:gitlink@ccf.org.cn
  • QQ群
  • QQ群
  • 公众号
  • 公众号

版权所有:中国计算机学会技术支持:开源发展技术委员会
京ICP备13000930号-9 京公网安备 11010802047560号