6.6 KiB
6.6 KiB
tags, date created, date modified
| tags | date created | date modified |
|---|---|---|
| 星期三, 十一月 26日 2025, 9:49:46 晚上 | 星期三, 十一月 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 确立的时钟基线,我们需要对齐以下约束:
- 硬件能力 (Hardware Cap):
- 网迅 NIC:需确认驱动是否支持
SO_TIMESTAMPING接口读取硬件 RX 时间戳。通常企业级网卡(Intel X710/Mellanox)均支持,国产网卡需实测验证。若不支持,需回退到内核软打点。
- 网迅 NIC:需确认驱动是否支持
- 协议支持 (Protocol):
- UDP 数据包本身不携带“发送时间”(除非应用层协议写了)。因此我们依赖的是接收端打点 (Ingress Timestamping)。
- 数据结构关联:
- 生成的纳秒级时间戳必须填入
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 配置:
int flags = SO_TIMESTAMPNS; // 请求内核软件时间戳 (纳秒级) // 如果确认网卡支持硬件打点,则改为: // int flags = SOF_TIMESTAMPING_RX_HARDWARE | SOF_TIMESTAMPING_RAW_HARDWARE; setsockopt(sockfd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags)); -
接收逻辑 (核心代码范式):
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) | 代表信号到达系统的物理时刻,全链路透传,禁止修改。 |