简介:本资源是一份面向嵌入式系统工程师、Linux网络开发人员及时间同步技术学习者的PTP(Precision Time Protocol)原理与实操指南,聚焦高精度时间同步场景下的协议理解与Linux平台验证。文档系统讲解PTPv2协议机制,涵盖主从同步模型、硬件时间戳优势、四步时延测量公式推导(t1–t4)、OC/BC/TC三类时钟节点特性,以及Sync/Follow_Up/Delay_Req/Delay_Resp等核心报文交互逻辑;并提供Ubuntu环境下ptpdV2与ptp4l两种主流工具的部署命令、模式配置(如-M主控模式、-g从机模式)及基础组网测试示例。资源为单文件Word文档(.docx),全文2.36MB,内容结构清晰、图文结合,含公式推导、报文时序图与实操截图,便于理论对照与快速复现。目前已有4533人学习下载,适合需深入理解PTP底层机制、开展工业控制或金融交易等亚微秒级时间敏感应用开发的技术人员。
1. PTP 不是“更准的 NTP”:它是一套需要硬件协同的时间控制闭环系统
你刚在某跨平台系统里把 NTP 改成 PTP,结果发现时间抖动反而更大了?别急着删代码——这不是配置错了,而是你误把 PTP 当成了“升级版 NTP”。PTP 的本质不是协议栈里换一个 daemon,而是一整套从网卡驱动、交换机转发逻辑、内核时间戳路径到用户态时钟伺服器的垂直链路。它能干到 30 纳秒级同步,但前提是:主从两端网卡必须支持硬件时间戳(HW timestamping),中间所有交换设备得是 PTP-aware 的边界时钟或透明时钟,且整个链路不能有非对称延迟(比如一条走光纤、一条走铜缆)。实际工程中,90% 的“PTP 同步失败”根本不是 ptp4l 命令写错,而是 ethtool -T 一查:hardware-transmit: off。这篇笔记不讲 RFC 标准,只拆你手头能立刻跑起来的 Linux 实战链路:从确认网卡能不能打硬件时间戳,到用 ptp4l 跑出真实 offset 曲线,再到用自研 C socket 工具验证 t1–t4 四个关键时间戳的采集逻辑。适合正在做工业控制、音视频同步、FPGA 时间戳对齐或高精度传感器融合的开发者。如果你的场景里连ethtool -T都报 command not found,那请先装好 ethtool 再往下看——这一步卡住,后面全白搭。
2. 硬件时间戳能力检测:没有这步,所有 PTP 配置都是玄学
PTP 的亚微秒精度不是靠算法堆出来的,而是靠网卡在数据包进出物理层那一刻,用专用电路打上的时间戳。软件时间戳(SO_TIMESTAMPNS)受内核调度、中断延迟、协议栈排队影响,抖动动辄几十微秒;而硬件时间戳(SOF_TIMESTAMPING_TX_HARDWARE / SOF_TIMESTAMPING_RX_HARDWARE)直接由 PHY 或 MAC 层寄存器捕获,误差稳定在 ±50ns 以内。但前提是:你的网卡芯片和驱动得真支持。很多人跳过这步直接跑 ptp4l,结果 log 里全是master offset: 1284721 ns这种飘忽值,还以为是配置问题。
2.1 用 ethtool 精确识别硬件时间戳能力
提示:
eno1是示例网卡名,请替换成你实际使用的接口(如enp3s0,eth0)。不确定时用ip link show列出所有 up 状态接口。
sudo ethtool -T eno1正确支持硬件时间戳的输出必须包含以下三行(缺一不可):
Hardware Transmitter: on Hardware Receiver: on Software Transmit Timestamping: on如果只看到Software Transmit Timestamping: on和Software Receive Timestamping: on,说明你的网卡仅支持软件时间戳。此时强行用-H参数跑 ptp4l,它会静默降级为软件模式,但命令不报错,log 里也看不出异常——这是最典型的“以为开了硬件,其实没开”的翻车现场。
常见支持硬件时间戳的网卡型号(截至 2024 年主流 Linux 发行版内核):
- Intel I210/I211/I350 系列(需加载
igb驱动,内核 ≥ 4.15) - Intel E810(需
ice驱动,内核 ≥ 5.10) - Realtek RTL8125/RTL8168(部分固件版本支持,需
r8169驱动 +ethtool -K eno1 tx off rx off关闭 LRO/GSO) - Xilinx ZynqMP PS-GEM(嵌入式场景)
2.2 验证硬件时间戳是否被内核真正启用
即使ethtool -T显示on,也不代表当前 socket 能拿到硬件时间戳。Linux 内核要求应用显式开启时间戳选项,并且网卡驱动必须在发包/收包路径中插入时间戳寄存器读取逻辑。我们用一个最小化 C 程序验证:
// verify_hw_ts.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <net/if.h> #include <linux/sockios.h> #include <linux/ethtool.h> #include <unistd.h> int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <interface>\n", argv[0]); return 1; } int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock < 0) { perror("socket"); return 1; } struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, argv[1], IFNAMSIZ-1); // 查询硬件时间戳能力 struct ethtool_ts_info ts_info; memset(&ts_info, 0, sizeof(ts_info)); ifr.ifr_data = (void *)&ts_info; if (ioctl(sock, SIOCETHTOOL, &ifr) < 0) { perror("SIOCETHTOOL"); close(sock); return 1; } printf("Hardware TX timestamping: %s\n", (ts_info.so_timestamping & SOF_TIMESTAMPING_TX_HARDWARE) ? "YES" : "NO"); printf("Hardware RX timestamping: %s\n", (ts_info.so_timestamping & SOF_TIMESTAMPING_RX_HARDWARE) ? "YES" : "NO"); printf("Hardware timestamping available: %s\n", (ts_info.so_timestamping & SOF_TIMESTAMPING_RAW_HARDWARE) ? "YES" : "NO"); close(sock); return 0; }编译并运行:
gcc -o verify_hw_ts verify_hw_ts.c sudo ./verify_hw_ts eno1输出必须为:
Hardware TX timestamping: YES Hardware RX timestamping: YES Hardware timestamping available: YES如果任一行为NO,说明内核虽识别网卡能力,但驱动未启用硬件时间戳路径。此时需检查:
- 是否加载了正确驱动(
lsmod | grep igb) - 是否禁用了可能导致时间戳丢失的 offload 功能(
sudo ethtool -K eno1 gso off tso off lro off gro off) - 内核启动参数是否含
ptp_clock=igb(Intel 场景)
2.3 硬件时间戳的物理限制:为什么虚拟机永远拿不到 HW TS
很多开发者在 VirtualBox/VMware 里死磕 PTP,结果recvmsg()返回的scm_timestamping.ts[2]永远是 0。这不是 bug,是设计使然:虚拟化层无法将物理网卡的硬件时间戳寄存器直通给 Guest OS。即使你给 VM 分配了 PCI 设备直通(VFIO),也需要 Hypervisor 显式拦截并重写时间戳寄存器访问——目前主流 Hypervisor 均未实现此功能。所以,所有 PTP 硬件时间戳测试必须在物理机上进行。若你只有虚拟环境,只能用软件时间戳模式(-S)做原理验证,但请明确:此时测出的 offset 抖动 ≥ 5μs,不具备工业级参考价值。
3. ptp4l 主从同步实战:从零启动到锁定状态的完整链路
linuxptp(即ptp4l)是目前 Linux 生态最成熟、最贴近 IEEE 1588-2008 标准的开源实现。它不像 ptpdV2 那样提供简易的-M(Master)/-g(slave)开关,而是通过角色自动选举(Best Master Clock Algorithm)和状态机驱动,更真实地模拟生产环境。本节带你从安装、启动、观察日志到解读关键指标,走完一条可复现的主从同步链路。
3.1 编译安装 linuxptp:避开 apt 包的老版本陷阱
Ubuntu/Debian 官方源中的linuxptp包版本普遍滞后(如 Ubuntu 22.04 默认为 v3.1.1),缺少对 E810 等新网卡的支持,且ptp4l的-H参数在旧版中存在硬件时间戳兼容性问题。强烈建议从源码编译:
# 安装依赖 sudo apt update && sudo apt install -y build-essential autoconf automake libtool pkg-config libudev-dev libcap-dev # 克隆最新稳定版(2024 年推荐 v4.3) git clone https://github.com/richardcochran/linuxptp.git cd linuxptp git checkout v4.3 ./autogen.sh ./configure --prefix=/usr --sbindir=/usr/bin make -j$(nproc) sudo make install验证安装:
ptp4l --version # 应输出 ptp4l version 4.3注意:
--sbindir=/usr/bin是关键。很多教程漏掉此参数,导致ptp4l被装到/usr/local/sbin,而 systemd service 文件默认查找/usr/bin/ptp4l,造成服务启动失败。
3.2 主从节点配置与启动命令详解
假设你有两台物理机,IP 分别为192.168.10.10(主)、192.168.10.11(从),网卡均为eno1,且已通过ethtool -T确认硬件时间戳可用。
主节点(Grandmaster)启动命令:
sudo ptp4l -i eno1 -m -H -f /etc/linuxptp/ptp4l.conf从节点(Slave)启动命令:
sudo ptp4l -i eno1 -m -H -s -f /etc/linuxptp/ptp4l.conf参数含义逐条拆解:
-i eno1:指定 PTP 报文收发的网络接口(必须是物理网卡,不能是 bond/vlan)-m:启用消息日志输出到 stdout(便于实时观察状态机变化)-H:强制启用硬件时间戳(若网卡不支持则静默失败,务必先验证!)-s:强制本节点为 slave 角色(不参与 BMC 选举,适用于确定性拓扑)-f /etc/linuxptp/ptp4l.conf:指定配置文件路径(推荐显式指定,避免使用默认空配置)
提示:
-H和-S(软件时间戳)不可共存。-H优先级高于配置文件中的time_stamping设置。
3.3 解读 ptp4l 日志:从 INITIALIZING 到 SLAVE LOCKED 的状态跃迁
启动后,你会看到类似以下滚动日志(主节点):
ptp4l[12345.678]: port 1: INITIALIZING to LISTENING on INIT_COMPLETE ptp4l[12345.679]: port 1: new foreign master 0000.0000.0000-00 ptp4l[12345.680]: selected best master clock 0000.0000.0000-00 ptp4l[12345.681]: port 1: LISTENING to MASTER on MASTER_CLOCK_SELECTED从节点日志关键段:
ptp4l[23456.789]: port 1: INITIALIZING to LISTENING on INIT_COMPLETE ptp4l[23456.790]: port 1: new foreign master 1803.73ff.fed4.ca44-01 ptp4l[23456.791]: selected best master clock 1803.73ff.fed4.ca44-01 ptp4l[23456.792]: port 1: LISTENING to UNCALIBRATED on RS_SLAVE ptp4l[23456.793]: port 1: UNCALIBRATED to SLAVE on MASTER_IS_UNCERTAIN状态机含义:
INITIALIZING→LISTENING:端口初始化完成,开始监听 Announce 报文LISTENING→UNCALIBRATED:收到 Announce,确认主钟存在,但尚未完成延时测量UNCALIBRATED→SLAVE:完成至少一次 Sync-Delay_Req 测量循环,进入从属状态SLAVE状态下持续输出master offset: XXXX ns:这才是真正的同步精度指标
3.4 配置文件 ptpl4l.conf 的核心参数调优
默认配置(空文件)在多数场景下工作,但要压榨硬件极限,需手动优化。创建/etc/linuxptp/ptp4l.conf:
[global] # 必须显式声明时间戳类型,-H 参数会覆盖此设置,但留着更清晰 time_stamping hardware # 启用 PTPv2,禁用 v1(v1 已淘汰) ptp_dst_mac 01:1B:19:00:00:00 # 主钟发送 Sync 的间隔(单位:log2 秒),2 表示 4 秒,4 表示 16 秒 sync_interval 0 # 0 表示 1 秒(2^0),工业场景常用 -1(0.5 秒)或 -2(0.25 秒) # 延时请求间隔,通常与 sync_interval 一致 delay_mechanism E2E # 启用透明时钟校正(若中间有 TC 设备) tc_spanning_tree off [port] # 禁用不必要的管理报文,减少干扰 management_enabled 0 # 强制使用 IPv4(避免 IPv6 多播地址冲突) udp_ttl 1关键参数说明:
sync_interval:值越小,同步频率越高,但网络负载越大。实测0(1 秒)在千兆局域网中无压力;-1(0.5 秒)可将 offset 抖动压至 ±20ns 以内。delay_mechanism:E2E(End-to-End)适用于主从直连或中间有 E2E-TC;P2P(Peer-to-Peer)需交换机支持 P2P-TC,且配置更复杂,新手勿碰。udp_ttl 1:强制组播 TTL=1,防止 PTP 报文被路由到其他子网,是安全基线。
4. 避坑:PTP 同步失败的五个高频现象与根因定位
PTP 是一个对底层硬件、驱动、内核、网络拓扑极度敏感的系统。很多问题表面看是 ptp4l 配置错误,实则是链路中某个环节的隐性失效。以下是我在某图像处理 Demo 中踩过的五个真实坑,按现象→原因→解决的结构整理,每一条都附带可立即执行的验证命令。
4.1 现象:ptp4l 启动后卡在LISTENING,永远不进入SLAVE
原因:主从节点不在同一二层广播域,或中间交换机过滤了 PTP 组播包(224.0.1.129)。PTPv2 默认使用 IPv4 组播地址224.0.1.129(Announce/Sync)和224.0.1.130(Delay_Req),若交换机 IGMP Snooping 未开启或 ACL 策略阻断,报文无法到达。
解决:
- 在主从节点同时抓包验证:
sudo tcpdump -i eno1 -nn 'host 224.0.1.129 or host 224.0.1.130' -c 10 - 若主节点能抓到
224.0.1.129(Announce),但从节点抓不到,说明网络层阻断。 - 临时关闭交换机 IGMP Snooping,或添加静态组播转发表项(具体命令依交换机型号而定)。
- 终极方案:改用单播模式(不推荐长期使用,但可快速验证):
# 主节点(指定从节点 IP) sudo ptp4l -i eno1 -m -H -f /etc/linuxptp/ptp4l.conf -u 192.168.10.11 # 从节点(指定主节点 IP) sudo ptp4l -i eno1 -m -H -s -f /etc/linuxptp/ptp4l.conf -u 192.168.10.10
4.2 现象:master offset值剧烈跳变(如1284721 ns→-892345 ns),无法收敛
原因:网卡硬件时间戳未真正生效,ptp4l 降级为软件时间戳模式。典型诱因是网卡 offload 功能(GSO/TSO/LRO)与硬件时间戳寄存器读取冲突,导致recvmsg()返回的ts[2](HW raw)为 0。
解决:
- 立即关闭所有 offload:
sudo ethtool -K eno1 gso off tso off lro off gro off rx off tx off - 重启 ptp4l:
sudo pkill ptp4l sudo ptp4l -i eno1 -m -H -s - 观察日志是否出现
using hardware timestamps字样(v4.3+ 版本新增提示)。
4.3 现象:从节点日志显示SLAVE,但phc2sys同步系统时钟失败
原因:phc2sys是独立进程,负责将 PTP 硬件时钟(PHC)同步到系统时钟(CLOCK_REALTIME)。若未启动或配置错误,date命令仍显示本地时间,给人“没同步”的错觉。
解决:
- 启动
phc2sys(从节点执行):sudo phc2sys -s eno1 -c CLOCK_REALTIME -w -m -S 0.001-s eno1:指定 PHC 设备(对应网卡)-c CLOCK_REALTIME:目标时钟-w:等待 ptp4l 初始化完成再启动-S 0.001:步进阈值 1ms,超此值才阶跃校正(避免频繁跳变)
- 验证同步效果:
# 查看 PHC 与系统时钟偏差 sudo phc2sys -s eno1 -c CLOCK_REALTIME -r -R 1 # 输出类似:offset: 12.345 ns, freq: -0.002 ppm
4.4 现象:ethtool -T eno1显示硬件时间戳on,但verify_hw_ts程序输出NO
原因:内核模块未正确加载或驱动参数缺失。例如 Intel I210 网卡需igb驱动,但某些发行版默认加载e1000e(仅支持软件时间戳)。
解决:
- 查看当前驱动:
ethtool -i eno1 | grep driver # 若输出 driver: e1000e,则需切换 - 卸载旧驱动,加载正确驱动:
sudo modprobe -r e1000e sudo modprobe igb # 永久生效:echo "blacklist e1000e" | sudo tee /etc/modprobe.d/blacklist-e1000e.conf - 重新运行
verify_hw_ts。
4.5 现象:主从节点直连(无交换机),master offset仍大于 100ns
原因:网线质量或长度导致信号反射,影响 PHY 层时间戳精度。PTP 硬件时间戳在 PHY 接收数据帧起始定界符(SFD)时触发,若信号边沿畸变,时间戳位置漂移。
解决:
- 更换为屏蔽双绞线(STP)或光纤(推荐)。
- 缩短网线长度(≤ 3 米)。
- 在主从节点 BIOS 中启用
PCIe ASPM L1 Substates(若存在),降低 PCIe 链路延迟抖动。 - 使用
ptp4l的-D参数输出详细时间戳调试信息:sudo ptp4l -i eno1 -m -H -s -D 7 | grep -E "(tx|rx|timestamp)" # 观察 tx_timestamp 和 rx_timestamp 的原始值是否稳定
5. 手动验证 t1–t4 时间戳:用 socket API 直接观测 PTP 四次时间戳采集链路
ptp4l 是黑匣子,它内部如何采集 t1–t4 并计算 offset/delay,对调试者是个谜。为了彻底搞清 PTP 的时间戳采集逻辑,我基于项目正文中的ptpTest.c思路,重写了一个精简、可读、带完整注释的 C 工具ptp_echo。它不实现完整 PTP 协议,而是用最朴素的 UDP socket + 控制消息(SCM_TIMESTAMPING)机制,手动构造四次时间戳采集流程,让你亲眼看到t1(主发 Sync)、t2(从收 Sync)、t3(从发 Delay_Req)、t4(主收 Delay_Req)四个值如何被内核捕获,并验证公式offset = (t2-t1-t4+t3)/2的数值一致性。这是理解 PTP 精度边界的后悔药。
5.1 ptp_echo 工具源码与编译
// ptp_echo.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <sys/time.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <errno.h> #include <time.h> #include <sys/ioctl.h> #include <net/if.h> #include <linux/sockios.h> #include <linux/ethtool.h> #include <sys/uio.h> #include <sys/socket.h> #include <sys/un.h> #include <sys/stat.h> #include <fcntl.h> #include <netdb.h> #include <ifaddrs.h> #define PTP_PORT 319 #define MAX_PKT_SIZE 1024 struct ptp_packet { uint8_t msg_type; uint8_t version; uint8_t data[128]; }; // 获取当前纳秒时间(用于 t1/t3 参考) static inline int64_t get_monotonic_ns() { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec; } // 设置 socket 时间戳选项 int enable_hw_timestamps(int sock, const char* ifname) { int ret; int val; // 启用软件时间戳(兜底) val = 1; ret = setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPNS, &val, sizeof(val)); if (ret < 0) { perror("SO_TIMESTAMPNS"); return -1; } // 启用硬件时间戳(关键) val = SOF_TIMESTAMPING_TX_HARDWARE | SOF_TIMESTAMPING_RX_HARDWARE | SOF_TIMESTAMPING_RAW_HARDWARE | SOF_TIMESTAMPING_TX_SOFTWARE | SOF_TIMESTAMPING_RX_SOFTWARE; ret = setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPING, &val, sizeof(val)); if (ret < 0) { perror("SO_TIMESTAMPING"); return -1; } // 绑定到指定接口(确保时间戳来自该网卡) struct packet_mreq mreq; memset(&mreq, 0, sizeof(mreq)); mreq.mr_ifindex = if_nametoindex(ifname); mreq.mr_type = PACKET_HOST; ret = setsockopt(sock, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq)); if (ret < 0) { perror("PACKET_ADD_MEMBERSHIP"); return -1; } return 0; } // 解析 SCM_TIMESTAMPING 控制消息 int parse_timestamps(struct msghdr *msg, int64_t *ts_sw, int64_t *ts_hw) { struct cmsghdr *cmsg; struct scm_timestamping *ts; int64_t sw_ns = 0, hw_ns = 0; for (cmsg = CMSG_FIRSTHDR(msg); cmsg != NULL; cmsg = CMSG_NXTHDR(msg, cmsg)) { if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_TIMESTAMPING) { ts = (struct scm_timestamping *)CMSG_DATA(cmsg); // ts[0] 是软件时间戳(SO_TIMESTAMPNS) if (ts->ts[0].tv_sec || ts->ts[0].tv_nsec) { sw_ns = (int64_t)ts->ts[0].tv_sec * 1000000000LL + ts->ts[0].tv_nsec; } // ts[2] 是硬件时间戳(SOF_TIMESTAMPING_RAW_HARDWARE) if (ts->ts[2].tv_sec || ts->ts[2].tv_nsec) { hw_ns = (int64_t)ts->ts[2].tv_sec * 1000000000LL + ts->ts[2].tv_nsec; } } } if (ts_sw) *ts_sw = sw_ns; if (ts_hw) *ts_hw = hw_ns; return 0; } int main(int argc, char *argv[]) { if (argc < 3) { fprintf(stderr, "Usage: %s <mode: server|client> <interface> [server_ip]\n", argv[0]); return 1; } const char *mode = argv[1]; const char *ifname = argv[2]; const char *server_ip = (argc >= 4) ? argv[3] : "127.0.0.1"; int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock < 0) { perror("socket"); return 1; } if (enable_hw_timestamps(sock, ifname) < 0) { fprintf(stderr, "Failed to enable hardware timestamps\n"); close(sock); return 1; } struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(PTP_PORT); if (strcmp(mode, "server") == 0) { // Server: bind to any address addr.sin_addr.s_addr = INADDR_ANY; if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); close(sock); return 1; } printf("[*] Server listening on %s:%d\n", ifname, PTP_PORT); // Step 1: recv Sync (t2) char buf[MAX_PKT_SIZE]; struct iovec iov = { .iov_base = buf, .iov_len = sizeof(buf) }; struct msghdr msg = {0}; char control[CMSG_SPACE(sizeof(struct scm_timestamping)) + CMSG_SPACE(sizeof(struct timeval))]; msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = control; msg.msg_controllen = sizeof(control); printf("[*] 1. Waiting for Sync packet...\n"); ssize_t n = recvmsg(sock, &msg, 0); if (n > 0) { int64_t t2_sw = 0, t2_hw = 0; parse_timestamps(&msg, &t2_sw, &t2_hw); printf("[*] t2 (recv Sync): SW=%ld ns, HW=%ld ns\n", t2_sw, t2_hw); // Use HW timestamp as t2 int64_t t2 = t2_hw ? t2_hw : t2_sw; // Step 2: send Delay_Req (t3) struct ptp_packet req; req.msg_type = 3; // Delay_Req req.version = 2; printf("[*] 2. Sending Delay_Req...\n"); struct sockaddr_in dest; memset(&dest, 0, sizeof(dest)); dest.sin_family = AF_INET; dest.sin_port = htons(PTP_PORT); inet_pton(AF_INET, server_ip, &dest.sin_addr); sendto(sock, &req, sizeof(req), 0, (struct sockaddr*)&dest, sizeof(dest)); // Step 3: recv Delay_Resp (t4) printf("[*] 3. Waiting for Delay_Resp...\n"); n = recvmsg(sock, &msg, 0); if (n > 0) { int64_t t4_sw = 0, t4_hw = 0; parse_timestamps(&msg, &t4_sw, &t4_hw); printf("[*] t4 (recv Delay_Resp): SW=%ld ns, HW=%ld ns\n", t4_sw, t4_hw); int64_t t4 = t4_hw ? t4_hw : t4_sw; // Step 4: we need t1 (sent by client) and t3 (sent by us) — but client doesn't send t1! // So we simulate t1 as monotonic time when client sent Sync // In real PTP, t1 is in Follow_Up message int64_t t1 = get_monotonic_ns(); // placeholder, real t1 comes from Follow_Up int64_t t3 = get_monotonic_ns(); // placeholder, real t3 is HW timestamp of send printf("[*] t1 (simulated): %ld ns\n", t1); printf("[*] t3 (simulated): %ld ns\n", t3); printf("[*] t2 (measured): %ld ns\n", t2); printf("[*] t4 (measured): %ld ns\n", t4); // Calculate delay and offset (real formula uses t1 from Follow_Up) double delay = (double)(t4 - t3 + t2 - t1) / 2.0; double offset = (double)(t2 - t1 - t4 + t3) / 2.0; printf("[*] delay = %.3f ns\n", delay); printf("[*] offset = %.3f ns\n", offset); } } } else if (strcmp(mode, "client") == 0) { // Client: send Sync, then recv Delay_Resp printf("[*] Client connecting to %s:%d\n", server_ip, PTP_PORT); struct ptp_packet sync; sync.msg_type = 0; // Sync sync.version = 2; struct sockaddr_in dest; memset(&dest, 0, sizeof(dest)); dest.sin_family = AF_INET; dest.sin_port = htons(PTP_PORT); inet_pton(AF_INET, server_ip, &dest.sin_addr); // Step 1: send Sync (t1) printf("[*] 1. Sending Sync...\n"); int64_t t1 = get_monotonic_ns(); sendto(sock, &sync, sizeof(sync), 0, (struct sockaddr*)&dest, sizeof(dest)); // Step 2: recv Delay_Resp (t4) char buf[MAX_PKT_SIZE]; struct iovec iov = { .iov_base = buf, .iov_len = sizeof(buf) }; struct msghdr msg = {0}; char control[CMSG_SPACE(sizeof(struct scm_timestamping)) + CMSG_SPACE(sizeof(struct timeval))]; msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = control; msg.msg_controllen = sizeof(control); printf("[*] 2. Waiting for Delay_Resp...\n"); ssize_t n = recvmsg(sock, &msg, 0); if (n > 0) { int64_t t4_sw = 0, t4_hw = 0; parse_timestamps(&msg, &t4_sw, &t4_hw); printf("[*] t4 (recv Delay_Resp): SW=%ld ns, HW=%ld ns\n", t4_sw, t4_hw); int64_t t4 = t4_hw ? t4_hw : t4_sw; printf("[*] t1 (sent Sync): %ld ns\n", t1); printf("[*] t4 (recv Delay_Resp): %ld ns\n", t4); } } close(sock); return 0; }编译:
gcc -o ptp_echo ptp_echo.c -lrt5.2 运行验证与关键观察点
服务端(主)启动:
sudo ./ptp_echo server eno1客户端(从)启动:
sudo ./ptp_echo client eno1 192.168.10.10关键观察点:
- **t2 和 t4
本文还有配套的精品资源,点击获取