
简介本资源是一份面向嵌入式系统工程师、Linux网络开发人员及时间同步技术学习者的PTPPrecision 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.15Intel E810需ice驱动内核 ≥ 5.10Realtek RTL8125/RTL8168部分固件版本支持需r8169驱动 ethtool -K eno1 tx off rx off关闭 LRO/GSOXilinx 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_clockigbIntel 场景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 那样提供简易的-MMaster/-gslave开关而是通过角色自动选举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禁用 v1v1 已淘汰 ptp_dst_mac 01:1B:19:00:00:00 # 主钟发送 Sync 的间隔单位log2 秒2 表示 4 秒4 表示 16 秒 sync_interval 0 # 0 表示 1 秒2^0工业场景常用 -10.5 秒或 -20.25 秒 # 延时请求间隔通常与 sync_interval 一致 delay_mechanism E2E # 启用透明时钟校正若中间有 TC 设备 tc_spanning_tree off [port] # 禁用不必要的管理报文减少干扰 management_enabled 0 # 强制使用 IPv4避免 IPv6 多播地址冲突 udp_ttl 1关键参数说明sync_interval值越小同步频率越高但网络负载越大。实测01 秒在千兆局域网中无压力-10.5 秒可将 offset 抖动压至 ±20ns 以内。delay_mechanismE2EEnd-to-End适用于主从直连或中间有 E2E-TCP2PPeer-to-Peer需交换机支持 P2P-TC且配置更复杂新手勿碰。udp_ttl 1强制组播 TTL1防止 PTP 报文被路由到其他子网是安全基线。4. 避坑PTP 同步失败的五个高频现象与根因定位PTP 是一个对底层硬件、驱动、内核、网络拓扑极度敏感的系统。很多问题表面看是 ptp4l 配置错误实则是链路中某个环节的隐性失效。以下是我在某图像处理 Demo 中踩过的五个真实坑按现象→原因→解决的结构整理每一条都附带可立即执行的验证命令。4.1 现象ptp4l 启动后卡在LISTENING永远不进入SLAVE原因主从节点不在同一二层广播域或中间交换机过滤了 PTP 组播包224.0.1.129。PTPv2 默认使用 IPv4 组播地址224.0.1.129Announce/Sync和224.0.1.130Delay_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.129Announce但从节点抓不到说明网络层阻断。临时关闭交换机 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.104.2 现象master offset值剧烈跳变如1284721 ns→-892345 ns无法收敛原因网卡硬件时间戳未真正生效ptp4l 降级为软件时间戳模式。典型诱因是网卡 offload 功能GSO/TSO/LRO与硬件时间戳寄存器读取冲突导致recvmsg()返回的ts[2]HW raw为 0。解决立即关闭所有 offloadsudo ethtool -K eno1 gso off tso off lro off gro off rx off tx off重启 ptp4lsudo 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 ppm4.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-t4t3)/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 doesnt 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本文还有配套的精品资源点击获取