找回密码
立即注册
搜索
发回帖 发新帖

6553

积分

1

好友

818

主题
发表于 2026-9-21 01:54:40 | 查看: 101| 回复: 0

前两篇文章,我们分别讲了时间同步的基本原理,以及 gPTP 如何通过报文交换实现同步。

这一篇我们不再展开协议理论,而是具体看看 Linux 驱动如何实现时间同步。

我们以 Linux 6.18 的 stmmac 以太网驱动为例,沿着硬件时钟的注册、访问和调节,再到报文收发时的取戳与返回,把相关代码串起来。

不过,同步协议并不是全部运行在驱动里。进入源码前,先明确用户态同步程序和网卡驱动各自负责什么,以及它们操作的是哪只时钟。

1. Linux 中的同步软件与时钟

1.1 从 ptp4l 到网卡硬件时钟

在 Linux 上,LinuxPTP 提供了运行 PTP/gPTP 的用户态程序,其中负责网络同步的程序叫作 ptp4l。

前面讲过的报文交换、时间戳配对、时延与 offset 计算,以及决定怎样调钟的伺服算法,都可以由它完成。

用户态程序负责计算,硬件负责在报文经过取戳位置时记录时间,两者之间由内核和网卡驱动连接:驱动配置取戳硬件,把记录结果交给用户态,也把用户态的调钟请求落实到硬件。

这里需要先认识时间戳来自哪只时钟,支持硬件时间戳的 MAC、PHY 或交换芯片,可以使用关联的硬件计数器记录事件时间。

这类供 PTP 使用的硬件时钟称为 PHC,PTP Hardware Clock。

PHC 持续计时,取戳单元在报文发送或接收时锁存它的读数。

因此驱动需要区分读取 PHC 的当前时间,以及取回某次报文的事件时间,后者必须与那份报文保持对应。

1.2 PHC 与系统时间的关系

普通应用调用 clock_gettime(CLOCK_REALTIME, ...) 读取的是 Linux 系统时间,它由内核维护,与网卡的 PHC 是不同对象。

因此,ptp4l 使用硬件时间戳把 PHC 同步好以后,系统时间不会仅因为这件事就自动跟随。

如果应用也需要使用这条共同时间轴,可以由 LinuxPTP 的另一个程序 phc2sys 在 PHC 与系统时钟之间完成同步。

对于跟随网络时间的节点,典型关系是:

网络中的 GM
    → ptp4l 校正本机 PHC
    → phc2sys 校正本机 CLOCK_REALTIME
    → 应用读取系统时间

这里的方向取决于谁提供参考时间。 上面的例子中本机从网络获取参考时间,因此由 phc2sys 让系统时钟跟随已经同步好的 PHC。

然而如果本机承担 GM,并以系统时钟作为参考,方向就反过来了:由 phc2sys 调整 PHC,让它跟随系统时钟,再向网络提供时间。

这样 Linux 中要连接的对象就清楚了: ptp4l 一方面需要取得网络事件时间,另一方面需要控制 PHC。

Linux 分别提供了 socket 时间戳接口 SO_TIMESTAMPING,以及通过 /dev/ptpN 访问 PHC 的时钟接口。

Linux PTP 时间同步架构:ptp4l 与 phc2sys 访问 PHC 的路径

图 1:左列表示报文取戳路径,右列表示 PHC 访问路径。ptp4l 同时使用这两类接口,phc2sys 再按需要连接 PHC 与系统时钟;图中的 CLOCK_REALTIME 表示应用可访问的系统时钟。

在 stmmac 中,stmmac_ptp.c 实现 PHC 的注册和调钟回调,stmmac_hwtstamp.c 负责底层时间寄存器操作,stmmac_main.c 则包含初始化、取戳配置和报文收发处理。

我们先从硬件时钟怎样注册到内核开始。

2. PHC 的注册与调节

2.1 把硬件时钟交给 PTP 框架

在 stmmac 的设备探测阶段,stmmac_dvr_probe() 将网卡的操作表设为 stmmac_netdev_ops,其中的 ndo_open 指向 stmmac_open()。

后续启用网卡接口时,内核通过这个入口启动设备。

Linux 6.18 的 stmmac_open() 进一步调用 __stmmac_open()。

后者完成 stmmac_hw_setup() 后,再调用 stmmac_setup_ptp():使能 PTP 参考时钟,检查硬件取戳能力并初始化时间计数器;初始化成功后,才进入 stmmac_ptp_register() 注册 PHC。

stmmac 打开路径中 PTP 初始化调用的流程

时间计数器初始化完成后,调用进入 stmmac_ptp_register()。

接下来,我们看驱动怎样把这只硬件时钟注册到 Linux,让用户态程序能够读取和调整它。

Linux 通过 PTP 框架向用户态提供统一的时钟接口,但具体怎样读取计数器、修改时间和调整走速,需要由网卡驱动实现。

因此,注册时驱动需要把这些操作对应的函数交给框架,供框架在收到用户态请求时调用。

这些操作保存在 struct ptp_clock_info 中。下面是 stmmac 提供的四个基础时钟操作:

const struct ptp_clock_info stmmac_ptp_clock_ops = {
    .adjfine = stmmac_adjust_freq,
    .adjtime = stmmac_adjust_time,
    .gettime64 = stmmac_get_time,
    .settime64 = stmmac_set_time,
};

这些字段把通用操作与 stmmac 的具体函数对应起来:

操作 回调 对硬件时钟的作用
读取当前时间 gettime64 读出 MAC 时间计数器的值
设置当前时间 settime64 把计数器设置到指定读数
修正当前偏差 adjtime 按给定增量调整当前读数
调整运行速率 adjfine 改变计数器此后的走速

准备好这些方法后,stmmac 将实际使用的回调对象注册到 PTP class:

priv->ptp_clock = ptp_clock_register(&priv->ptp_clock_ops,
                                    priv->device);

注册成功后,框架为这只 PHC 分配索引并提供 /dev/ptpN 设备。

这里的 N 是 PTP 时钟索引,不能根据网卡名字猜测:eth0 不一定对应 /dev/ptp0。

例如,用下面的查询确认 eth0 关联的 PHC:

ethtool -T eth0

其中的 PTP Hardware Clock 字段给出索引。只有先确认这层关联,后续打开的设备才是当前网卡取戳所使用的时钟,设备名应按实际系统替换。

2.2 从用户态调钟请求到驱动回调

有了设备节点以后,用户态先打开 /dev/ptpN,再把文件描述符转换成动态 POSIX clock ID,也就是一个能够标识这只时钟的 ID。

随后,clock_gettime()、clock_settime() 和 clock_adjtime() 就可以明确地作用于它,这个用法由 Linux PTP 硬件时钟框架提供。

因此同样调用 clock_gettime(),传入 CLOCK_REALTIME 时读取系统时间,传入 PHC 对应的动态 clock ID 时读取网卡硬件时间。函数名相同,操作的时钟对象由参数决定。

当 ptp4l 使用这个 ID 发出校正请求时,内核通过 PTP class 找到注册的 ptp_clock_info,再根据请求类型调用 adjtime 或 adjfine。

stmmac 的函数随后访问 MAC 寄存器,才真正改变硬件时钟。

这层关联保存在 drivers/ptp/ptp_clock.c 中。

注册时,框架创建 struct ptp_clock,用其中的 info 指向驱动传入的回调对象,再通过 posix_clock_register() 接入动态 POSIX 时钟。

用户态请求先进入框架的通用操作,再转给这只 PHC 对应的驱动。

以读时为例,框架的 ptp_clock_gettime() 会优先使用 gettimex64,本文所示的 stmmac 回调表提供的是 gettime64,因此请求会进入 stmmac_get_time()。

调钟也采用同样的分派关系:ptp_clock_adjtime() 根据请求模式调用驱动的偏差修正或调频方法。

ptp4l/phc2sys 通过 PTP class 操作 MAC PHC 的调用关系

图 2:注册建立 PHC 与驱动回调的关联;运行时,用户态的读时、设时和校正请求通过这层关联到达 MAC。

2.3 从驱动回调到 MAC 时间寄存器

进入 stmmac_get_time() 后,函数首先通过 container_of(),从传入的 ptp_clock_info 找回包含它的 struct stmmac_priv。

这个私有对象保存着当前网卡的硬件操作表和寄存器基址,通用回调由此找到了具体设备。

驱动加锁后调用 stmmac_get_systime(priv, priv->ptpaddr, &ns),这里还经过一层硬件分派:hwif.c 按 MAC 版本选择时间戳操作表和 PTP 寄存器偏移,hwif.h 中的调用宏再转入相应的 get_systime()。

这样上层读时接口不必随着 MAC 版本改变。

stmmac_hwtstamp.c 中的 get_systime() 读取秒寄存器 PTP_STSR 和纳秒寄存器 PTP_STNSR。

两个寄存器无法在同一次总线读取中取回,如果恰好跨过整秒,就可能拼出一个不一致的时间,所以驱动会再次读取秒值进行检查:

sec1 = readl_relaxed(ioaddr + PTP_STSR);
do {
    sec0 = sec1;
    ns = readl_relaxed(ioaddr + PTP_STNSR);
    sec1 = readl_relaxed(ioaddr + PTP_STSR);
} while (sec0 != sec1);

上面摘录了读取循环,只有前后两次秒值相同,纳秒字段才属于这一秒;否则重新读取。

得到一致结果后,驱动组合成纳秒数,再转换为 timespec64 返回用户态。

这条路径读取的是 PHC 当前时间,报文事件的时间戳由后面的收发路径取回。

调钟则沿相反方向把参数送进硬件。

stmmac_adjust_time() 将有符号的纳秒偏差拆成秒、纳秒和增减方向,再由底层 adjust_systime() 写入时间更新寄存器,并在 PTP_TCR 中置位 TSUPDT,触发硬件更新。

调频入口仍叫 stmmac_adjust_freq(),但在 Linux 6.18 的回调表中绑定的是 adjfine,参数为带 16 位小数的 ppm 值,即 scaled_ppm。

它先用 adjust_by_scaled_ppm() 从 default_addend 算出新的 addend,再调用底层 config_addend():将新值写入 PTP_TAR,置位 PTP_TCR 中的 TSADDREG,并轮询等待硬件清除该位。

addend 是 MAC 计时速率控制中的增量参数,更新它改变的是 PHC 此后的走速,不是重新设置整块 SoC 的晶振频率。

到这一步,用户态提交的校正量才落实为硬件计数行为的变化。

现在软件已经有办法控制硬件时钟了。

不过,注册成功只建立了访问关系,回调是否正确、硬件是否实际响应,还需要运行验证。

即使 PHC 能正常读写,协议所需的报文时间戳也还要经过另一条配置与返回路径。

3. 硬件时间戳的两层配置

在上一篇中,我们已经区分了需要精确收发时刻的事件报文,以及不依赖自身精确收发时刻的通用报文。

网卡不必默认给所有流量取戳,软件需要告诉它要捕获哪些事件,并说明当前应用希望接收什么时间信息。

Linux 把这两个要求分开处理:设备级配置决定网卡怎样取戳,socket 级配置决定应用怎样请求和接收时间戳。

3.1 设备级配置与报文过滤

设备配置中的 struct hwtstamp_config 包含发送模式和接收过滤条件,分别由 tx_type、rx_filter 表示。

以常见的 Two-Step 硬件取戳为例,发送方向可以启用 HWTSTAMP_TX_ON,接收方向则请求捕获所需的 PTP 事件报文。

在 Linux 6.18 的 stmmac 中,这类设置经过网卡的 ndo_hwtstamp_set 回调,进入 stmmac_hwtstamp_set()。

它把用户态提出的发送模式、接收报文范围等要求转换成 MAC 控制位,并保存实际生效的设置,接收过滤条件不一定与用户请求完全相同。

例如,硬件只能为一组 PTPv2 事件取戳,不能单独筛选其中某一种时,驱动可以返回更宽的过滤范围。

用户态需要检查驱动返回的配置,确认所需报文仍在捕获范围内。

3.2 Socket 级请求与时间戳报告

网卡具备取戳条件以后,应用还需要通过 setsockopt() 配置 SO_TIMESTAMPING。

在硬件取戳场景中,下面三个标志分别表达生成请求和报告要求:

标志 含义
SOF_TIMESTAMPING_TX_HARDWARE 请求发送硬件时间戳
SOF_TIMESTAMPING_RX_HARDWARE 请求接收硬件时间戳
SOF_TIMESTAMPING_RAW_HARDWARE 要求返回可用的原始硬件时间戳

这里的 "请求" 和 "报告" 需要分开理解:硬件记录了一个时间,不代表内核一定会把它交给当前 socket;应用还要选择希望收到的时间戳类型。

RAW_HARDWARE 返回的值属于对应的硬件时钟,不能直接当作 CLOCK_REALTIME 的系统时间。

设备级与 Socket 级硬件时间戳配置路径

图 3:设备设置控制 MAC 的发送取戳和接收过滤,socket 设置表达应用的取戳请求及报告要求。两类配置分别作用于不同对象。

ptp4l 会在初始化相关 socket 时完成这些工作。

因此,ethtool -T 显示支持硬件时间戳,只能证明驱动报告了相应能力,还需要设备配置和应用请求匹配,运行中才能取得所需结果。

接下来我们沿一次发送和一次接收,看看这些设置怎样作用于报文,又怎样把硬件记录的时间送回应用。

4. TX 时间戳的异步返回

在上一篇中,GM 先发送 Sync,等拿到精确发送时间后,再用 Follow_Up 把它告诉对端。

这个先后关系放到 Linux 中,就要求发送时间戳能够在报文发出以后返回给 ptp4l。

sendmsg() 成功通常只表示数据已被内核接收,不能据此认为报文已经经过 MAC 的取戳点。

内核会把这次发送请求连同取戳要求交给驱动,等硬件完成事件记录后再返回时间戳。

4.1 从 socket 请求到发送描述符

Linux 使用 struct sk_buff 管理网络报文,通常简称 skb。除了报文数据,它还携带处理这份报文所需的元信息,硬件发送取戳请求就是其中一项。

当应用请求发送硬件时间戳时,内核在 skb 的发送标志中设置 SKBTX_HW_TSTAMP。

stmmac 的发送路径检查这个标志和设备取戳状态;能够处理这次请求时,再设置 SKBTX_IN_PROGRESS,并在发送描述符中启用时间戳捕获。

描述符是驱动交给网卡的工作记录,其中既包含发送数据的信息,也可以包含这次发送的取戳要求。

MAC 执行发送任务,报文经过硬件取戳位置时,便记录关联 PHC 的读数。

ptp4l 请求发送事件报文
    → skb 携带 SKBTX_HW_TSTAMP
    → stmmac 设置 SKBTX_IN_PROGRESS 和描述符取戳要求
    → MAC 发送报文并记录 PHC 时间

前两个标志表达的是请求和处理状态,不能单独证明有效时间戳已经产生。结果还需要从硬件取出,并与原来的发送报文保持对应。

4.2 从完成处理到 socket error queue

发送完成后,stmmac 的清理路径调用 stmmac_get_tx_hwtstamp(),检查时间戳状态,从描述符或 MAC 专用时间戳寄存器中读取结果,再通过 skb_tstamp_tx() 把它交回网络栈。

这里读取的是硬件先前锁存的事件时间。

驱动处理中断或执行清理时,即使已经晚了一段时间,也不应该重新读取"此刻的 PHC"来替代它,否则又会把软件处理延迟带回测量。

发送调用此时已经返回,内核需要另一个通道通知应用,这个通道就是 socket 的 error queue,错误队列。

它也用于传递这类异步控制信息,时间戳出现在其中并不表示发送失败。

stmmac_get_tx_hwtstamp()
    → skb_tstamp_tx()
    → socket error queue
    → recvmsg(..., MSG_ERRQUEUE)
    → ptp4l 得到此前发送事件的时间戳

应用从队列中取出消息后,还要解析随消息返回的附属信息。

其中携带时间戳的控制消息通常称为 SCM_TIMESTAMPING,硬件时间位于时间戳数组的 ts[2]。

应用使用旧、新时间 ABI 时需匹配相应结构;新程序应优先使用 SO_TIMESTAMPING_NEW,避免 32 位旧时间结构的 2038 年问题。

把结果与原来的 Sync 配对后,ptp4l 才能形成对应的 Follow_Up。

驱动负责把本机的事件时间送到用户态,Follow_Up 负责把必要信息送到网络对端,这是两个连续但不同的传递过程。

5. RX 时间戳的随包返回

接收方向同样需要硬件记录事件,但时间顺序有所不同:驱动开始处理收到的报文时,报文已经经过接收取戳位置。

它可以在把数据交给协议栈的同时,把此前捕获的接收时间一起带上。

在 stmmac 中,stmmac_get_rx_hwtstamp() 先判断接收时间戳是否有效,再从相应描述符取值。

不同 MAC 版本可能把时间戳放在当前描述符或附加的上下文描述符中,驱动需要使用与硬件版本匹配的读取方式。

取得时间后,驱动将它写入 skb 的硬件时间戳信息:

shhwtstamp = skb_hwtstamps(skb);
shhwtstamp->hwtstamp = ns_to_ktime(ns);

这两行摘自接收取戳路径,省略了前面的有效性检查等处理。

ns 是读回并按驱动要求修正的硬件时间;将它存入 skb 后,后续处理就能继续保持"这份报文—这次接收时间"的关联。

当应用通过普通 recvmsg() 接收报文时,内核同时通过附属控制消息返回硬件时间戳。应用需要解析这些信息,不能只读取报文数据区。

TX/RX 硬件时间戳采集与返回路径

图 4:TX 时间戳在发送完成后经 error queue 返回,RX 时间戳随正常接收消息返回。两者都携带硬件事件时间,但应用读取的队列不同。

接收 Sync 时,ptp4l 可以由此留下本地接收读数;后来收到匹配的 Follow_Up,再取出其中携带的发送时间。

即使软件处理发生得较晚,参与计算的仍是先前那次 Sync 的事件时间,与上一篇的计算前提一致。

因此,抓包看到报文到达,只证明数据收到了,不能证明这个 skb 携带有效硬件时间戳。

反过来,发送函数返回成功,也不能证明应用已经从 error queue 拿到了发送时间,这两条路径需要分别检查。

6. 从事件时间戳到持续校正

至此,前面两条实现路径可以在 ptp4l 中接起来了:本地接收读数经 RX 路径交给它,发送时间由匹配的 Follow_Up 提供,传播时间来自持续的 Pdelay 测量。

取得这些输入后,ptp4l 计算 offset,再由伺服决定怎样调钟。

如果伺服策略允许直接修正当前偏差,它可以通过 PHC 的时钟接口提交相应调整;如果采用渐进校正,就通过调频让后续读数逐步接近目标。

请求经过 PTP class 和 stmmac 回调,最终作用到产生收发时间戳的 MAC 计数器。

下一轮报文经过同一个取戳单元时,硬件便会记录调整后的 PHC 时间。

软件再次计算 offset,就能判断此次校正有没有达到预期;振荡器走速继续变化时,也能用新的样本继续调整。

测量与调节必须对应同一只时钟,或者有明确、正确的时钟间转换关系。

如果时间戳来自网卡 PHC,软件却只改了系统时钟,网卡下一次取戳仍然使用原来的 PHC,前面观察到的偏差就不会因为这次系统调钟而自动消失。

这也把 ptp4l 与 phc2sys 的分工解释完整了:前者通过网络事件校正关联 PHC,后者按部署需要把同步关系延伸到本机其他时钟。

应用最终读取哪个时钟,需要与系统的同步设计一致。

7. 按闭环路径定位问题

有了上述关系,上一篇提到的 TX timestamp timeout、offset 持续增长等现象,就可以更快地追到内核对象和驱动处理位置。

下面是我对一些常见的时间同步报错简单整理的可能错误原因,便于后期定位:

观察到的现象 优先检查的位置 需要确认的结果
查不到关联 PHC 硬件能力识别、参考时钟、PHC 注册路径 设备实际具备能力,注册成功且与目标网卡关联
PHC 能读,事件报文取不到戳 设备过滤器与 socket 配置 所需事件在捕获范围内,应用请求并接收硬件戳
TX timestamp timeout skb 标志、描述符取戳、完成处理与 error queue 硬件生成有效戳,驱动提交结果,用户态及时取回
收到报文却没有 RX 硬件戳 RX 描述符有效位、版本对应的取值路径、skb 附属信息 有效时间被写入正确 skb,并随接收消息返回
测量有效但 offset 持续增长 PHC 选择、调钟回调、硬件计时参数及其他调钟进程 校正请求作用到目标 PHC,且没有被其他控制覆盖
PHC 已同步,应用时间仍不一致 应用时钟来源、phc2sys 方向与时间尺度 应用使用的时钟确实接入了预期同步关系

例如,TX 超时既可能是硬件没有产生时间戳,也可能是完成处理没有把结果交回,或者用户态没有在等待期限内取到它。

仅凭 timeout 就认定"对端没有回包",会把本机发送取戳问题与网络响应问题混在一起。

如果设备能够报告取戳能力,PHC 也已经注册,但运行时仍拿不到有效戳,我们就应继续检查配置和收发路径。

前一项检查通过,只排除了其中一部分原因,不代表后面的所有条件都已经成立。

我认为 BSP 调试最有价值的做法,是把"硬件有能力""软件已配置""本次事件有有效结果"分开保留证据。只有明确测量输入可靠、调钟作用于正确对象,进一步分析伺服参数才有依据。

8. 总结

前面两篇解释了时间为什么会走散,以及 gPTP 怎样从报文交换中求出可以用于校正的偏差。

这一篇把测量和调节接到了 Linux 的具体实现:用户态运行协议与伺服,PTP 框架开放硬件时钟的控制接口,网卡驱动负责配置取戳并返回事件时间。

PHC 控制路径让软件能够修正当前读数和运行速率,TX/RX 返回路径让软件能够用新的事件判断校正效果。

两者围绕同一时间基准持续运行,才使协议计算真正成为设备上的同步过程。

经过三篇文章到这里,相信大家已经对时间同步形成了从基本概念、协议原理到驱动实现的完整关系,这几篇其实还是相对较为深入和硬核的。

之后有时间的时候,我会再另写一篇 Linux gPTP 使用教程,这个可能更适合使用者,因为可能很多同学只是想使用时间同步,并不需要一开始就理解得这么深入。

所以我会再写一个 Linux gPTP 使用教程结合实验平台介绍环境准备、配置、常用命令和同步结果检查,把这些原理对应到可以观察的运行过程,希望对大家能有帮助。本文由云栈社区整理分享。




上一篇:Spring Boot 处理 10 万条 Excel 导入:异步任务 + SSE 实时推送进度
下一篇:数据中心半导体2031年1.53万亿:存储上位,电力成瓶颈
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-14 09:00 , Processed in 0.083292 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表