126 lines
6.6 KiB
Markdown
126 lines
6.6 KiB
Markdown
|
|
---
|
|||
|
|
tags: []
|
|||
|
|
date created: 星期三, 十一月 26日 2025, 9:49:46 晚上
|
|||
|
|
date modified: 星期三, 十一月 26日 2025, 10:06:18 晚上
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 2.6.2 多级数据打点策略 (Multi-Level Timestamping Strategy)
|
|||
|
|
|
|||
|
|
**基线核心宗旨**:**“As Early As Possible (越早越好)”**。
|
|||
|
|
时间戳必须尽可能在物理层(PHY)或链路层(MAC)生成,以消除操作系统中断调度、驱动处理和协议栈拷贝带来的**不定长抖动(Jitter)**。
|
|||
|
|
|
|||
|
|
-----
|
|||
|
|
|
|||
|
|
## 1. 约束输入与对齐 (Constraints & Alignment)
|
|||
|
|
|
|||
|
|
基于 2.1.1 审计结果(网迅 WX1860AL4 网卡)和 2.6.1 确立的时钟基线,我们需要对齐以下约束:
|
|||
|
|
|
|||
|
|
1. **硬件能力 (Hardware Cap)**:
|
|||
|
|
- **网迅 NIC**:需确认驱动是否支持 `SO_TIMESTAMPING` 接口读取硬件 RX 时间戳。通常企业级网卡(Intel X710/Mellanox)均支持,国产网卡需实测验证。若不支持,需回退到内核软打点。
|
|||
|
|
2. **协议支持 (Protocol)**:
|
|||
|
|
- UDP 数据包本身不携带“发送时间”(除非应用层协议写了)。因此我们依赖的是**接收端打点 (Ingress Timestamping)**。
|
|||
|
|
3. **数据结构关联**:
|
|||
|
|
- 生成的纳秒级时间戳必须填入 `RawDataPacket` 的 Header,并最终映射到 Protobuf 的 `timestamp_us`。
|
|||
|
|
|
|||
|
|
-----
|
|||
|
|
|
|||
|
|
## 2. 权衡分析与选项呈现 (Trade-off Matrix)
|
|||
|
|
|
|||
|
|
我们按照“离物理线路的距离”定义三个打点层级:
|
|||
|
|
|
|||
|
|
| 选项 | A. 用户态打点 (User-space) | B. 内核软打点 (Kernel SW) **(基线)** | C. 硬件硬打点 (Hardware HW) **(理想)** |
|
|||
|
|
| :--- | :--- | :--- | :--- |
|
|||
|
|
| **生成位置** | `recvmsg()` 返回后的应用层代码。 | 网卡驱动收到中断,SKB (Socket Buffer) 创建时刻。 | 网卡 PHY 芯片接收到前导码(Preamble)时刻。 |
|
|||
|
|
| **抖动来源** | **极大**。受 CPU 调度、软中断处理、内存拷贝影响,抖动可达 10-100µs。 | **中等**。受中断响应延迟影响,抖动约 1-10µs。 | **极小**。几乎无抖动 (\< 100ns)。 |
|
|||
|
|
| **PTP 依赖** | 依赖系统时钟 (`CLOCK_REALTIME`)。 | 依赖系统时钟。 | 依赖网卡 PHC (PTP Hardware Clock) 与系统时钟的同步 (`phc2sys`)。 |
|
|||
|
|
| **实现复杂度**| 低。调用 `Clock::now()` 即可。 | 中。需配置 Socket 选项并解析辅助数据 (`CMSG`)。 | 高。需硬件支持,且需处理 PHC 到 UTC 的转换。 |
|
|||
|
|
| **适用场景** | 功能调试、非实时业务。 | **当前国产环境最稳妥的基线**。 | 相控阵雷达、高频交易。 |
|
|||
|
|
|
|||
|
|
-----
|
|||
|
|
|
|||
|
|
## 3. 基线确立与实施规范
|
|||
|
|
|
|||
|
|
考虑到国产网卡驱动的成熟度风险,我们确立 **“优先硬件,兜底内核,严禁用户态”** 的分级策略。
|
|||
|
|
|
|||
|
|
### 3.1 策略分级定义 (Hierarchy Definition)
|
|||
|
|
|
|||
|
|
- **Priority 1 (HW)**: 尝试启用 **`SO_TIMESTAMPING_RX_HARDWARE`**。
|
|||
|
|
- 如果网卡支持,这是绝对真值。
|
|||
|
|
- *注意*:硬件时间戳通常是 PHC 时间(TAI),需在用户态根据 `ptp4l` 的 offset 转换为 UTC。
|
|||
|
|
- **Priority 2 (SW)**: 回退到 **`SO_TIMESTAMPNS`** (内核接收时间)。
|
|||
|
|
- 这是**工程基线**。它反映了数据包进入 Linux 网络栈的第一时间点,消除了应用程序调度的延迟。
|
|||
|
|
- **Priority 3 (App)**: **严禁作为生产标准**。仅在上述两者皆失败时,使用 2.6.1 定义的 `HighPrecisionClock::now()` 补救,并标记数据质量为 `LOW_PRECISION`。
|
|||
|
|
|
|||
|
|
### 3.2 实现规范:辅助数据解析 (CMSG Parsing)
|
|||
|
|
|
|||
|
|
在 `DataReceiver` 的 I/O 线程中,必须改造 `recvmmsg` 的调用方式,以提取内核附带的时间戳元数据。
|
|||
|
|
|
|||
|
|
- **Socket 配置**:
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
int flags = SO_TIMESTAMPNS; // 请求内核软件时间戳 (纳秒级)
|
|||
|
|
// 如果确认网卡支持硬件打点,则改为:
|
|||
|
|
// int flags = SOF_TIMESTAMPING_RX_HARDWARE | SOF_TIMESTAMPING_RAW_HARDWARE;
|
|||
|
|
setsockopt(sockfd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags));
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- **接收逻辑 (核心代码范式)**:
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
void UdpReceiver::receiveLoop() {
|
|||
|
|
struct mmsghdr msgs[BATCH_SIZE];
|
|||
|
|
struct iovec iovecs[BATCH_SIZE];
|
|||
|
|
char cmsg_buf[BATCH_SIZE][256]; // 存放辅助数据的缓冲区
|
|||
|
|
|
|||
|
|
// … 初始化 msgs, iovecs, cmsg …
|
|||
|
|
|
|||
|
|
int n = recvmmsg(sockfd, msgs, BATCH_SIZE, 0, nullptr);
|
|||
|
|
|
|||
|
|
for (int i = 0; i < n; ++i) {
|
|||
|
|
uint64_t timestamp_ns = 0;
|
|||
|
|
|
|||
|
|
// 解析辅助数据 (Control Message)
|
|||
|
|
struct cmsghdr *cmsg;
|
|||
|
|
for (cmsg = CMSG_FIRSTHDR(&msgs[i].msg_hdr); cmsg; cmsg = CMSG_NXTHDR(&msgs[i].msg_hdr, cmsg)) {
|
|||
|
|
// 优先提取硬件时间戳,若无则提取软件时间戳
|
|||
|
|
if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_TIMESTAMPNS) {
|
|||
|
|
struct timespec *ts = (struct timespec *)CMSG_DATA(cmsg);
|
|||
|
|
timestamp_ns = ts->tv_sec * 1000000000ULL + ts->tv_nsec;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 兜底:如果内核没给时间戳,立刻用 TSC 软时钟打点
|
|||
|
|
if (timestamp_ns == 0) {
|
|||
|
|
timestamp_ns = HighPrecisionClock::now();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 固化:写入 RawDataPacket Header
|
|||
|
|
// 这一刻起,这个时间戳就是数据的"法定出生时间"
|
|||
|
|
auto packet = MakePacket(std::move(payload), seq_id);
|
|||
|
|
packet.header.timestamp_us = timestamp_ns / 1000;
|
|||
|
|
// … 推送队列 …
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.3 全链路时间戳传递 (Propagation)
|
|||
|
|
|
|||
|
|
这个“出生时间戳” (`timestamp_us`) 必须在系统中神圣不可侵犯:
|
|||
|
|
|
|||
|
|
- **接收端 (`DataReceiver`)**:生成并写入 `RawDataPacket.header`。
|
|||
|
|
- **处理端 (`SignalProcessor`)**:继承该时间戳。虽然 FFT 处理是在 5ms 后进行的,但数据的物理意义依然是“那个时刻的回波”。
|
|||
|
|
- **输出端 (`DisplayController`)**:将该时间戳写入 Protobuf 的 `timestamp_us` 字段发送给显控。
|
|||
|
|
- *注意*:显控端看到的延迟 = `Now() - timestamp_us`。这个延迟包含了:`内核排队 + 信号处理耗时 + 航迹关联耗时 + 序列化耗时 + 网络传输耗时`。这正是我们想要的**真实物理延迟**。
|
|||
|
|
|
|||
|
|
-----
|
|||
|
|
|
|||
|
|
## 总结:2.6.2 基线图谱
|
|||
|
|
|
|||
|
|
| 维度 | 核心基线 | 技术细节 |
|
|||
|
|
| :--- | :--- | :--- |
|
|||
|
|
| **打点源** | **内核软打点 (`SO_TIMESTAMPNS`)** | 兼顾精度与兼容性,消除用户态抖动。 |
|
|||
|
|
| **理想源** | **硬件打点 (HW RX)** | 若网卡支持,优先升级至此 (需实测)。 |
|
|||
|
|
| **获取方式** | **`recvmmsg` + `CMSG`** | 从 Socket 辅助数据中提取,而非调用 `time()`。 |
|
|||
|
|
| **语义** | **生成时间 (Generation Time)** | 代表信号到达系统的物理时刻,全链路透传,禁止修改。 |
|