共享内存
Aether 中的实时值不会通过热路径上的代理或数据库传输。 io(通信服务)和自动化(模型/规则服务)共享 IO 拥有的点段以及单独的通道运行状况段,并通过 Unix 域套接字交换固定大小的通知。一个小的提交见证人证明这两个段来自相同的物理拓扑发布。设备读取数据会在数十纳秒内进入共享内存。本页描述了该段本身以及构建在其之上的两个基于套接字的信令平面。有关其周围的服务,请参阅架构;有关值的含义,请参阅数据模型。
事实来源:crates/aether-dataplane/(物理标头、槽、锁定)、extensions/shm-bridge/(类型化清单、点/运行状况发布和自我修复读取器)和 services/io/src/core/channels/shm_listener.rs(命令侦听器)。通过 v4 滚动兼容性门后,旧版 SHM 聚合箱已被删除。
该段是一个文件:一个 64 字节标头,后跟一个固定大小的 32 字节点槽数组(crates/aether-dataplane/src/core/header.rs 中的 calculate_file_size 恰好是 64 + 32 × max_slots;默认容量为 100,000 个槽)。两个结构体大小均在编译时断言。
文件路径由 default_shm_path() (crates/aether-dataplane/src/core/config.rs) 按以下顺序解析:
AETHER_SHM_PATH环境变量(如果已设置)。/shm/rtdb/aether-rtdb.shm,如果/shm/rtdb目录存在(Docker 部署挂载共享 tmpfs 卷)- Linux 上的
/dev/shm/aether-rtdb.shm(RAM 支持的 tmpfs)。 /tmp/aether-rtdb.shm,否则(macOS 开发)。
标头(UnifiedHeader、#[repr(C, align(64))])包含:幻数、布局版本、max_slots、实时 slot_count、最后更新时间戳、写入器心跳、routing_hash(通道/点布局的指纹)、writer_generation(化身计数器)和 publication_epoch(公共点/健康发布身份)。所有多字节字段都使用本机字节序,因此读取器和写入器必须在同一架构上运行。
每个 PointSlot 都包含一个工程值(f64 位)、一个原始值(f64 位)、一个毫秒时间戳、一个 seqlock 序列计数器和一个脏标志。从未被写入的槽在两个值字段中都保留一个安静的 NaN 标记 - 未写入的槽是自描述的,永远不会与真实设备的零读数混淆。下游消费者在 is_finite 上进行过滤。
槽通过统一索引进行寻址。每个进程独立地从相同的不可变 ChannelPointManifest 派生相同的 (channel_id, point_type, point_id) → slot 映射。协议通过清单 routing_hash、确切的槽计数、提交的发布纪元和写入器生成进行验证。逻辑测量/动作路由和协议寄存器映射不参与物理槽布局。
写入器所有权是类型强制的
“写入器所有权是类型强制的”标题的链接通道点有四种槽类型:遥测 (T) 和信号 (S) 是测量端;控制(C)和调整(A)是动作侧。所有权规则为:
- io 获取拥有 T/S 插槽。
ShmAcquisitionStateWriter仅接受类型化的AcquiredPointSample批次,并在任何突变之前拒绝 C/A 地址。 - 受控命令调度镜像 C/A 插槽。
ShmDeviceCommandSink解析一个类型化的物理目标,检查镜像前后的写入器生成,并将完整的命令帧发送到 io。不能写T/S地址。
保护主要在分机端口边界;原始插槽索引写入保留在物理适配器内。运行时检查为清单成员资格、槽边界、稳定生成和规范文件身份提供深度防御。
ShmReadTopologyGeneration 提供生产读取视图。它将点和健康状况绑定到一个提交见证人并固定两个作家世代;调试工具仍可能显式打开单个物理段。
一致性:seqlock
“一致性:seqlock”标题的链接每个槽都受到每个槽 seqlock 的保护:编写器将序列计数器更改为奇数值,写入三个数据字段,然后将其更改回偶数。读取器读取序列,读取数据,然后重新读取序列;仅当两次读取返回相同的偶数值时,快照才有效。内存排序在读取端使用成对的 Acquire 栅栏,在写入端使用 Release 栅栏加上 Release 增量 — crates/aether-dataplane/src/core/slot.rs 中的注释解释了为什么在 AArch64 上单个 Acquire 加载不足。
存在两个读取入口点,选择正确的入口点很重要:
try_load_consistent()— 在任何争用(奇数序列或序列)上返回None的单次尝试改变)。这是在异步运行时工作线程上运行的任务的变体:永远不要在 tokio 工作线程上自旋。load_consistent()— 使用自旋提示重试try_load_consistent最多 32,768 次,在极端争用情况下将最坏情况的自旋限制在大约 3–16 毫秒。它适用于专用线程。当重试用尽时,它会记录一条警告并返回None- 它永远不会返回撕裂的数据。
在生产中,重试路径几乎从不迭代:写入之间的协议 I/O 意味着读取器很少与正在进行的写入发生冲突。
生成和重建
“生成和重建”标题的链接三种身份让读取器检测到他们的视图是stale:
routing_hash是通道点布局的指纹。 io 在创建时写入;每个协调的开放路径都会根据本地配置重新计算自己的指纹,并在不匹配时拒绝打开——否则槽索引将默默地指向错误的点。错误消息告诉操作员重新启动io以重新同步。writer_generation标识作者化身。它在创建时从挂钟纳秒结合每个进程的随机数、强制偶数和非零:不变量是“静止时偶数,重新配置运行时奇数”,因此读者将自己排除在奇数值之外。命令/读取适配器会比较每个操作的生成,并检测未赶上的 io 重新启动或重新配置。publication_epoch+ 提交见证将点和运行状况文件绑定到一个已完成的 IO 事务。见证人还记录哈希值、计数和写入器生成。缺失、部分、损坏或混合的出版物可重试失败;读者永远不会根据相同的哈希值进行猜测。
重新配置永远不会改变实时布局。 ShmWriterHandle 和 ShmChannelHealthWriterHandle 构建完整的暂存文件,并通过其规范路径自动重命名它们,同时保留一个跨平面发布租约。提交见证最后被重命名,并且是线性化点。保留的 mmap 被奇怪的写入者一代所围住;自我修复的读取器可能仅重新打开由其服务级别拓扑固定的纪元和写入器生成。 History 和 Uplink 将其 SQLite 路由和提交的 SHM 读取视图替换为一个 Arc,因此集合传递不能混合逻辑代和物理代。崩溃孤立的暂存文件在恢复时受到限制和清理。
命令通知
“命令通知”标题的链接当自动化发出命令时 - 规则操作或 HTTP 控制请求(请参阅应用程序和代理的安全操作 了解允许到达设备的内容) - ShmDeviceCommandSink 将 C/A 值镜像到固定写入器生成中,并通过 Unix 域套接字发送通知(/tmp/aether-m2c.sock) 因此 io 立即做出反应而不是轮询。在测量中,通知路径是亚毫秒级的; ~1–2 毫秒是调度代码记录的快乐路径的设计预算。
通知 (DeviceCommandFrame) 是一个固定的 56 字节帧,承载路由目标(通道、点类型、点)、命令负载(值位加上发出和到期时间戳)和生产者排序(producer_id,每次自动化重启时都会更改的每个化身 ID,加上单调seq)。因为帧携带完整的命令,所以 io 永远不必读回插槽 - 并且对同一点的两次快速写入作为两个事件到达,而不是合并为一个事件。
io 的 ShmCommandListener 绑定套接字,立即将其限制为模式 0600(如果失败则拒绝侦听 — 任何可以编写此套接字的人都可以注入设备命令),并对每个点的传入事件进行重复数据删除:不同的 producer_id 始终重置状态(自动重启),而在同一生产者中,使用包装序列比较 (seq.wrapping_sub(last_seq) > u64::MAX / 2) 将帧视为过时或重复而丢弃。过期的帧在排队之前被丢弃。然后,统一通道任务再次根据配置的可写点(包括最小值/最大值)检查该值,并在调用协议适配器之前立即执行步骤。未知点、无效点约束、NaN/无穷大以及批次中被拒绝的成员都会使整个命令失败,而无需接触硬件。在发送端,ShmNotifier 重试失败的写入三次,然后将自身标记为断开连接,并以指数退避(1 秒加倍到 5 秒上限)重新连接。没有轮询回退:如果套接字保持关闭状态,则通知结果会报告降级的传递,并且调用者决定要显示的内容。
PointWatch 事件平面
“PointWatch 事件平面”标题的链接命令流自动化 → io; PointWatch 是相反的方向,它使规则引擎成为事件驱动的(请参阅规则引擎)。每次 T/S 槽写入后,io 都会查阅订阅位图 — 一个覆盖所有槽的原子 u64 字的独立 12,504 字节 mmap 文件(aether-rtdb-point-watch-subs.shm,位于主段旁边)。 io 在启动时将其创建为零填充;自动化在加载或重新加载规则时设置位。热路径检查是单个宽松的原子负载和位测试,大约 1–2 ns,常见情况(槽未订阅)立即返回。
命中时,io 构建 56 字节 PointWatchEvent — 通道、点、点类型、值位、原始位、槽索引、时间戳、生产者 ID — 并将其推送到由后台任务耗尽的有界进程内通道(容量 2048)每次写入专用套接字时最多可批量处理 64 个事件(/tmp/aether-point-watch-automation.sock、aether-automation 侦听、aether-io 连接、与命令平面相同的 1-5 秒重新连接退避)。由于事件本身带有值,因此自动化可以直接根据事件评估死区,而无需回读;重复事件是无害的(最坏的情况是额外的死区检查),这就是帧没有序列字段的原因。
在自动化方面,管道保持端到端有界:侦听器将帧转发到 1024 容量通道,调度程序(libs/aether-rules/ 中的 PointWatchDispatcher)映射 (channel, point) → rule IDs 并将唤醒事件转发到调度程序自己的 1024 容量通道。每个阶段都使用非阻塞try_send;溢出时,事件将被丢弃,并且 dropped_count 计数器会递增,而不是阻塞 io 的写入路径。丢弃的事件由规则引擎的周期性滴答恢复,因此过载会降级为旧的轮询延迟,而不是失去正确性。
在生产硬件(Cortex-A55 @ 1.4 GHz、ECU-1170)上针对初始 PointWatch 基准测试测量的回报:P50 处的点更改到事件传递延迟为 206 µs,P99 处为 526 µs(规则评估带来累积数字)到约 215 µs P50 — 请参阅数据流),而之前的 Redis-tick 模型下为 50–150 毫秒 — 中位数大约提高了 500 倍。
相关页面
“相关页面”标题的链接- 架构 — 共享此段的服务
- 数据模型 — T/S/C/A 值的含义和 NaN 哨兵
- 数据流 — 上行链路/下行链路路径和延迟预算
- 规则引擎 — PointWatch 事件的使用方
- 应用程序和应用程序的安全操作Agents — 写入到达设备