重新添加压缩包
面向无一致性跨机共享内存的高性能 RPC 框架
CXL-RPC 是一个面向 CXL 2.0 跨主机共享内存 的 C++20 RPC 原型系统。它针对当前真实 CXL 平台通常不具备跨主机硬件缓存一致性、也不能依赖跨节点 CAS/FAA 等原子读改写操作的现实约束,使用显式缓存维护、无原子 SPSC 队列和共享内存偏移量引用来完成请求传输、远程执行和大对象访问。
与把 CXL 内存当作不可缓存消息通道的方案不同,CXL-RPC 让请求和缓冲区在必要时保留在本地 CPU 缓存中,并在跨节点交接点进行显式写回与失效;与传统网络 RPC 相比,大对象可通过共享 CXL 缓冲区按引用传递,避免反复序列化和复制。
本项目是“中国研究生操作系统开源创新大赛”参赛作品,相关的说明书、答辩材料与演示视频位于仓库根目录。
传统 RPC 即使建立在 TCP、RDMA 或用户态网络之上,请求通常仍要经历序列化、协议处理、网络传输、反序列化以及一次或多次数据复制。CXL 等跨机互连技术使多台主机能够映射同一片物理内存,为 RPC 绕开这条消息传递路径提供了新的基础。
不过,真实 CXL 2.0 平台的共享内存并不等于“多机 std::atomic 可用的普通共享内存”:跨主机缓存并不自动一致,且不能假设跨节点原子读改写可用于队列协调。因此,CXL-RPC 的设计目标是:
std::atomic
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
era
CXL-RPC 在数据发布前通过 clwb 写回修改,在读取远端发布的数据前通过 clflushopt 失效可能过期的本地缓存。队列使用递增 era 判断槽位是否已被重新发布,避免为每条消息频繁维护跨机可见的队头、队尾指针。该协议只依赖普通读写与显式缓存维护,不依赖跨节点原子操作。
clwb
clflushopt
默认构建会启用缓存维护。仅用于排查或兼容性实验时,可设置 -DCXL_RPC_DISABLE_CACHE_OPERATIONS=ON 关闭缓存预取、失效和写回操作。
-DCXL_RPC_DISABLE_CACHE_OPERATIONS=ON
Data
BufferDescriptor
INVALIDATE_CXL_REGION
include/cxl_rpc/...
RPConnection
当前实现面向 Linux/x86-64:底层映射使用 mmap,缓存维护路径使用 x86 指令。建议使用 GCC 或 Clang,并满足以下要求:
mmap
truncate
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
2^12 = 4 KiB
CXL_ALIGN_SIZE_OFFSET
21
2^21 = 2 MiB
CXL_RPC_PAD_IMMEDIATE_REQUEST_WRITEBACK
OFF
CXL_RPC_DISABLE_CACHE_OPERATIONS
CXL_RPC_KV_DEMO_TRACE
例如,为 KV 演示增加请求跟踪:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCXL_RPC_KV_DEMO_TRACE=ON cmake --build build --parallel
这些测试锁定共享内存消息 ABI、缓冲对象编码和 RPC 注册逻辑,不映射 DAX 设备:
ctest --test-dir build --output-on-failure
仓库提供的脚本会创建一个 2 GiB 的临时稀疏文件,启动服务端并执行 buffered KV 客户端,退出时自动清理文件和进程。单机 DRAM/文件映射不等价于真实 CXL 跨主机缓存行为。
# buffered KV 的完整 PUT/GET 示例 bash scripts/run_buffered_kv_demo.sh
将服务器与客户端分别部署到能够映射同一块 CXL 共享内存的主机。两端必须使用同一套 CMake ABI 参数,且就以下映射配置保持一致:
dax_dev_path
begin_offset
data_len
先复制并分别编辑 configs/demo.json,为不同主机赋予不同的 node_id,并把 dax_dev_path 改为实际 DAX 设备。例如,服务端:
configs/demo.json
node_id
./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_len、begin_offset、页大小构建参数或协议注册顺序都会破坏互操作性。
server
begin_offset + cxl_meta_size()
使用生产设备前,请先确认该平台的 DAX/CXL 配置、访问权限、内存归属与缓存维护指令支持情况。示例脚本使用的是临时文件,不能替代真实跨主机一致性与性能验证。
所有端到端脚本都假定可执行文件已构建在 build/ 下,并且会自行创建 2 GiB 临时后备文件。基准的默认预热时间是 2 秒、测量时间是 5 秒;可在对应 JSON 中修改,或使用命令行 --key=value 覆盖配置项。
build/
--key=value
bash scripts/run_buffered_kv_demo.sh
bash scripts/run_malloc_throughput.sh
CXL-RPC malloc throughput
bash scripts/run_buffered_kv_throughput.sh
bash scripts/run_indirect_kv_throughput.sh
bash scripts/run_allocator_sweep.sh
logs/allocator_sweep_<timestamp>/throughput.csv
KV 线程数扫掠由 JSON 的 throughput_thread_counts 控制。也可以在命令行快速缩小范围:
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 描述。
run_allocator_sweep.sh
scripts/configs/allocator_sweep.conf
启动服务端后,可在一个或多个终端运行 kv_client;每个进程拥有独立的连接:
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,核心的调用生命周期如下:
include/cxl_rpc/api/Requester.hpp
loadConfig(argc, argv)
cxl_init()
cxl_register_rpc_at()
cxl_create_connection(max_inflight_count)
cxl_call()
cxl_send_async()
cxl_receive()
cxl_destroy()
一个内联请求通过 DataWriter 向消息的 14 B 参数区写入定长、平凡可复制的数据。服务端使用 RpcRegistry 在工作线程启动前注册相同 ID 的处理器。
DataWriter
RpcRegistry
#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();
重要约束:
max_inflight_count
RpcBufferWriter
RpcBufferReader
include/cxl_rpc/examples/BufferedKvClient.hpp 提供了 typed buffered KV API:
include/cxl_rpc/examples/BufferedKvClient.hpp
cxl_rpc_buffered_kv()
cxl_rpc_buffered_kv_get()
cxl_rpc_receive_buffered_kv()
cxl_rpc_release_buffered_kv_buffer()
include/cxl_rpc/examples/IndirectKvClient.hpp 提供对应的 indirect KV API。前者把完整 KV body 置于一个缓冲区中,后者将实际 KV body 与引用它的 envelope 分离,适用于业务对象本来就驻留于 CXL 内存的情形。两种 adapter 在匹配的 receive 返回前都不能复用传入的缓冲区描述符。
include/cxl_rpc/examples/IndirectKvClient.hpp
JSON 配置存放在 configs/。程序使用 -path <config.json> 指定配置文件,随后可使用 --key=value 覆盖其中字段。例如:
configs/
-path <config.json>
./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
常用字段如下:
node_count
thread_count
max_qp_count
server_worker_count
cxl_queue_depth
udp_buffer_slots_count
throughput_warmup_seconds
throughput_measure_seconds
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 # 演示视频
主要构建目标包括:
allocator_correctness
malloc_throughput
buffered_kv_throughput
indirect_kv_throughput
buffered_kv_demo
wire_format_test
buffered_object_test
rpc_registry_test
项目说明书记录了在 4 节点 CXL 2.0 平台及 DRAM 验证环境上的实验。以下数字是特定硬件、线程配置、工作负载和比较基线下的结果,不应视为对任意平台或业务负载的通用承诺:
更完整的架构图、测试方法和结果讨论请参阅 项目说明书.docx 与 作品介绍 PPT.pptx;可运行演示位于 演示视频.mp4。
项目说明书.docx
作品介绍 PPT.pptx
演示视频.mp4
CXL-RPC 的核心运行时、传输协议、连接管理、分配器与示例代码均在本仓库中实现。cxlock/ 中的 CXLock 是根据相关设计资料独立复现、整理并维护的对比实现,与 cxl_rpc_runtime 独立构建。项目使用以下第三方组件:
cxlock/
cxl_rpc_runtime
third_party/nlohmann/json.hpp
third_party/HdrHistogram_c
版权所有:中国计算机学会技术支持:开源发展技术委员会 京ICP备13000930号-9 京公网安备 11010802047560号
CXL-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 的设计目标是:核心设计
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四个运行时模块
era队列;16 B 固定消息,4 个请求可占用一个 64 B 缓存行;批量写回和跨队列预取降低 CXL 访问开销。跨节点可见性
CXL-RPC 在数据发布前通过
clwb写回修改,在读取远端发布的数据前通过clflushopt失效可能过期的本地缓存。队列使用递增era判断槽位是否已被重新发布,避免为每条消息频繁维护跨机可见的队头、队尾指针。该协议只依赖普通读写与显式缓存维护,不依赖跨节点原子操作。三种请求传递路径
Data消息Data中传递 CXL 缓冲区相对偏移和精确长度,业务对象位于外部共享缓冲区BufferDescriptor的范围自动写回/失效。INVALIDATE_CXL_REGION请求远端刷新对应范围。功能与特性
include/cxl_rpc/...。RPConnection;每条连接保证顺序执行,调用方不应并发推进同一连接。快速开始
环境要求
当前实现面向 Linux/x86-64:底层映射使用
mmap,缓存维护路径使用 x86 指令。建议使用 GCC 或 Clang,并满足以下要求:truncate与 GNUtimeout(用于仓库中的运行脚本)以 Debian/Ubuntu 为例:
构建
常用的 CMake 选项:
CXL_PAGE_SIZE_OFFSET122^12 = 4 KiB。这是客户端/服务端共享 ABI 的一部分,两端必须一致。CXL_ALIGN_SIZE_OFFSET212^21 = 2 MiB。CXL_RPC_PAD_IMMEDIATE_REQUEST_WRITEBACKOFFCXL_RPC_DISABLE_CACHE_OPERATIONSOFFCXL_RPC_KV_DEMO_TRACEOFF例如,为 KV 演示增加请求跟踪:
先运行不需要 CXL 设备的单元测试
这些测试锁定共享内存消息 ABI、缓冲对象编码和 RPC 注册逻辑,不映射 DAX 设备:
单机 KV 演示
仓库提供的脚本会创建一个 2 GiB 的临时稀疏文件,启动服务端并执行 buffered KV 客户端,退出时自动清理文件和进程。单机 DRAM/文件映射不等价于真实 CXL 跨主机缓存行为。
在真实 CXL 平台上运行
将服务器与客户端分别部署到能够映射同一块 CXL 共享内存的主机。两端必须使用同一套 CMake ABI 参数,且就以下映射配置保持一致:
dax_dev_path:双方可访问同一物理 CXL 内存的 DAX 设备路径;begin_offset:映射起始偏移;data_len:RPC 数据区长度;先复制并分别编辑
configs/demo.json,为不同主机赋予不同的node_id,并把dax_dev_path改为实际 DAX 设备。例如,服务端:交互式 KV 客户端:
server会先映射 RPC 元数据区,再从begin_offset + cxl_meta_size()映射数据区。客户端和服务端需要映射同一地址范围;只改变某一侧的data_len、begin_offset、页大小构建参数或协议注册顺序都会破坏互操作性。示例与基准测试
所有端到端脚本都假定可执行文件已构建在
build/下,并且会自行创建 2 GiB 临时后备文件。基准的默认预热时间是 2 秒、测量时间是 5 秒;可在对应 JSON 中修改,或使用命令行--key=value覆盖配置项。bash scripts/run_buffered_kv_demo.shbash scripts/run_malloc_throughput.shCXL-RPC malloc throughput(Mops)bash scripts/run_buffered_kv_throughput.shbash scripts/run_indirect_kv_throughput.shbash scripts/run_allocator_sweep.shlogs/allocator_sweep_<timestamp>/throughput.csvKV 线程数扫掠由 JSON 的
throughput_thread_counts控制。也可以在命令行快速缩小范围:run_allocator_sweep.sh的矩阵由scripts/configs/allocator_sweep.conf描述。交互式 KV 演示
启动服务端后,可在一个或多个终端运行
kv_client;每个进程拥有独立的连接:如需查看每次请求的客户端/服务端流程,在构建时开启
CXL_RPC_KV_DEMO_TRACE。应用接口
公共 API 位于
include/cxl_rpc/api/Requester.hpp,核心的调用生命周期如下:loadConfig(argc, argv)读取 JSON 配置和--key=value覆盖项。cxl_init()映射并初始化客户端运行时。cxl_register_rpc_at(),不要依赖注册先后顺序。cxl_create_connection(max_inflight_count)获取连接。cxl_call()同步调用,或使用cxl_send_async()与cxl_receive()分离提交和接收。cxl_destroy()。一个内联请求通过
DataWriter向消息的 14 B 参数区写入定长、平凡可复制的数据。服务端使用RpcRegistry在工作线程启动前注册相同 ID 的处理器。重要约束:
RPConnection不可由多个线程并发推进;需要并行提交时,为工作线程创建独立连接。max_inflight_count必须为 1–64。异步请求返回的 slot 在cxl_receive()前一直被占用。Data是精确的 16 B 共享内存 ABI;内联参数容量为 14 B。大对象应使用BufferDescriptor、RpcBufferWriter/RpcBufferReader或项目中的 typed adapter。大对象与 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覆盖其中字段。例如:常用字段如下:
dax_dev_pathbegin_offset、data_lennode_id、node_countnode_id。thread_countmax_qp_countserver_worker_countcxl_queue_depthudp_buffer_slots_countthroughput_warmup_seconds、throughput_measure_secondsthroughput_thread_countsmalloc_throughput_inflight_counts工程结构
主要构建目标包括:
serverallocator_correctnessmalloc_throughputbuffered_kv_throughput、indirect_kv_throughputbuffered_kv_demo、kv_clientwire_format_test、buffered_object_test、rpc_registry_test实验结果与边界
项目说明书记录了在 4 节点 CXL 2.0 平台及 DRAM 验证环境上的实验。以下数字是特定硬件、线程配置、工作负载和比较基线下的结果,不应视为对任意平台或业务负载的通用承诺:
更完整的架构图、测试方法和结果讨论请参阅
项目说明书.docx与作品介绍 PPT.pptx;可运行演示位于演示视频.mp4。依赖与致谢
CXL-RPC 的核心运行时、传输协议、连接管理、分配器与示例代码均在本仓库中实现。
cxlock/中的 CXLock 是根据相关设计资料独立复现、整理并维护的对比实现,与cxl_rpc_runtime独立构建。项目使用以下第三方组件:third_party/nlohmann/json.hpp;third_party/HdrHistogram_c;