Linux 网络:PTP 简介

文章目录

  • 1. 前言
  • 2. PTP(Precision Time Protocol​) IEEE 1588 协议简介
    • 2.1 PTP IEEE 1588 协议时间同步原理
    • 2.2 PTP IEEE 1588 协议时钟类型
      • 2.2.1 普通时钟(OC: Ordinary Clock)
      • 2.2.2 边界时钟(BC: Boundary Clock)
      • 2.2.3 透明时钟(TC: Transparent Clock)
        • 2.2.3.1 端对端透明时钟(E2ETC: End to End Transparent Clock)
        • 2.2.3.2 点对点透明时钟(P2PTC: Peer to Peer Transparent Clock)
    • 2.3 PTP IEEE 1588 协议报文
      • 2.3.1 PTP IEEE 1588 报文格式
        • 2.3.1.1 IEEE 1588 v1 报文格式
        • 2.3.1.2 IEEE 1588 v2 报文格式
      • 2.3.2 PTP IEEE 1588 报文相关的 地址 和 端口号
        • 2.3.2.1 封装于 L2 层 的以太网帧相关的 MAC 地址
        • 2.3.2.2 封装于 L4 层 的以太网帧相关的 IP 和 端口号
  • 3. Linux PTP 协议栈
    • 3.1 Linux PTP 协议栈框架一览
    • 3.2 Linux PTP 协议栈: 内核空间部分
      • 3.2.1 PTP 硬件时钟 时间戳
        • 3.2.1.1 注册 PTP 硬件时钟设备
          • 3.2.1.1.1 MAC 层的 PTP 时钟注册
            • 3.2.1.1.1.1 网卡驱动加载时注册 PTP 时钟
            • 3.2.1.1.1.2 启动网卡设备时注册 PTP 时钟
          • 3.2.1.1.2 PHY 层的 PTP 时钟注册
          • 3.2.1.1.3 注册 PTP 时钟的公共流程
        • 3.2.1.2 用 PTP 硬件时钟给 PTP 报文 打时间戳
          • 3.2.1.2.1 MAC 层 PTP 时钟 对 传入、传出 网络包 打时间戳
            • 3.2.1.2.1.1 MAC 层 PTP 时钟 对 传入网络包 打时间戳
            • 3.2.1.2.1.2 MAC 层 PTP 时钟 对 传出网络包 打时间戳
          • 3.2.1.2.2 PHY 层 PTP 时钟 对 传入、传出网络包 打时间戳
            • 3.2.1.2.2.1 PHY 层 PTP 时钟 对 传入网络包 打时间戳
            • 3.2.1.2.2.2 PHY 层 PTP 时钟 对 传出网络包 打时间戳
        • 3.2.1.3 PTP 硬件时钟 时间戳 小结
          • 3.2.1.3.1 MAC 层时间戳 和 PHY 层时间戳 的 异同
            • 3.2.1.3.1.1 MAC 层时间戳 和 PHY 层时间戳 的 相同点
            • 3.2.1.3.1.2 MAC 层时间戳 和 PHY 层时间戳 的 差异点
          • 3.2.1.3.2 传入、传出 网络包 PTP 硬件时钟时间戳 的 异同
          • 3.2.1.3.3 用户空间 获取 传入、传出 网络包 硬件时间戳 的 过程
            • 3.2.1.3.3.1 使能 传入、传出 网络包 硬件时间戳
            • 3.2.1.3.3.2 读取 传出网络包 的 硬件时间戳
            • 3.2.1.3.3.3 读取 传入网络包 的 硬件时间戳
      • 3.2.2 系统时钟 CLOCK_REALTIME 软件时间戳
        • 3.2.2.1 用 系统时钟 CLOCK_REALTIME 给 传入、传出网络包 打时间戳
          • 3.2.2.1.1 用系统时钟 CLOCK_REALTIME 给 传入网络包 打时间戳
          • 3.2.2.1.2 用系统时钟 CLOCK_REALTIME 给 传出网络包 打时间戳
        • 3.2.2.2 传入、传出网络包 系统时钟 CLOCK_REALTIME 软件时间戳 的 异同
        • 3.2.2.3 用户空间 获取 传入、传出 网络包 软件时间戳 的 过程
          • 3.2.2.3.1 使能 传入、传出 网络包 软件时间戳
          • 3.2.2.3.2 读取 传出网络包 的 软件时间戳
          • 3.2.2.3.3 读取 传入网络包 的 软件时间戳
      • 3.2.3 PTP 硬件时钟 和 系统时钟 CLOCK_REALTIME 时间戳 对比
    • 3.3 Linux PTP 协议栈:用户空间部分
      • 3.3.1 linuxptp 的配置
      • 3.3.2 使用 PTP 硬件时钟时间戳的情形
        • 3.3.2.1 初始化
          • 3.3.2.1.1 打开 PTP 硬件时钟设备 和 创建处理 PTP 协议包套接字
        • 3.3.2.2 处理 PTP 协议包
          • 3.3.2.2.1 获取 Toffset
          • 3.3.2.2.2 用 Toffset 同步 PTP 硬件时钟
      • 3.3.3 使用 系统时钟 CLOCK_REALTIME 时间戳的情形
      • 3.3.4 ptp4l 使用范例
  • 4. Linux PTP 相关工具
    • 4.1 ethtool 查询
    • 4.2 phc2sys
    • 4.3 其它 linuxptp 工具
  • 5. 参考资料

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. PTP(Precision Time Protocol​) IEEE 1588 协议简介

PTP(Precision Time Protocol​) IEEE 1588 协议 是一个付费协议,本小节内容基于网络公开资料进行搜集整理而成。PTP(Precision Time Protocol​) IEEE 1588 协议 是一种精密时间同步协议标准,旨在实现网络中设备之间的高精度时间同步PTP(Precision Time Protocol​) IEEE 1588 协议 随着发展,已经有了如下几个版本:

. 1588 v1(IEEE 1588-2002)
. 1588 v2 (IEEE 1588-2008)
. 1588 v2.1 (IEEE 1588-2019)

1588 v2 相对于 1588 v1 ,一个重大的改变是引入了增加时间同步精度的 透明时钟(TC: Transparent Clock)。关于 透明时钟(TC: Transparent Clock) 的概念,后面会进行描述。

2.1 PTP IEEE 1588 协议时间同步原理

PTP IEEE 1588 采用系主从层次式结构来同步时钟,实现机制如下图所示:
在这里插入图片描述
上图中:

T1: 主时钟(master) 发送 【同步报文 Sync】 的时间
T2: 从时钟(slave) 收到 【同步报文 Sync】 的时间
T3: 从时钟(slave) 发送 【延时请求报文 Delay_Req】 的时间
T4: 主时钟(master) 收到 【延时请求应答报文 Delay_Resp】 的时间

另外:

. 主时钟(master) 向 从时钟(slave) 发送 Follow_Up 报文:Follow_Up 报文 携带 主时钟(master) 发送同步报文 Sync 的时间,传递给 从时钟(slave)。Follow_Up 报文仅在 Two-Step 模式下使用,而在 One-Step 模式下,Sync 报文自带了时间 T1,不再需要 Follow_Up 报文。
. 主时钟(master) 记录 收到 从时钟(slave) 发送的 Delay_Req 报文时间 T4,然后通过Delay_Resp 报文发送给 从时钟(slave)

这样,经过图中 4 次报文交互,在 从时钟(slave) 一侧,记录了所有的 T1, T2, T3, T4 ,通过这 4 个时间,就可以计算出 主从时钟 传输延时 T d e l a y {T}_{delay} Tdelay
在这里插入图片描述
以及 主从时钟之间 的 时间偏差 T o f f s e t {T}_{offset} Toffset
在这里插入图片描述
注意,上面两个公式都假定 master -> slaveslave -> master 的发送延时 是相同的。如果 master -> slaveslave -> master 的发送延时不对称,则上述计算公式就会由偏差,针对这种问题,IEEE 1588 通过在 PTP 通信报文中嵌入时间校正域(Correction Field)来解决。

2.2 PTP IEEE 1588 协议时钟类型

在上一小节 2.1 中,我们提到了 主时钟(master)从时钟(slave),但这到底是什么? 主时钟(master)从时钟(slave),顾名思义,就是两个时钟,更具体点,就是某台设备上的时间计时部件。譬如有两台通过网线直连的电脑主机,各自电脑上的计时部件就称为是 主时钟(master)从时钟(slave)。至于用哪一台电脑的时间计时部件作为 主时钟(master),是通过 PTP IEEE 1588 协议最佳主时钟算法(BMCA: Best Master Clock Algorithm) 来确立的。位于网络中主机都通过 Announce 报文宣告自己的时钟精度等特性,最终选举出 主时钟(master) 。被选举出来的 主时钟(master) 作为 从时钟(slave)基准时钟(时间同步源)。其它作为 从时钟(slave) 的设备通过 2.1 中的时钟同步机制得到的 T o f f s e t {T}_{offset} Toffset,来调整自身时钟以保持和 主时钟(master) 同步:或缩小 T o f f s e t {T}_{offset} Toffset,或和 主时钟(master) 保持相对稳定的 T o f f s e t {T}_{offset} Toffset
到目前为止,我们所讲述的都是最简单的 主时钟(master)从时钟(slave) 直接连接的拓扑结构。但现实世界总是复杂的,主时钟(master)从时钟(slave) 之间可能存在 路由器交换机,一个 主时钟(master) 也可以作为多个 从时钟(slave)基准时钟(时间同步源),等等其它情形。在这些复杂的拓扑结构中,IEEE 1588 协议按设备在拓扑中的位置,引入了 普通时钟(OC: Ordinary Clock)边界时钟(BC: Boundary Clock)透明时钟(TC: Transparent Clock) 这几个概念。

2.2.1 普通时钟(OC: Ordinary Clock)

普通时钟(OC: Ordinary Clock) 可以位于 IEEE 1588 拓扑结构中任何位置,这些设备包含的时钟,就称为 普通时钟(OC: Ordinary Clock)普通时钟(OC: Ordinary Clock) 可以作为 主时钟(master)从时钟(slave)主时钟(master) 向网络 发送 基准时钟从时钟(slave) 从网络 接收 基准时钟。下面图中标记为 masterslave 的,全都是 普通时钟(OC: Ordinary Clock)
在这里插入图片描述
在这里插入图片描述
可以看到,普通时钟(OC: Ordinary Clock) 可以在拓扑中任何位置。其中,在交换机 Switch 上,进口网口的时钟 作为 Grandmasterslave出口网卡的时钟 作为 末端设备master

2.2.2 边界时钟(BC: Boundary Clock)

边界时钟(BC: Boundary Clock)2个2个以上 端口:一个作 slave,用于跟上级 master 同步;一个做 master,用于给下级slave 提供 基准时钟。如 2.2.1 小节图中的 Switch ,它就是一个 边界时钟(BC: Boundary Clock)

2.2.3 透明时钟(TC: Transparent Clock)

透明时钟(TC: Transparent Clock) 是在 IEEE 1588 v2 中提出来的,定义了两种 透明时钟(TC: Transparent Clock) 模型。分别是:

. 端对端透明时钟(End to End Transparent Clock,简称 E2ETC)
. 点对点透明时钟(Peer to Peer Transparent Clock,简称 P2PTC)

这两种 透明时钟(TC: Transparent Clock) 都能计算 PTP 报文经过网络交换设备(交换机、路由器等)的时延,二者区别在于对路径延迟测量方式不同。在 IEEE1588 v2 标准中定义,E2E 透明时钟 是一种能够计算 PTP 同步报文在网络交换设备中的驻留时间,并且把此时间累加在 PTP 同步报文的校正域(Correction Field,以下简称CF)中的时钟模型。当同步报文到达从钟,从钟计算时间偏差时把校正域(即 PTP 同步报文在透明时钟中的延时)考虑在内,这样就可以补偿掉同步报文在透明时钟上的延时,使得网络交换设备看起来“透明”(相当于导线),有效避免了延时和延时抖动,提高了网络交换设备级联时的同步精度。主从时钟通过3级级联交换设备实现时间同步的原理如下图所示:
在这里插入图片描述
由上图所示可得,经过 透明时钟(TC: Transparent Clock) 总的驻留时间 CF(Correction Field) 的计算公式为:

CF = TS2 - TS1 + TS4 - TS3 + TS6 - TS5

主从时钟的时间偏差 的计算公式为:

主从时钟的时间偏差 = 收到 Sync 时间-发送 Sync 时间-路径延迟-驻留时间= ((T2-T1-CF)(T4-T3-CF')) / 2

其中:

CF: Sync 报文 在每个中间节点的驻留时间 之和
CF': Delay_Req 报文 在每个中间节点的驻留时间 之和

透明时钟(TC: Transparent Clock) 提出之前,解决主从时间同步通过交换设备产生的非对称延迟及延迟抖动问题,通常采用设计边界时钟(BC: Boundary Clock),将现在使用的集线器或者交换机给替换掉。如下图所示:
在这里插入图片描述
相对于普通时钟只有一个 PTP 端口,边界时钟有两个以上的 PTP 端口,每个端口可以处于不同的状态。在主从时钟之间布置若干个边时钟,逐级同步,边界时钟既是上级时钟的从时钟,也是下级时钟的主时钟,由不同的端口来实现主从功能。边界时钟能降低非对称性的影响。但边界时钟是通过逐级同步实现不同端口的主从时钟同步的,如果在第一级产生了同步误差,这种误差将被逐级的往下传,造成误差积聚,同步精度不高,稳定性差。将 边界时钟(BC: Boundary Clock) 替换为 透明时钟(TC: Transparent Clock) 后,如下图:
在这里插入图片描述
透明时钟(TC: Transparent Clock) 对中间设备驻留时间的校正,克服了 边界时钟(BC: Boundary Clock) 逐级同步造成误差逐渐传递的问题。

2.2.3.1 端对端透明时钟(E2ETC: End to End Transparent Clock)

端对端透明时钟(E2ETC: End to End Transparent Clock) 的 时钟模型如下图所示:
在这里插入图片描述
端对端透明时钟(E2ETC: End to End Transparent Clock) 对 交换机 和 路由器 提出了要求:

转发所有的 非 PTP 报文 和 PTP报文,但对于 PTP 事件报文,每个端口通过事件端口能识别该报文并产生相应的时间戳。
然后该报文通过一个驻留时间桥计算该报文在本点驻留的时间(报文穿过本点所花的时间),驻留时间将累加到报文的校正域
(Correction Field)字段中。

由以上分析可以得出,要实现支持 透明时钟 的 交换机 和 路由器 需要包含以下3个主要功能:

1. 普通 交换机、路由器 的功能;
2. 能识别 PTP 事件报文 并 标记报文 的 收发时间戳 的功能;
3. 完成 驻留时间的计算 及 修改报文 的 Correction Field 字段。
2.2.3.2 点对点透明时钟(P2PTC: Peer to Peer Transparent Clock)

(待续)

2.3 PTP IEEE 1588 协议报文

2.3.1 PTP IEEE 1588 报文格式

PTP 报文 可能是封装的位于 L2 层 的以太网帧,通常经由 以太网 PHY 芯片处理,这些报文通常不会再往上传递到内核网络协议栈,其报文格式是如下:
在这里插入图片描述
在这里插入图片描述
PTP 报文 也可能是封装 L4 层 的 TCP/UDP 报文,其格式如下:
在这里插入图片描述
在这里插入图片描述

2.3.1.1 IEEE 1588 v1 报文格式

(待续,暂未比较完整的相关信息,先放一个 Wireshark 抓包)
IEEE 1588 v1 报文 Sync 抓包:
在这里插入图片描述
IEEE 1588 v1 报文 Follow_Up 抓包:
在这里插入图片描述

2.3.1.2 IEEE 1588 v2 报文格式

IEEE 1588 v2 报文 必须包含消息头消息体消息扩展字节扩展字节长度可能为 0。看一下 IEEE 1588 v2 报文消息头 的格式:
在这里插入图片描述
PTP IEEE 1588 v2 报文头部的 messageType(也即 2.3.1.2 图中的 MsgType) 域 指定 PTP 报文类型。PTP IEEE 1588 1588 v2 消息分为两类:事件消息(EVENT Message)通用消息(General Message)事件消息(EVENT Message) 报文是时间概念报文进出设备端口时需要打上精确的时间戳;而 通用消息(General Message) 报文则是非时间概念报文进出设备不会产生时戳。类型值 0x00 ~ 0x03 的 为 事件消息(EVENT Message)0x8 ~ 0x0D通用消息(General Message)

事件消息(EVENT Message):
0x00: Sync
0x01: Delay_Req
0x02: Pdelay_Req
0x03: Pdelay_Resp0x04-7: Reserved通用消息(General Message):
0x08: Follow_Up
0x09: Delay_Resp
0x0A: Pdelay_Resp_Follow_Up
0x0B: Announce
0x0C: Signaling
0x0D: Management0x0E-0x0F: Reserved

限于篇幅,这里只对 Sync,Follow_Up,Delay_Req,Delay_Resp 几个 PTP 报文的格式加以说明。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.3.2 PTP IEEE 1588 报文相关的 地址 和 端口号

IANA 组织将有些 IP 和 端口号分配给 PTP IEEE 1588 协议使用。

2.3.2.1 封装于 L2 层 的以太网帧相关的 MAC 地址

在这里插入图片描述

2.3.2.2 封装于 L4 层 的以太网帧相关的 IP 和 端口号

在这里插入图片描述

224.0.0.107 | PTP-pdelay | [NIST: IEEE Std 1588][Kang_Lee] | 2007-02-02

对 PTP IEEE 1588 协议的介绍,本文就进行到这里。本文剩余篇幅都是对 Linux PTP 协议栈实现的分析,对这些内容不感兴趣的读者,可以结束对本文的阅读。

3. Linux PTP 协议栈

3.1 Linux PTP 协议栈框架一览

PTP 协议栈的实现,主要就是根据 2.1 PTP IEEE 1588 协议时间同步原理 的内容,通过 PTP 报文的时间戳 计算 T o f f s e t {T}_{offset} Toffset,然后按 T o f f s e t {T}_{offset} Toffset 调整时钟,以达到 从时钟(slave)主时钟(master) 同步的目的。PTP 报文的时间戳,可能有两个来源:

1. 网络设备自带的硬件时钟(MAC 自带的硬件时钟,或 PHY 自带的硬件时钟)。这种【网络设备自带硬件时钟】提供的时间戳,称为【硬件时间戳】。
2. 系统时钟(如 ARM 芯片的 timer)。这种由【系统时钟】提供的时间戳,称为【软件时间戳】。

用下图来简单的描述下 Linux PTP 协议栈的框架结构:
在这里插入图片描述
在上图中,将 Linux PTP 协议栈的实现分为 内核空间用户空间 两大部分。内核空间 的 PTP 协议栈相关工作概括如下:

 (1.1) 处理 L2 层 PTP 协议包,为进出的 PTP 事件协议包,用 (存在的) PTP 硬件时钟 或 系统时钟 CLOCK_REALTIME(没有 PTP 硬件时钟) 打上时间戳;(1.2) 提供 PTP 硬件时钟驱动,提供 /dev/ptpX 设备节点,让用户空间可以读取、调整 PTP 硬件时钟。

用户空间 PTP 协议栈相关工作概括为:处理 L4 层 的 PTP 协议包,并根据这些协议包的时间戳等信息,进行时钟(调整)同步。本文不会对所有类型时钟的工作进行分析,仅对大多时候使用更多的 普通时钟(OC)master / slave 的工作进行更细致的分析,它们的工作概括如下:

(2.1) 所有的 时钟设备 通过 BMCA(Best Master Clock Algorithm) 算法 选出 master 时钟;
(2.2) master 定时发送 Sync 包,携带 Sync 包时间戳的 Follow_Up (One-Step 模式不需要,One-Step 模式Sync 自带时间戳)(2.3) slave 处理 PTP 协议包 (Sync, Follow_Up, Delay_Req, Delay_Resp, ...),提取这些 PTP 数据报的时间戳,得到 从时钟 相对于 主时钟 的 时间偏差,并根据这个 时间偏差值 调整 (存在的) PTP 硬件时钟 或 系统时钟 CLOCK_REALTIME (没有 PTP 硬件时钟 的情形)

下面从 Linux 内核 到 用户空间,自底向上的分析整个 Linux PTP 协议栈的实现和工作流程。用户空间的实现以 linuxptp 项目代码为例来进行分析。

3.2 Linux PTP 协议栈: 内核空间部分

PTP 数据报时间戳 可能来源于 (1) 网络设备自带的硬件时钟(2) 系统时钟 CLOCK_REALTIME

3.2.1 PTP 硬件时钟 时间戳

3.2.1.1 注册 PTP 硬件时钟设备

PTP 硬件时钟,可以实现在 MAC 层,也可以实现 PHY 层,两种方式选其中之一即可。Linux 内核提供 ptp_clock_register() 接口注册 PTP 时钟。PTP 硬件时钟 的作用,从 3.1 小节中的框图可知,提供 /dev/ptpX 设备节点,供用户空间读取时间、调整时间用。下面来看 MAC 层 和 PHY 层的 PTP 时钟的注册过程。

3.2.1.1.1 MAC 层的 PTP 时钟注册

MAC 层注册 PTP 时钟的时机可能是:

. 网卡驱动加载时。如后面例子中的 igb_probe(). 启动网卡设备时。如后面例子中 stmmac_open()

下面分别以 intel igb 网卡 和 stmicroMAC 驱动 为例,来说明上述两种情形下的 PTP 时钟注册过程。

3.2.1.1.1.1 网卡驱动加载时注册 PTP 时钟
/* 1. 网卡驱动加载时 */
igb_probe() /* drivers/net/ethernet/intel/igb.c */.../* do hw tstamp init after resetting */igb_ptp_init(adapter);/* 见 3.2.3 PTP 时钟注册公共流程分析 */adapter->ptp_clock = ptp_clock_register(&adapter->ptp_caps, &adapter->pdev->dev);...
3.2.1.1.1.2 启动网卡设备时注册 PTP 时钟
/** 2. 启动网卡设备时,在 stmmac_open() 中注册 PTP 时钟:* ip link set dev eth0 up* ifconfig eth0 up*/
sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
ioctl(sockfd, SIOCSIFFLAGS, {ifr_name="eth0", ifr_flags=IFF_UP|IFF_BROADCAST|IFF_RUNNING|IFF_MULTICAST})sock_ioctl()...dev_change_flags()__dev_change_flags()__dev_open()/* 调用网卡驱动 open (启动)接口 */ops->ndo_open(dev) = stmmac_open(dev)...stmmac_hw_setup(dev, true);/* STMicro MAC 硬件 PTP 初始化 */ret = clk_prepare_enable(priv->plat->clk_ptp_ref);ret = stmmac_init_ptp(priv);...priv->hw->ptp = &stmmac_ptp;priv->hwts_tx_en = 0;priv->hwts_rx_en = 0;stmmac_ptp_register(priv);priv->ptp_clock_ops = stmmac_ptp_clock_ops;/* 见 3.2.3 PTP 时钟注册公共流程分析 */priv->ptp_clock = ptp_clock_register(&priv->ptp_clock_ops, priv->device);
3.2.1.1.2 PHY 层的 PTP 时钟注册

dp83640 以太网 PHY 芯片的驱动为例,说明 PHY 层的 PTP 时钟注册流程。

phy_probe() /* drivers/net/phy/phy_device.c */...if (phydev->drv->probe)err = phydev->drv->probe(phydev); /* PHY 驱动入口: dp83640_probe() */dp83640_probe(phydev) /* drivers/net/phy/dp83640.c */clock->chosen = dp83640;clock->ptp_clock = ptp_clock_register(&clock->caps, &phydev->mdio.dev);
3.2.1.1.3 注册 PTP 时钟的公共流程

不管是处于 MAC 层 还是 PHY 层 的 PTP 时钟注册,都通过接口 ptp_clock_register() 完成。前面已经通过几个例子分析了 MAC 层PHY 层 各自注册 PTP 时钟的 前期过程,下面接着分析 PTP 时钟注册的 公共过程,即 ptp_clock_register()

/* 3.2.3 PTP 时钟注册公共流程分析 */
struct ptp_clock *ptp_clock_register(struct ptp_clock_info *info, struct device *parent); /* drivers/ptp/ptp_clock.c */struct ptp_clock *ptp;...ptp = kzalloc(sizeof(struct ptp_clock), GFP_KERNEL);...ptp->clock.ops = ptp_clock_ops;.../* Create a new device in our class. */ptp->dev = device_create_with_groups(ptp_class, parent, ptp->devid,ptp, ptp->pin_attr_groups,"ptp%d", ptp->index); /* 创建并注册 PTP 设备 */.../* Register a new PPS source. */if (info->pps) {struct pps_source_info pps;.../* 创建并注册 /dev/pps%d 字符设备 */ptp->pps_source = pps_register_source(&pps, PTP_PPS_DEFAULTS);...}.../* Create a posix clock. *//* 注册 PTP 时钟字符设备 (/dev/ptp%d) */err = posix_clock_register(&ptp->clock, ptp->devid);...cdev_init(&clk->cdev, &posix_clock_file_operations); /* 设定 /dev/ptp%d 字符设备文件接口 */...err = cdev_add(&clk->cdev, devid, 1); /* 添加字符设备到系统 */return ptp;
3.2.1.2 用 PTP 硬件时钟给 PTP 报文 打时间戳

PTP 硬件时钟的工作,就是用 MACPHY 自带的硬件计数器的计数值,给收发的 PTP 协议数据报 盖上时间戳。下面分别对实现在 MAC 层PHY 层 的 PTP 硬件时钟,从 收(RX)、发(TX) 两个方向给 PTP 协议数据报 打时间戳 的过程,一一加以说明。

3.2.1.2.1 MAC 层 PTP 时钟 对 传入、传出 网络包 打时间戳

本小节以前文提到的 intel igb MAC 驱动注册的 PTP 时钟为例,对 PTP 协议数据包 打时间戳 的 过程加以说明。

3.2.1.2.1.1 MAC 层 PTP 时钟 对 传入网络包 打时间戳

有网络数据帧进入网卡时,会产生中断信号。收取网络数据帧的整个过程从 intel igb 网卡中断入口 igb_intr() 开始:

igb_intr() /* drivers/net/ethernet/intel/igb/igb_main.c */.../* 触发 NET_RX_SOFTIRQ 软中断接口 net_rx_action(),调度 igb 网卡驱动的 poll 接口收包 igb_poll() */napi_schedule(&q_vector->napi);return IRQ_HANDLED;/* NET_RX_SOFTIRQ 软中断接口 */
net_rx_action()napi_poll()igb_poll()

支持 PTP 时钟的 MAC 芯片自动为接收的数据帧生成时间戳,并保存到硬件寄存器里;igb_poll() 收取网络数据帧时,从硬件寄存器读取该时间戳并记录到 skb_hwtstamps(skb)->hwtstamp

igb_poll() /* drivers/net/ethernet/intel/igb/igb_main.c */...if (q_vector->rx.ring) {int cleaned = igb_clean_rx_irq(q_vector, budget);struct igb_ring *rx_ring = q_vector->rx.ring;struct sk_buff *skb = rx_ring->skb;.../* populate checksum, timestamp, VLAN, and protocol */igb_process_skb_fields(rx_ring, rx_desc, skb);/* 网卡硬件已经(在硬件寄存器里)给数据报打了时间戳,但数据报不包含时间戳 */if (igb_test_staterr(rx_desc, E1000_RXDADV_STAT_TS) &&!igb_test_staterr(rx_desc, E1000_RXDADV_STAT_TSIP))igb_ptp_rx_rgtstamp(rx_ring->q_vector, skb);.../* 从网卡寄存器读取 接收的数据帧的时间戳 的 高、低 32-bit */regval = rd32(E1000_RXSTMPL);regval |= (u64)rd32(E1000_RXSTMPH) << 32;/* 记录 从寄存器 读取的 硬件时间戳 到 @skb */igb_ptp_systim_to_hwtstamp(adapter, skb_hwtstamps(skb), regval);memset(hwtstamps, 0, sizeof(*hwtstamps));/* Upper 32 bits contain s, lower 32 bits contain ns. */hwtstamps->hwtstamp = ktime_set(systim >> 32, systim & 0xFFFFFFFF);......}...
3.2.1.2.1.2 MAC 层 PTP 时钟 对 传出网络包 打时间戳

网卡向外发送数据帧时,支持 PTP 时钟的 MAC 芯片自动为发送帧生成时间戳,并保存到硬件寄存器里,同时生成一个中断信号;网卡驱动中断处理接口 igb_intr() 处理发送帧时间戳中断信号,读取硬件寄存器保存的发送帧时间戳,创建发送帧的数据副本,将从寄存器读取的发送帧时间戳记录到该数据副本帧,最后将数据帧副本添加到对应套接字对象的错误消息队列,方便用户提取发送帧的时间戳信息。

igb_intr() /* drivers/net/ethernet/intel/igb/igb_main.c */u32 icr = rd32(E1000_ICR);.../* 发送数据帧时,硬件生成的时间戳加载到寄存器后,会产生中断信号 */if (icr & E1000_ICR_TS)igb_tsync_interrupt(adapter);...if (tsicr & E1000_TSICR_TXTS) { /* 发送帧时间戳 寄存器 已加载 *//* retrieve hardware timestamp */schedule_work(&adapter->ptp_tx_work); /* 触发 igb_ptp_tx_work() 调用 */ack |= E1000_TSICR_TXTS;}...igb_ptp_tx_work()...tsynctxctl = rd32(E1000_TSYNCTXCTL);if (tsynctxctl & E1000_TSYNCTXCTL_VALID)igb_ptp_tx_hwtstamp(adapter);/* 从寄存器 读取硬件生成的 发送数据帧 的 时间戳 */regval = rd32(E1000_TXSTMPL);regval |= (u64)rd32(E1000_TXSTMPH) << 32;/* 记录 发送数据帧 的 时间戳 到 @shhwtstamps */igb_ptp_systim_to_hwtstamp(adapter, &shhwtstamps, regval);.../* Notify the stack and free the skb after we've unlocked */skb_tstamp_tx(skb, &shhwtstamps);__skb_tstamp_tx(orig_skb, hwtstamps, orig_skb->sk, SCM_TSTAMP_SND);.../* 克隆 发送 skb @orig_skb 的 副本到 @skb */skb = skb_clone(orig_skb, GFP_ATOMIC);...if (hwtstamps)*skb_hwtstamps(skb) = *hwtstamps; /* 设置 克隆 @skb 的 时间戳 为 硬件时间戳 @hwtstamps */elseskb->tstamp = ktime_get_real(); /* 设置 克隆 @skb 的 时间戳 为 系统时间 */__skb_complete_tx_timestamp(skb, sk, tstype, opt_stats);struct sock_exterr_skb *serr;...serr = SKB_EXT_ERR(skb);memset(serr, 0, sizeof(*serr));serr->ee.ee_errno = ENOMSG;serr->ee.ee_origin = SO_EE_ORIGIN_TIMESTAMPING;serr->ee.ee_info = tstype; /* SCM_TSTAMP_SND */.../** 添加 @skb 到 sock 错误消息队列 sock::sk_error_queue : * 这个 @skb 的 原始版本 已经通过网卡往外发送, 现在将其增加* 了时间戳消息的副本 @skb 放到 sock 的错误消息队列, 这样用* 户空间可以通过取 sock 错误消息的方式,提取发送包的 时间戳* 信息.*/err = sock_queue_err_skb(sk, skb);...skb_queue_tail(&sk->sk_error_queue, skb);if (!sock_flag(sk, SOCK_DEAD))/* 唤醒等待读取 socket 错误状态的进程 */sk->sk_error_report(sk) = sock_def_error_report(sk);wq = rcu_dereference(sk->sk_wq);if (skwq_has_sleeper(wq))wake_up_interruptible_poll(&wq->wait, POLLERR);sk_wake_async(sk, SOCK_WAKE_IO, POLL_ERR);return 0;dev_kfree_skb_any(skb);else/* reschedule to check later */schedule_work(&adapter->ptp_tx_work);
3.2.1.2.2 PHY 层 PTP 时钟 对 传入、传出网络包 打时间戳

本小节以前文提到的 以太网 PHY 芯片 dp83640 驱动注册的 PTP 时钟为例,对 网络包 收(RX)发(TX) 打时间戳 的 过程加以说明。

3.2.1.2.2.1 PHY 层 PTP 时钟 对 传入网络包 打时间戳

在收到网络数据包时,进入函数 netif_receive_skb_internal() 进行收取工作:

netif_receive_skb_internal(skb).../** 开启 CONFIG_NETWORK_PHY_TIMESTAMPING 配置的情形下,* 调用 PHY 驱动 .rxtstamp 接口,处理 传入包 PTP 协议 * 数据包 时间戳 。* 如果 CONFIG_NETWORK_PHY_TIMESTAMPING 未开启,不做* 任何处理, skb_defer_rx_timestamp() 返回 false 。*/if (skb_defer_rx_timestamp(skb)) // 见后续分析return NET_RX_SUCCESS; /* 网络包已经处理 */// 接上面分析
skb_defer_rx_timestamp(skb)struct phy_device *phydev;unsigned int type;...type = ptp_classify_raw(skb); /* 提取 收取的 @skb 的 PTP 数据报类型 */...if (type == PTP_CLASS_NONE) /* 不是 PTP 协议类型包, */return false; /* 不做处理 */phydev = skb->dev->phydev; /* 接收 @skb 包的 PHY 设备 */if (likely(phydev->drv->rxtstamp))/* PHY 驱动处理 @type 类型的 PTP 协议包 */return phydev->drv->rxtstamp(phydev, skb, type); /* dp83640_rxtstamp():见后续分析 *//* PHY 驱动没能成功处理 PTP 协议包 */return false;// 接上面分析
dp83640_rxtstamp(phydev, skb, type)...list_for_each_safe(this, next, &dp83640->rxts) {rxts = list_entry(this, struct rxts, list);if (match(skb, type, rxts)) {shhwtstamps = skb_hwtstamps(skb);memset(shhwtstamps, 0, sizeof(*shhwtstamps));shhwtstamps->hwtstamp = ns_to_ktime(rxts->ns); /* 记录 PTP 硬件时钟 时间戳 到 @skb */list_del_init(&rxts->list);list_add(&rxts->list, &dp83640->rxpool);break;}}...
3.2.1.2.2.2 PHY 层 PTP 时钟 对 传出网络包 打时间戳
/* 从 网卡驱动的 发送接口 开始 */
igb_xmit_frame()igb_xmit_frame_ring(skb, igb_tx_queue_mapping(adapter, skb));.../** 为 传出数据包 @skb 生成 并 记录 硬件时间戳 和 软件时间戳* (如果设置了 SKBTX_SW_TSTAMP) ,将 生成的 软硬件时间戳* 记录到 传出数据包 的 克隆包,然后将 克隆包 添加到 * 传出数据包 所属套接字的 错误消息队列:* . 如果开启了 CONFIG_NETWORK_PHY_TIMESTAMPING 配置, *   调用 PHY 驱动 .txtstamp 接口,为 PTP 协议数据包 *   生成 传出包 硬件时间戳,并记录 硬件时间戳 到 *   原始 PTP 数据包 的 克隆包,然后将 克隆包 添加到 *   传出数据包 所属套接字的 错误消息队列;* . 如果设置了 SKBTX_SW_TSTAMP 标志位,用 系统时间 为 *   传出数据包 生成 软件时间戳,记录生成的 软件时间戳 到*   传出数据 的 克隆包,然后将 克隆包 添加到 传出数据 *   所属套接字 的 错误消息队列。*/skb_tx_timestamp(skb); /* net/core/timestamping.c */skb_clone_tx_timestamp(skb);struct phy_device *phydev;struct sk_buff *clone;unsigned int type;...type = classify(skb);if (type == PTP_CLASS_NONE) /* 只为 传出 PTP 协议数据包 生成 时间戳 */return;phydev = skb->dev->phydev;if (likely(phydev->drv->txtstamp)) {clone = skb_clone_sk(skb); /* 克隆 传出数据包 */...// 见后续分析phydev->drv->txtstamp(phydev, clone, type); /* dp83640_txtstamp() */}......// 接前面分析
dp83640_txtstamp(phydev, clone, type)...switch (dp83640->hwts_tx_en) {case HWTSTAMP_TX_ONESTEP_SYNC:if (is_sync(skb, type)) {kfree_skb(skb);return;}/* fall through */case HWTSTAMP_TX_ON:skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS;skb_info->tmo = jiffies + SKB_TIMESTAMP_TIMEOUT;skb_queue_tail(&dp83640->tx_queue, skb); /* 添加 到 PTP 传出数据包队列,待处理 (decode_txts()) */break;}// decode_txts() 处理 dp83640_txtstamp() 放入到 @dp83640->tx_queue 队列的 PTP 包
dp83640_rxtstamp()if (is_status_frame(skb, type)) {decode_status_frame()...if (PSF_RX == type/*传入 PTP 数据包*/ && len >= sizeof(*phy_rxts)) {...} else if (PSF_TX == type/*传出 PTP 数据包*/ && len >= sizeof(*phy_txts)) {decode_txts(dp83640, phy_txts); /* 为 传出 PTP 数据包 设置 硬件时间戳 */.../** 如果使能了 套接字 的 传出包时间戳,则 将 带有传出包 的* 硬件时间戳 克隆包 @skb 记录到 套接字 @sk 的 错误消息队列。*/skb_complete_tx_timestamp(skb, &shhwtstamps);...if (likely(refcount_inc_not_zero(&sk->sk_refcnt))) {*skb_hwtstamps(skb) = *hwtstamps;__skb_complete_tx_timestamp(skb, sk, SCM_TSTAMP_SND, false);struct sock_exterr_skb *serr;serr = SKB_EXT_ERR(skb);memset(serr, 0, sizeof(*serr));serr->ee.ee_errno = ENOMSG;serr->ee.ee_origin = SO_EE_ORIGIN_TIMESTAMPING;serr->ee.ee_info = tstype; /* SCM_TSTAMP_SND, ... */.../* 添加 时间戳 @skb 到 sock 错误消息队列 sock::sk_error_queue */err = sock_queue_err_skb(sk, skb);...sock_put(sk);return;}}kfree_skb(skb);return true;}
3.2.1.3 PTP 硬件时钟 时间戳 小结
3.2.1.3.1 MAC 层时间戳 和 PHY 层时间戳 的 异同
3.2.1.3.1.1 MAC 层时间戳 和 PHY 层时间戳 的 相同点

MAC 层 和 PHY 层 的 时间戳,由于都是在收发时由硬件提供,所以都能够准确的反映收发包的准确时间,同时都可以通过配置过滤器,为指定类型的传入、传出网络包生成时间戳,这是它们彼此的相同点。

3.2.1.3.1.2 MAC 层时间戳 和 PHY 层时间戳 的 差异点

由于 MAC 层获取收发时间戳是内存映射的寄存器读取收发时间戳,相对于 PHY 层通过 MDIO 总线读取寄存器获取收发时间戳,显然 MAC 层获取收发时间戳的速度要比 PHY 层更快,这是它们彼此的不同点。

3.2.1.3.2 传入、传出 网络包 PTP 硬件时钟时间戳 的 异同

对于 传入、传出 网络包,记录硬件时间戳的位置不同

  • 对 传入网络包,从 MAC 层 和 PHY 读取 的 硬件时间戳 记录在 sk_buff 的 skb_hwtstamps(skb) 中;
  • 对 传出网络包,从 MAC 层 和 PHY 读取 的 硬件时间戳 记录在 socket 的 错误消息队列中。
3.2.1.3.3 用户空间 获取 传入、传出 网络包 硬件时间戳 的 过程
3.2.1.3.3.1 使能 传入、传出 网络包 硬件时间戳
/* * 1. 开启、配置 PTP 硬件时钟 硬件时间戳功能。*/
struct hwtstamp_config cfg;/* 使能 硬件 L2 层 和 L4 层 PTP 协议事件包 时间戳生成 功能 */
cfg.type = HWTSTAMP_TX_ON;
cfg.rx_filter = HWTSTAMP_FILTER_PTP_V2_EVENT;
err = ioctl(fd, SIOCSHWTSTAMP, &ifreq);/** 最终会调用* . 网卡驱动的 时间戳配置接口 igb_ptp_set_ts_config() (MAC 层提供时间戳的情形) * . PHY 层驱动的 .hwtstamp 如 dp83640_hwtstamp() (PHY 层提供时间戳的情形)*/.../* * 2. 使能 socket 的 传入、传出 网络包 硬件时间戳*/
int flags = SOF_TIMESTAMPING_TX_HARDWARE |SOF_TIMESTAMPING_RX_HARDWARE |SOF_TIMESTAMPING_RAW_HARDWARE;
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags));if (level == SOL_SOCKET)err =  sock_setsockopt(sock, level, optname, optval, optlen);...switch (optname) {...case SO_TIMESTAMPING:...sk->sk_tsflags = val;...break;...}...else...
3.2.1.3.3.2 读取 传出网络包 的 硬件时间戳
/** 读取 传出网络包 的 硬件时间戳*/// 从前面的分析中了解到,传入网络包的时间戳,记录在 套接字的错误消息队列 中, 
// 现在将这个时间戳取出来。
// 这里以 UDP 套接字举例。TCP 套接字的类似,感兴趣的读者可自行阅读源码。// 3.1 发送数据
sendto(fd, buf, len, 0, &addr->sa, sizeof(addr->sin));// 3.2 从 套接字的错误消息队列 取回 发送数据包 的 时间戳
static struct msghdr msg;
...
recvmsg(fd, &msg, MSG_ERRQUEUE);...udp_recvmsg()if (flags & MSG_ERRQUEUE) /* MSG_ERRQUEUE 标记,指示只收取 sock 的错误消息数据 */return ip_recv_error(sk, msg, len, addr_len);...skb = sock_dequeue_err_skb(sk);...sock_recv_timestamp(msg, sk, skb);...struct skb_shared_hwtstamps *hwtstamps = skb_hwtstamps(skb); /* 硬件时间戳 */...if (sock_flag(sk, SOCK_RCVTSTAMP) ||(sk->sk_tsflags & SOF_TIMESTAMPING_RX_SOFTWARE) ||(kt/* 0 值无效 */ && sk->sk_tsflags & SOF_TIMESTAMPING_SOFTWARE) ||(hwtstamps->hwtstamp/* 0 值无效 */ &&(sk->sk_tsflags & SOF_TIMESTAMPING_RAW_HARDWARE)))__sock_recv_timestamp(msg, sk, skb); /* 读取 @skb 的软、硬件时间戳,从 @msg 返回到用户空间 */...if (shhwtstamps &&(sk->sk_tsflags & SOF_TIMESTAMPING_RAW_HARDWARE) &&!skb_is_swtx_tstamp(skb, false_tstamp) &&/* 硬件时间戳 放入 scm_timestamping::ts[2] */ktime_to_timespec_cond(shhwtstamps->hwtstamp, tss.ts + 2)) {empty = 0;...}if (!empty) {/* 通过 CMSG 形式向用户空间返回 时间戳 数据 */put_cmsg(msg, SOL_SOCKET, SCM_TIMESTAMPING, sizeof(tss), &tss);...}else.........
3.2.1.3.3.3 读取 传入网络包 的 硬件时间戳
/** 读取 传入网络包 的 硬件时间戳*/// 从前面的分析中了解到,传出网络包的时间戳,记录在 `sk_buff 的 skb_hwtstamps(skb)` 
// 中,现在将这个时间戳取出来。
// 这里以 UDP 套接字举例。TCP 套接字的类似,感兴趣的读者可自行阅读源码。recvmsg(fd, &msg, flags);...udp_recvmsg()...sock_recv_ts_and_drops(msg, sk, skb);#define TSFLAGS_ANY	  (SOF_TIMESTAMPING_SOFTWARE			| \SOF_TIMESTAMPING_RAW_HARDWARE)if (sk->sk_flags & FLAGS_TS_OR_DROPS || sk->sk_tsflags & TSFLAGS_ANY/*软、硬件时间戳*/)__sock_recv_ts_and_drops(msg, sk, skb);sock_recv_timestamp(msg, sk, skb); /* 读取 sock @sk 的 @skb 的时间戳信息给用户空间 */...struct skb_shared_hwtstamps *hwtstamps = skb_hwtstamps(skb); /* 硬件时间戳 */if (sock_flag(sk, SOCK_RCVTSTAMP) ||(sk->sk_tsflags & SOF_TIMESTAMPING_RX_SOFTWARE) ||(kt/* 0 值无效 */ && sk->sk_tsflags & SOF_TIMESTAMPING_SOFTWARE) ||(hwtstamps->hwtstamp/* 0 值无效 */ &&(sk->sk_tsflags & SOF_TIMESTAMPING_RAW_HARDWARE)))__sock_recv_timestamp(msg, sk, skb);struct skb_shared_hwtstamps *shhwtstamps = skb_hwtstamps(skb);...if (shhwtstamps &&(sk->sk_tsflags & SOF_TIMESTAMPING_RAW_HARDWARE) &&!skb_is_swtx_tstamp(skb, false_tstamp) &&/* 硬件时间戳 放入 scm_timestamping::ts[2] */ktime_to_timespec_cond(shhwtstamps->hwtstamp, tss.ts + 2)) {empty = 0;...}if (!empty) {/* 通过 CMSG 形式向用户空间返回 时间戳 数据 */put_cmsg(msg, SOL_SOCKET, SCM_TIMESTAMPING, sizeof(tss), &tss);...}else.....else if (unlikely(sock_flag(sk, SOCK_TIMESTAMP)))...else if (unlikely(sk->sk_stamp == SK_DEFAULT_STAMP))......

3.2.2 系统时钟 CLOCK_REALTIME 软件时间戳

3.2.2.1 用 系统时钟 CLOCK_REALTIME 给 传入、传出网络包 打时间戳

如果不支持 PTP 硬件时钟,可以用 系统时钟 CLOCK_REALTIME 对 PTP 报文打时间戳。对于 PTP 硬件时钟对 PTP 报文打时间戳,时间点都是在 PTP 报文进出网络设备的时候;而对于用 系统时钟 CLOCK_REALTIME 对 PTP 报文打时间戳时机,根据用户空间 setsockopt() 调用传递的参数不同,可以有多种时机,本文只讨论以下时机给 PTP 数据报打时间戳情形:

. 对接收的数据包:数据报 正由 网卡驱动 进入 网络协议栈 给 PTP 数据报打时间戳
. 对发送的数据包:数据报 正要 传递给网卡硬件缓冲前 给 PTP 数据报打时间戳

接下来,来分别看看在 接收 和 发送 PTP 数据报时,内核是怎么给它们打上时间戳的。

3.2.2.1.1 用系统时钟 CLOCK_REALTIME 给 传入网络包 打时间戳
// (1) 使能 传入网络包 软时间戳(系统时钟时间戳):
//     netdev_tstamp_prequeue && netstamp_needed 成立时,为 传入网络包 生成 软时间戳。
//     其中:
//     netdev_tstamp_prequeue: /proc/sys/net/core/netdev_tstamp_prequeue, 默认为 1
//     netstamp_needed: 通过下面的 setsockopt() 代码片段使能
unsigned int opt = SOF_TIMESTAMPING_SOFTWARE | // 请求 对 PTP 数据报 打上 系统时间戳SOF_TIMESTAMPING_RX_SOFTWARE | ...;  // 数据报 正由 网卡驱动 进入 网络协议栈 给 PTP 数据报打时间戳
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &opt, sizeof(opt));sock_setsockopt()...switch (optname) {...case SO_TIMESTAMPING:sk->sk_tsflags = val;if (val & SOF_TIMESTAMPING_RX_SOFTWARE)// 启用软件时间戳sock_enable_timestamp(sk, SOCK_TIMESTAMPING_RX_SOFTWARE);if (!sock_flag(sk, flag)) {...if (sock_needs_netstamp(sk) &&!(previous_flags & SK_FLAGS_TIMESTAMP))net_enable_timestamp(); // 使能 netstamp_needed}else......}// (2) 将 数据报 传给 网络协议栈 时 打时间戳
netif_receive_skb_internal(skb)/* 为 @skb 生成 软件时间戳 */// net_timestamp_check(netdev_tstamp_prequeue, skb);// 展开为:if (static_key_false(&netstamp_needed)) {if (netdev_tstamp_prequeue && !skb->tstamp)__net_timestamp(SKB);/* 用 CLOCK_REALTIME 时钟生成 @skb 软件时间戳 */skb->tstamp = ktime_get_real();}
3.2.2.1.2 用系统时钟 CLOCK_REALTIME 给 传出网络包 打时间戳

还是以 Intel 的 igb 网卡为例来进行说明:

// (1) 使能 发出网络包 软时间戳(系统时钟时间戳):以 UDP 包发送为例// setsockopt() 标记 使能 出网络包 软时间戳(系统时钟时间戳)
unsigned int opt = SOF_TIMESTAMPING_SOFTWARE | // 请求 对 PTP 数据报 打上 系统时间戳SOF_TIMESTAMPING_TX_SOFTWARE | ...;
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &opt, sizeof(opt));sock_setsockopt()...switch (optname) {...case SO_TIMESTAMPING:sk->sk_tsflags = val; // SOF_TIMESTAMPING_TX_SOFTWARE | ......}sendto()...udp_sendmsg().../** 将 时间戳标记 @tsflags 映射 到 时间戳标记 @tx_flags:*  tsflags                     | tx_flags* -----------------------------|--------------------* SOF_TIMESTAMPING_TX_SOFTWARE | SKBTX_SW_TSTAMP* -----------------------------|--------------------*/sock_tx_timestamp(sk, ipc.sockc.tsflags, &ipc.tx_flags);if (unlikely(tsflags))__sock_tx_timestamp(tsflags, tx_flags);u8 flags = *tx_flags;...// SOF_TIMESTAMPING_TX_SOFTWARE 标志映射为 SKBTX_SW_TSTAMPif (tsflags & SOF_TIMESTAMPING_TX_SOFTWARE)flags |= SKBTX_SW_TSTAMP; // 使能 发送包 软时间戳......// (2) 正要将 数据报 传给 网络卡 前 打时间戳
sendto()...udp_sendmsg()...igb_xmit_frame() /* 网卡驱动的 发送接口 */igb_xmit_frame_ring(skb, igb_tx_queue_mapping(adapter, skb));...skb_tx_timestamp(skb);// 生成 硬件时间戳.../** 为 传出数据包 生成 软件时间戳,记录生成的 软件时间戳 到* 传出数据 的 克隆包,然后将 克隆包 添加到 传出数据 所属套接字* 的 错误消息队列。*/if (skb_shinfo(skb)->tx_flags & SKBTX_SW_TSTAMP)skb_tstamp_tx(skb, NULL);__skb_tstamp_tx(orig_skb, hwtstamps, orig_skb->sk, SCM_TSTAMP_SND);...skb = skb_clone(orig_skb, GFP_ATOMIC); /* 克隆 传出 数据包 */.../* 将 带传出包 的 时间戳 克隆包 添加到 套接字 @sk 的 错误消息队列 */__skb_complete_tx_timestamp(skb, sk, tstype, opt_stats);
3.2.2.2 传入、传出网络包 系统时钟 CLOCK_REALTIME 软件时间戳 的 异同

对于 传入、传出 网络包,记录 系统时钟 CLOCK_REALTIME 生成的 软件时间戳 的位置不同

  • 对 传入网络包,系统时钟 CLOCK_REALTIME 生成 的 软件时间戳 记录在 sk_buff 中;
  • 对 传出网络包,系统时钟 CLOCK_REALTIME 生成 的 软件时间戳 记录在 socket 的 错误消息队列中。
3.2.2.3 用户空间 获取 传入、传出 网络包 软件时间戳 的 过程
3.2.2.3.1 使能 传入、传出 网络包 软件时间戳
int flags = SOF_TIMESTAMPING_TX_SOFTWARE |SOF_TIMESTAMPING_RX_SOFTWARE |SOF_TIMESTAMPING_SOFTWARE;
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags));if (level == SOL_SOCKET)err =  sock_setsockopt(sock, level, optname, optval, optlen);...switch (optname) {...case SO_TIMESTAMPING:sk->sk_tsflags = val;if (val & SOF_TIMESTAMPING_RX_SOFTWARE)sock_enable_timestamp(sk, SOCK_TIMESTAMPING_RX_SOFTWARE); /* 启用 sock 软件时间戳 */if (!sock_flag(sk, flag)) {sock_set_flag(sk, flag);if (sock_needs_netstamp(sk) &&!(previous_flags & SK_FLAGS_TIMESTAMP))net_enable_timestamp(); /* 启用网络软件时间戳,将 netstamp_needed 置为 true */}else......}else...
3.2.2.3.2 读取 传出网络包 的 软件时间戳

参看 3.2.1.3.3.2 读取 传出网络包 的 硬件时间戳,系统保持了读取软硬件时间戳接口和方式的一致性。

3.2.2.3.3 读取 传入网络包 的 软件时间戳

参看 3.2.1.3.3.3 读取 传入网络包 的 硬件时间戳,系统保持了读取软硬件时间戳接口和方式的一致性。

3.2.3 PTP 硬件时钟 和 系统时钟 CLOCK_REALTIME 时间戳 对比

PTP 硬件时钟系统时钟 CLOCK_REALTIME 各自提供的 硬件、软件 时间戳,可以使用相同的系统接口进行访问,但很明显 硬件时间戳 具有更高的精度,对系统消耗更小。

3.3 Linux PTP 协议栈:用户空间部分

对 Linux PTP 协议栈用户空间部分,我们以 Linux 下常见实现 linuxptp 为例来进行说明。从 3.1 小节了解到,Linux PTP 协议栈用户空间 部分的任务是处理 L4 层PTP 数据报,然后提取分析这些数据报的时间戳,然后通过调整 存在的 PTP 硬件时钟系统时钟 CLOKC_REALTIME(PTP 硬件时钟不存在的情形) 达到与 主时钟(master) 同步的目的。
Linux PTP 是一个工具集合,最核心的工具是 ptp4l ,它完成了 Linux PTP 协议栈用户空间的工作。解析来以 ptp4l 的代码为例,来分析 Linux PTP 协议栈用户空间的工作细节。ptp4l 实现了 普通时钟(OC: Ordinary Clock)透明时钟(TC: Transparent Clock)边界时钟(BC: Boundary Clock),本文只关注 普通时钟(OC: Ordinary Clock) 部分。

3.3.1 linuxptp 的配置

在开始后续的讨论之前,先来看一看 linuxptp 的配置的配置。ptp4l 的配置是一个 3级 结构。首先,ptp4l 在代码内部内置了一组默认配置:

/* linuxptp/config.c */struct config_item config_tab[] = {...PORT_ITEM_ENU("BMCA", BMCA_PTP, bmca_enu),...GLOB_ITEM_INT("clientOnly", 0, 0, 1),...GLOB_ITEM_ENU("clock_servo", CLOCK_SERVO_PI, clock_servo_enu),GLOB_ITEM_ENU("clock_type", CLOCK_TYPE_ORDINARY, clock_type_enu),...PORT_ITEM_ENU("delay_mechanism", DM_E2E, delay_mech_enu), /* -E */...PORT_ITEM_ENU("network_transport", TRANS_UDP_IPV4, nw_trans_enu), /* -2 (L2), -4 (UDPv4), -6 (UDPv6) */...GLOB_ITEM_ENU("time_stamping", TS_HARDWARE, timestamping_enu), /* -H, -S, -L */PORT_ITEM_INT("transportSpecific", 0, 0, 0x0F),...
};

其次,ptp4l 的命令行参数会覆盖默认配置表 config_tab[] 中的同名配置项的默认配置:

/* linuxptp/ptp4l.c */main()...cfg = config_create(); /* @cfg: 程序默认内置配置 config_tab[] */...while (EOF != (c = getopt_long(argc, argv, "AEP246HSLf:i:p:sl:mqvh",opts, &index))) { /* 命令行参数 覆盖 默认内置配置 @cfg 的 同名选项 */ {...}/* 配置文件 的 配置 覆盖 默认内置配置 @cfg 和 命令行参数的 同名配置项 */if (config && (c = config_read(config, cfg))) {return c;}...

最后,-f 命令行选项参数指定的配置文件,又会覆盖 默认内置配置 和 命令行参数的 同名配置项。

3.3.2 使用 PTP 硬件时钟时间戳的情形

在所有的主机上,我们假设都以如下命令启动 ptp4l 程序:

ptp4l -i eth0 -H -m # -H 指示 ptp4l 使用 PTP 硬件时钟时间戳
3.3.2.1 初始化
3.3.2.1.1 打开 PTP 硬件时钟设备 和 创建处理 PTP 协议包套接字

2.3.1.2 小节了解到,IEEE 1588 v2 的 PTP 协议包分为 事件消息(EVENT Message)通用消息(General Message) 两种类型,ptp4l 分别为 事件消息(EVENT Message)通用消息(General Message) 各创建一个套接字:

main() /* linuxptp/ptp4l.c */...type = config_get_int(cfg, NULL, "clock_type"); /* CLOCK_TYPE_ORDINARY */...clock = clock_create(type, cfg, req_phc); /* linuxptp/clock.c */...enum servo_type servo = config_get_int(config, NULL, "clock_servo"); /* CLOCK_SERVO_PI */...if (config_get_int(config, NULL, "twoStepFlag")) { /* One-Step, Two-Step 模式确立 */c->dds.flags |= DDS_TWO_STEP_FLAG;}/* 时间戳方式, 默认为 TS_HARDWARE (PTP 时钟硬件时间戳),同时 -H 也可指定为 硬件时间戳 模式 */timestamping = config_get_int(config, NULL, "time_stamping");.../* Check the time stamping mode on each interface. */c->timestamping = timestamping; /* TS_HARDWARE */required_modes = clock_required_modes(c);int required_modes = 0;switch (c->timestamping) {...case TS_HARDWARE:case TS_ONESTEP:case TS_P2P1STEP:required_modes |= SOF_TIMESTAMPING_TX_HARDWARE | /* 请求 网络适配器 生成的 发送时间戳 */SOF_TIMESTAMPING_RX_HARDWARE | /* 请求 网络适配器 生成的 接收时间戳 */SOF_TIMESTAMPING_RAW_HARDWARE;break;...}return required_modes;/* * @c->timestamping 时间戳方式,要求 PTP 时钟硬件接口支持 @required_modes 特性. * 遍历所有的网络时钟接口, 看所有网络接口是否 都满足 @required_modes 特性 要求.*/STAILQ_FOREACH(iface, &config->interfaces, list) {...interface_get_tsinfo(iface); /* 通过网卡 ethtool 接口, 获取网卡 @iface 时间戳支持特性 */if (interface_tsinfo_valid(iface) &&!interface_tsmodes_supported(iface, required_modes)) {/* 网络接口不支持 硬件时间戳 */pr_err("interface '%s' does not support requested timestamping mode", interface_name(iface));return NULL;}}...if (c->free_running) {...}  else if (phc_index >= 0) {snprintf(phc, sizeof(phc), "/dev/ptp%d", phc_index);c->clkid = phc_open(phc); /* 打开 PTP 硬件时钟设备 /dev/ptp%d */clockid_t clkid;...fd = open(phc, O_RDWR);...clkid = FD_TO_CLOCKID(fd);/* check if clkid is valid */if (clock_gettime(clkid, &ts)) {close(fd);return CLOCK_INVALID;}if (clock_adjtime(clkid, &tx)) {close(fd);return CLOCK_INVALID;}return clkid; /* 返回 PTP 时钟 ID */...max_adj = phc_max_adj(c->clkid);...clockadj_init(c->clkid);}  else if (phc_device) {...}  else { /* 如: timestamping == TS_SOFTWARE */...}.../* Create the ports. */STAILQ_FOREACH(iface, &config->interfaces, list) {/* 创建 每接口的 UDP 多播套接字(EVENT + GENERAL 协议包) */if (clock_add_port(c, phc_device, phc_index, timestamping, iface)) { // 见后续 clock_add_port() 分析 ... (1)pr_err("failed to open port %s", interface_name(iface));return NULL;}}...LIST_FOREACH(p, &c->ports, list) { /* 初始化时钟 @c 上的 所有 port */port_dispatch(p, EV_INITIALIZE, 0); // 见后面 port_dispatch() 分析 ... (2)}return c;// 接上 (1): clock_add_port() 分析
clock_add_port(c, phc_device, phc_index, timestamping, iface)...p = port_open(phc_device, phc_index, timestamping,++c->last_port_number, iface, c); /* linuxptp/port.c */enum clock_type type = clock_type(clock);...struct port *p = malloc(sizeof(*p));...switch (type) {case CLOCK_TYPE_ORDINARY:case CLOCK_TYPE_BOUNDARY:p->dispatch = bc_dispatch;p->event = bc_event; /* 设定 时钟端口上 的 PTP 协议包 处理接口 */break;...}...p->trp = transport_create(cfg, config_get_int(cfg,interface_name(interface), "network_transport")); /* linuxptp/transport.c */struct transport *t = NULL;switch (type) {...case TRANS_UDP_IPV4: /* 创建 UDPv4 多播传输对象 */t = udp_transport_create();struct udp *udp = calloc(1, sizeof(*udp));...udp->t.close = udp_close;// udp_open() 用于创建两个分别用于 EVENT 和 GENERAL 类型的 PTP 协议包 套接字udp->t.open  = udp_open; udp->t.recv  = udp_recv;udp->t.send  = udp_send;udp->t.release = udp_release;udp->t.physical_addr = udp_physical_addr;udp->t.protocol_addr = udp_protocol_addr;return &udp->t;break;...}if (t) {t->type = type;t->cfg = cfg;}return t;...return p;...// 接上 (2): port_dispatch() 分析
port_dispatch(p, EV_INITIALIZE, 0); // 初始化 时钟 上的一个 portport_state_update(p, event, mdiff)/** master: ptp_fsm()* slave : ptp_slave_fsm()*/enum port_state next = p->state_machine(p->state, event, mdiff); /* 端口状态为 PS_INITIALIZING */...if (PS_INITIALIZING == next) {...port_initialize(p).../* 创建两个分别用于 EVENT 和 GENERAL 类型的 PTP 协议包 套接字 */transport_open(p->trp, p->iface, &p->fda, p->timestamping)udp_open() /* linuxptp/udp.c */.../* PTP-primary 多播地址:224.0.1.129 */if (!inet_aton(PTP_PRIMARY_MCAST_IPADDR, &mcast_addr[MC_PRIMARY]))return -1;/* PTP pdelay 多播地址:224.0.0.107 */if (!inet_aton(PTP_PDELAY_MCAST_IPADDR, &mcast_addr[MC_PDELAY]))return -1;/* PTP EVENT 类型协议包 多播套接字 创建 */efd = open_socket(name, mcast_addr, EVENT_PORT, ttl);/* PTP GENERAL 类型协议包 多播套接字 创建 */gfd = open_socket(name, mcast_addr, GENERAL_PORT, ttl);/* 启用套接字 PTP EVENT 类型协议包 多播套接字 接收 + 发送 的 时间戳 */if (sk_timestamping_init(efd, interface_label(iface), ts_type, TRANS_UDP_IPV4, interface_get_vclock(iface))) // 见后续分析 ... (3)goto no_timestamping;/* 启用套接字 PTP GENERAL 类型协议包 多播套接字 接收 的 时间戳 */if (sk_general_init(gfd)) // 见后续分析 ... (4)goto no_timestamping;......next = p->state_machine(next, event, 0); /* 端口状态切换为 PS_LISTENING */}// 接上面 (3) 处分析
sk_timestamping_init(efd, interface_label(iface), ts_type, TRANS_UDP_IPV4, interface_get_vclock(iface)) /* linuxptp/sk.c */int err, filter1, filter2 = 0, flags, tx_type = HWTSTAMP_TX_ON;struct so_timestamping timestamping;switch (type) {...case TS_HARDWARE:case TS_ONESTEP:case TS_P2P1STEP:flags = SOF_TIMESTAMPING_TX_HARDWARE |SOF_TIMESTAMPING_RX_HARDWARE |SOF_TIMESTAMPING_RAW_HARDWARE;break;...}if (type != TS_SOFTWARE) {filter1 = HWTSTAMP_FILTER_PTP_V2_EVENT;switch (type) {...case TS_HARDWARE:case TS_LEGACY_HW:tx_type = HWTSTAMP_TX_ON;break;...}switch (transport) {case TRANS_UDP_IPV4:case TRANS_UDP_IPV6:filter2 = HWTSTAMP_FILTER_PTP_V2_L4_EVENT;break;...}err = hwts_init(fd, device, filter1, filter2, tx_type);struct hwtstamp_config cfg;switch (sk_hwts_filter_mode) {...case HWTS_FILTER_NORMAL:cfg.tx_type   = tx_type;cfg.rx_filter = orig_rx_filter = rx_filter;err = ioctl(fd, SIOCSHWTSTAMP, &ifreq); /* 初始化、启用 PTP 硬件时钟 的 硬件时间戳 功能 */...break;...}...}timestamping.flags = flags;timestamping.bind_phc = vclock;if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING,&timestamping, sizeof(timestamping)) < 0) { /* 启用 socket 硬件时间戳 */...}flags = 1;if (setsockopt(fd, SOL_SOCKET, SO_SELECT_ERR_QUEUE,&flags, sizeof(flags)) < 0) {...}/* Enable the sk_check_fupsync option, perhaps. */if (sk_general_init(fd)) { // 见后续分析 ... (5)return -1;}return 0;// 接前面 (4), (5) 处
sk_general_init(fd)int on = sk_check_fupsync ? 1 : 0;if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPNS, &on, sizeof(on)) < 0) { // 启用 socket 的 收取包 的 时间戳...}return 0;

上面的代码核心可以总结为:
通过如下代码片段,用户空间可以请求内核在上述进、出时机,对 PTP 数据报打上时间戳:

// 1. 配置启用 PTP 硬件时钟时间戳功能
ioctl(fd, SIOCSHWTSTAMP, &ifreq);// 2. 启用 PTP 报文处理 UDPv4 套接字的时间戳
unsigned int flags = SOF_TIMESTAMPING_TX_HARDWARE |SOF_TIMESTAMPING_RX_HARDWARE |SOF_TIMESTAMPING_RAW_HARDWARE;
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags)); // 启用 EVENT 数据报 传入、传出网络包 时间戳int on = sk_check_fupsync ? 1 : 0;
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPNS, &on, sizeof(on)); // 启用 GENERAL 数据报 进入包 的 时间戳// 3. 开启初始化 PTP 硬件时钟设备,用于后续时钟同步操作
int fd = open("/dev/ptpX", O_RDWR);
...
3.3.2.2 处理 PTP 协议包
3.3.2.2.1 获取 Toffset

2.1 节的时钟同步原理了解到,获取 T o f f f s e t {T}_{offfset} Tofffset 是通过 Sync, Follow_Up, Delay_Req, Delay_Resp 这 4 个 PTP 协议包,得到 T1, T2, T3, T4 这 4 个时间戳,然后计算出 T o f f f s e t {T}_{offfset} Tofffset,然后通过 T o f f f s e t {T}_{offfset} Tofffset 来同步 slave 时钟 到 master 时钟。来看 ptp4l 的代码实现细节(我们假定使用 Two-Step 模式,One-Step 模式的流程基本相似,读者可自行分析):

/*** 1. master 时钟先发送 Sync 给 slave, 并记录发送 Sync 包 的 时间戳 T1 ,*    然后从 Follow_Up 包 将 T1 发送给 slave 。*/
main() /* linuxptp/ptp4l.c */...while (is_running()) {if (clock_poll(clock)) /* 读取 + 处理事件数据 */break;}...clock_poll(clock) /* linuxptp/clock.c */...clock_check_pollfd(c); /* 将套接字句柄添加到 clock::pollfd */cnt = poll(c->pollfd, (c->nports + 2) * N_CLOCK_PFD, -1); /* 从 UDPv4 EVENT, GENERAL 套接字查询事件数据 */...LIST_FOREACH(p, &c->ports, list) {/* Let the ports handle their events. */for (i = 0; i < N_POLLFD; i++) {if (cur[i].revents & (POLLIN|POLLPRI|POLLERR)) {if (cur[i].revents & POLLERR) {...} else { /* 读取到数据 */event = port_event(p, i); /* 处理事件数据 */p->event(p, fd_index) = bc_event(p, fd_index); /* linuxptp/port.c */...switch (fd_index) { /* FD_EVENT, FD_GENERAL, ... */...case FD_SYNC_TX_TIMER: /* master 通过定时器 定时向 slave 发送 SYNC */								pr_debug("%s: master sync timeout", p->log_name);port_set_sync_tx_tmo(p); /* 重启定时器 */// 见后续分析 ... (6)return port_tx_sync(p, NULL, p->seqnum.sync++) ?EV_FAULT_DETECTED : EV_NONE;...}}}}}// 接上面 (6) 处分析
port_tx_sync(p, NULL, p->seqnum.sync++) /* master 向 slave 发送 Sync 消息 */struct ptp_message *msg, *fup;int err, event;switch (p->timestamping) {case TS_SOFTWARE:case TS_LEGACY_HW:case TS_HARDWARE:event = TRANS_EVENT; /* 使用处理 事件类型 的 PTP 协议包的套接字 */break;...}...msg = msg_allocate(); // Sync...fup = msg_allocate(); // Follow_Up...msg->hwts.type = p->timestamping;/* 构建 Sync 消息头部 */msg->header.tsmt               = SYNC | p->transportSpecific;msg->header.ver                = ptp_hdr_ver;.../* 先发送 Sync , 后保存 T1, T1 将在 Follo_Up 里发送给 slave */err = port_prepare_and_send(p, msg, event);...if (msg_unicast(msg)) {...} else {cnt = transport_send(p->trp, &p->fda, event, msg);t->send(t, fda, event, 0, msg, len, NULL, &msg->hwts);udp_send() /* linuxptp/udp.c */.../* 发送 Sync 包 */cnt = sendto(fd, buf, len, 0, &addr->sa, sizeof(addr->sin));.../* 同时,取回 Sync 包发送的硬件时间戳 */return event == TRANS_EVENT ? sk_receive(fd, junk, len, NULL, hwts, MSG_ERRQUEUE) : cnt;struct cmsghdr *cm;...cnt = recvmsg(fd, &msg, flags);...for (cm = CMSG_FIRSTHDR(&msg); cm != NULL; cm = CMSG_NXTHDR(&msg, cm)) {level = cm->cmsg_level;type  = cm->cmsg_type;if (SOL_SOCKET == level && SO_TIMESTAMPING == type) {...ts = (struct timespec *) CMSG_DATA(cm);}...switch (hwts->type) {...case TS_HARDWARE:case TS_ONESTEP:case TS_P2P1STEP:/* 硬件时间戳在 ts[2] */hwts->ts = timespec_to_tmv(ts[2]);break;...}}}.../** Send the follow up message right away.*/fup->hwts.type = p->timestamping;/* 构建 Follow_Up 消息头部 */fup->header.tsmt               = FOLLOW_UP | p->transportSpecific;fup->header.ver                = ptp_hdr_ver;.../* 这一步是将上面得到的 时间戳 放入 Follow_Up 中,这个时刻就是 T1 */fup->follow_up.preciseOriginTimestamp = tmv_to_Timestamp(msg->hwts.ts);.../* 将 T1 从 Follow_Up 发送给 slave */err = port_prepare_and_send(p, fup, TRANS_GENERAL);/*** 2. slave 收取 Sync 包,并记录收到 Sync 包 的 时间戳 T2*    slave 收取 Follow_Up 包,提取 时间戳 T1*/
// 前面逻辑都是同 1. 一样:
// main() -> clock_poll() --> poll()
//                         |_ bc_event()p->event(p, fd_index) = bc_event(p, fd_index); /* linuxptp/port.c */...cnt = transport_recv(p->trp, fd, msg); /* 读取 PTP 消息 */.../** . slave 处理 Sync: 记录收到 Sync 的时间 T2 到 @msg* . slave 处理 Follow_Up: 记录 Follow_Up 消息的 时间戳消息数据 T1 到 @msg* ......*/err = msg_post_recv(msg, cnt);.../* 处理 PTP 协议消息 */switch (msg_type(msg)) {case SYNC: /* slave 处理 master 发送的 Sync 消息 */process_sync(p, msg);break;...case FOLLOW_UP:process_follow_up(p, msg); /* slave 处理 Follow_Up 消息 */break;...}.../*** 3. slave 向 master 发送 Delay_Req 包,并记录 Delay_Req 包 发送时间戳 T3*/
// 前面逻辑都是同 1. 一样:
// main() -> clock_poll() --> poll()
//                         |_ bc_event()
p->event(p, fd_index) = bc_event(p, fd_index); /* linuxptp/port.c */...switch (fd_index) { /* FD_EVENT, FD_GENERAL, ... */...case FD_DELAY_TIMER:pr_debug("%s: delay timeout", p->log_name);port_set_delay_tmo(p); /* 重启定时器 */delay_req_prune(p);...if (port_delay_request(p)) { /* 向 master 发送 Delay_Req 并记录 发送时间 T3 */return EV_FAULT_DETECTED;}......}/*** 4. master 收取 Delay_Req 包,并记录 Delay_Req 包 收取 时间戳 T4,然后向 *    slave 发送带有 T4 的 Delay_Resp 包*/
// 前面逻辑都是同 1. 一样:
// main() -> clock_poll() --> poll()
//                         |_ bc_event()
p->event(p, fd_index) = bc_event(p, fd_index); /* linuxptp/port.c */...cnt = transport_recv(p->trp, fd, msg); /* 读取 PTP 消息 *//** . master 处理 Delay_Req: 记录收到 Delay_Req 的时间 T4 到 @msg* ......*/err = msg_post_recv(msg, cnt);.../* 处理 PTP 协议消息 */switch (msg_type(msg)) {.../** master 处理 slave 发送的 Delay_Req 消息: * 记录收到 Delay_Req 消息的时间 T4, 然后将 T4 通过 Delay_Resp * 消息发送给 slave 。*/case DELAY_REQ:if (process_delay_req(p, msg))event = EV_FAULT_DETECTED;break;...}/*** 5. slave 收取 master 的 Delay_Resp 包,从中提取 T4,然后计算处 Toffset,*   然后根据 Toffset 调整 PTP 硬件时钟*/
// 前面逻辑都是同 1. 一样:
// main() -> clock_poll() --> poll()
//                         |_ bc_event()
p->event(p, fd_index) = bc_event(p, fd_index); /* linuxptp/port.c */...cnt = transport_recv(p->trp, fd, msg); /* 读取 PTP 消息 *//** . master 处理 Delay_Req: 记录收到 Delay_Req 的时间 T4 到 @msg* ......*/err = msg_post_recv(msg, cnt);/* 处理 PTP 协议消息 */...switch (msg_type(msg)) {...case PDELAY_RESP:if (process_pdelay_resp(p, msg))event = EV_FAULT_DETECTED;break;...}
3.3.2.2.2 用 Toffset 同步 PTP 硬件时钟

有几种代码路径触发时钟的同步,最终都会进入函数 port_synchronize()

/* linuxptp/port.c */
static void port_synchronize(struct port *p,uint16_t seqid,tmv_t ingress_ts,struct timestamp origin_ts,Integer64 correction1, Integer64 correction2,Integer8 sync_interval)
{...last_state = clock_servo_state(p->clock);state = clock_synchronize(p->clock, t2, t1c); /* 同步时钟 */switch (state) {...case SERVO_LOCKED: /* 时钟同步达到稳定状态 */port_dispatch(p, EV_MASTER_CLOCK_SELECTED, 0);break;...}
}

3.3.3 使用 系统时钟 CLOCK_REALTIME 时间戳的情形

在所有主机上,假定都使用如下命令启动 ptp4l 程序:

ptp4l -i eth0 -m -S

ptp4l 使用 系统时钟 CLOCK_REALTIME 时间戳,对比 使用 PTP 硬件时钟的情形,没有太大的差异,只不过时钟由 PTP 硬件时钟 变成了 系统时钟 CLOCK_REALTIME ,在此就不再赘述。

3.3.4 ptp4l 使用范例

masterslave 主机上都用如下命令启动 ptp4l

ptp4l -i eth0 -m -S

master 时钟的日志如下:

# ptp4l -i eth0 -m -S
ptp4l[179.555]: port 1: INITIALIZING to LISTENING on INIT_COMPLETE
ptp4l[179.556]: port 0: INITIALIZING to LISTENING on INIT_COMPLETE
ptp4l[186.827]: port 1: LISTENING to MASTER on ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES
ptp4l[186.827]: selected local clock 2aea0d.fffe.f3ab18 as best master
ptp4l[186.827]: port 1: assuming the grand master role

slave 时钟的日志如下:

# ptp4l -i eth0 -m -S
ptp4l[170.227]: port 1: INITIALIZING to LISTENING on INIT_COMPLETE
ptp4l[170.228]: port 0: INITIALIZING to LISTENING on INIT_COMPLETE
ptp4l[177.563]: port 1: LISTENING to MASTER on ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES
ptp4l[177.563]: selected local clock 2aea0d.fffe.f3ab18 as best master
ptp4l[177.563]: port 1: assuming the grand master role
ptp4l[180.239]: port 1: new foreign master 16ca5c.fffe.816730-1
ptp4l[184.238]: selected best master clock 16ca5c.fffe.816730
ptp4l[184.239]: foreign master not using PTP timescale
ptp4l[184.239]: port 1: MASTER to UNCALIBRATED on RS_SLAVE
ptp4l[186.238]: master offset 53818677672 s0 freq      +0 path delay    289479
ptp4l[187.238]: master offset 53818676505 s0 freq      +0 path delay    289479
ptp4l[188.238]: master offset 53818681755 s0 freq      +0 path delay    281604
ptp4l[189.238]: master offset 53818677161 s0 freq      +0 path delay    280948
ptp4l[190.238]: master offset 53818682775 s0 freq      +0 path delay    280292
ptp4l[191.238]: master offset 53818676942 s0 freq      +0 path delay    280292
ptp4l[192.238]: master offset 53818672786 s0 freq      +0 path delay    280656
ptp4l[193.238]: master offset 53818669942 s0 freq      +0 path delay    280292
ptp4l[194.238]: master offset 53818670818 s0 freq      +0 path delay    278833
ptp4l[195.238]: master offset 53818669359 s0 freq      +0 path delay    277375
ptp4l[196.238]: master offset 53818670600 s0 freq      +0 path delay    276426
ptp4l[197.238]: master offset 53818665058 s0 freq      +0 path delay    276426
ptp4l[198.238]: master offset 53818665933 s0 freq      +0 path delay    275843
ptp4l[199.238]: master offset 53818658349 s0 freq      +0 path delay    276426
ptp4l[200.239]: master offset 53818667099 s0 freq      +0 path delay    276426
ptp4l[201.239]: master offset 53818656600 s0 freq      +0 path delay    276426
ptp4l[202.239]: failed to step clock: Invalid argument
ptp4l[202.239]: master offset 53818653755 s1 freq   -1495 path delay    276937
ptp4l[203.239]: master offset 53818655541 s2 freq +100000000 path delay    276937
ptp4l[203.239]: port 1: UNCALIBRATED to SLAVE on MASTER_CLOCK_SELECTED
ptp4l[204.139]: master offset 53718671144 s2 freq +100000000 path delay    277156
ptp4l[205.039]: master offset 53618659110 s2 freq +100000000 path delay    277156
ptp4l[205.939]: master offset 53518652867 s2 freq +100000000 path delay    279125
ptp4l[206.839]: master offset 53418641504 s2 freq +100000000 path delay    279125

slave 的日志看到,已经达到了 s2 (即 SERVO_LOCKED 状态),即同步到了稳定状态,之后会根据时间戳做细微调整,继续保持和 master 时钟的同步。

4. Linux PTP 相关工具

4.1 ethtool 查询

$ ethtool -T eth0
Time stamping parameters for eth0:
Capabilities:software-transmit     (SOF_TIMESTAMPING_TX_SOFTWARE)software-receive      (SOF_TIMESTAMPING_RX_SOFTWARE)software-system-clock (SOF_TIMESTAMPING_SOFTWARE)
PTP Hardware Clock: none
Hardware Transmit Timestamp Modes: none
Hardware Receive Filter Modes: none

上述命令的内部实现为如下代码片段:

socket(AF_INET, SOCK_DGRAM, IPPROTO_IP) = 3
ioctl(3, SIOCETHTOOL, ETHTOOL_GET_TS_INFO...)   = 0

4.2 phc2sys

可以通过 phc2sys 将 PTP 硬件时钟的时间,同步到系统时钟 CLOCK_REALTIME ,或者反过来也可以。

4.3 其它 linuxptp 工具

在这里插入图片描述

5. 参考资料

IEEE 1588 协议相关文档
[1] IEEE1588Version2 IEEE 1588 Version 2
[2] White Paper Precision Clock Synchronization The Standard IEEE 1588
[3] IEEE1588v2 透明时钟研究与实现
[4] 时钟同步原理
[5] 比NTP还牛逼的时间同步协议:1588v2,亚微秒级!
[6] IEEE-1588 Standard for a Precision Clock Synchronization Protocol for Networked Measurement and Control Systems
[7] IEEE1588 verision 2 报文介绍
[8] 1588v2(PTP)报文通用格式
[9] IEEE 1588 报文封装

Linux 内核 PTP 相关文档
[10] 内核文档: timestamping
[11] Precision Time Protocol on Linux ~ Introduction to linuxptp
[12] PTP Clock Manager for Linux

本文涉及的支持 IEEE 1588 的芯片文档
[13] Intel Ethernet Controller I350 Datasheet
[14] DP83640 Precision PHYTER

LinuxPTP 工具相关文档
[15] LinuxPTP Project
[16] ptp4l(8): PTP Boundary/Ordinary/Transparent Clock
[17] phc2sys(8): synchronize two or more clocks
[18] 第 20 章 使用 ptp4l 配置 PTP
[19] linux ptp /ptp4l PTP 时钟如何同步配置
[20] 用ptp4l和phc2sys实现系统时钟同步
[21] Linuxptp使用总结
[22] Synchronizing Time with Linux PTP
[23] 更精准的时延:使用软件时间戳和硬件时间戳
[24] 网络时钟同步IEEE 1588/802.1AS
[25] 如何在 Linux 使用 PTP 进行时间同步
[26] Linux PTP 高精度时间同步实践

[27] 以 ptp4l、E2E 为例的 Linuxptp 代码分析
[28] [补充:以 ptp4l、E2E 为例的 Linuxptp 代码分析
[29] 剖析Linuxptp中ptp4l实现–OC

[30] IPv4 Multicast Address Space Registry

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/669329.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【论文研读】Better Together:Unifying Datalog and Equality Saturation

最近研究ReassociatePass整的头大&#xff0c;翻两篇Datalog的论文看看。 今天看的一篇是比较新的文章&#xff0c;23年4月贴到arxiv上的。 本文的主要贡献是提出了egglog,将Datalog和Eqsat结合起来&#xff0c;继承了Datalog的efficient incremental execution, cooperating a…

【前端web入门第四天】02 CSS三大特性+背景图

文章目录: 1. CSS三大特性 1.1继承性 1.2 层叠性 1.3 优先级 1.3.1 优先级1.3.2 优先级-叠加计算规则 2. 背景图 2.1 背景属性2.2 背景图2.3 背景图的平铺方式2.4 背景图位置2.5 背景图缩放2.6 背景图固定2.7 背景复合属性 1. CSS三大特性 1.1继承性 什么是继承性? 子级默…

前端实现搜索框筛选

效果图 页面解析 是一个input输入框和一个button按钮组成输入框查询 内容是一个折叠面板 html代码 <div class"left-content-box"><div class"colum-search"><el-input v-model"columKey" clearable placeholder"请输入关…

redis大数据统计之hyperloglog,GEO,Bitmap

目录 一、亿级系统常见的四中统计 1、聚合统计 2、排序统计 3、二值统计 4、基数统计 二、hyperloglog 去重的方式有哪些&#xff1f; hyperloglog实战演示 1、技术选型 2、代码演示 三、GEO GEO实战演示 四、Bitmap 一、亿级系统常见的四中统计 1、聚合统计 聚…

STM32F407移植OpenHarmony笔记7

继上一篇笔记&#xff0c;成功启动了liteos_m内核&#xff0c;可以创建线程了&#xff0c;也能看到shell控制台了。 今天研究文件系统&#xff0c;让控制台相关文件命令如mkdir和ls能工作。 liteos_m内核支持fatfs和littlefs两个文件系统&#xff0c; fatfs适用于SD卡&#xff…

将11.x.x升级至16.x.x不成功的一系列问题(二)node-sass sass-loader需安装指定版本

nvm 版本切换搞定了 咱就是说 那个node-sass好像有点毛病 还得指定对应的loaber版本 node.js 16.18.1对应的如下 “node-sass”: “^6.0.1”, “sass-loader”: “^10.0.1”, node.js 11.8.0 对应的如下 “node-sass”: “^4.14.1”, “sass-loader”: “^7.3.1”, 老项目即…

python-题库篇-数学

文章目录 求最大公约数和最小公倍数斐波那契数列求和运算求前n阶乘的和求年龄 求最大公约数和最小公倍数 两个数的最大公约数是两个数的公共因子中最大的那个数&#xff1b;两个数的最小公倍数 则是能够同时被两个数整除的最小的那个数。 输入&#xff1a;&#xff08;120 和…

零基础如何入门渗透测试2024年最新版,保姆级教程,小白必看!

转眼间&#xff0c;从大三开始学安全&#xff0c;到现在也有五年了&#xff0c;也算是对渗透测试有一定理解&#xff0c;今天也是出一篇入门教程&#xff0c;以实操为主&#xff0c;希望可以帮助到想入门渗透测试的小白。如果觉得有用&#xff0c;可以给我点个赞和收藏&#xf…

Android: 深入理解 ‘companion object {}‘

Android: 深入理解 ‘companion object {}’ Kotlin是一种现代的、静态类型的编程语言&#xff0c;它在设计时充分考虑了开发者的生产力和代码的可读性。其中一个独特的特性就是companion object。在本篇博客中&#xff0c;我们将深入探讨这个特性&#xff0c;理解它的工作原理…

Java21 + SpringBoot3集成七牛云对象存储OSS,实现文件上传

文章目录 前言实现步骤引入maven依赖修改配置文件创建七牛云配置类创建文件操作服务类创建文件操作控制器前端实现运行效果 总结 前言 近日心血来潮想做一个开源项目&#xff0c;目标是做一款可以适配多端、功能完备的模板工程&#xff0c;包含后台管理系统和前台系统&#xf…

EOF和0区别

题目描述 KiKi学习了循环&#xff0c;BoBo老师给他出了一系列打印图案的练习&#xff0c;该任务是打印用“*”组成的X形图案。 输入描述&#xff1a; 多组输入&#xff0c;一个整数&#xff08;2~20&#xff09;&#xff0c;表示输出的行数&#xff0c;也表示组成“X”的反斜…

你的歌声婉转入云霄

可爱的一朵玫瑰花 - 吕继宏 可爱的一朵玫瑰花塞地玛丽亚 可爱的一朵玫瑰花塞地玛丽亚 那天我在山上打猎骑着马&#xff08;人善被人欺马善被人骑&#xff09; 正当你在山下歌唱婉转入云霄 歌声使我迷了路 我从山坡滚下 哎呀呀 你的歌声婉转入云霄 强壮的青年哈萨克伊万杜达尔 …

【八大排序】选择排序 | 堆排序 + 图文详解!!

&#x1f4f7; 江池俊&#xff1a; 个人主页 &#x1f525;个人专栏&#xff1a; ✅数据结构冒险记 ✅C语言进阶之路 &#x1f305; 有航道的人&#xff0c;再渺小也不会迷途。 文章目录 一、选择排序1.1 基本思想1.2 算法步骤 动图演示1.3 代码实现1.4 选择排序特性总结 二…

C/C++内存管理的底层调用逻辑

✨Blog&#xff1a;&#x1f970;不会敲代码的小张:)&#x1f970; &#x1f251;推荐专栏&#xff1a;C语言&#x1f92a;、Cpp&#x1f636;‍&#x1f32b;️、数据结构初阶&#x1f480; &#x1f4bd;座右铭&#xff1a;“記住&#xff0c;每一天都是一個新的開始&#x1…

【TCP/IP】用户访问一个购物网站时TCP/IP五层参考模型中每一层的功能

当用户访问一个购物网站时&#xff0c;网络上的每一层都会涉及不同的协议&#xff0c;具体网络模型如下图所示。 以下是每个网络层及其相关的协议示例&#xff1a; 物理层&#xff1a;负责将比特流传输到物理媒介上&#xff0c;例如电缆或无线信号。所以在物理层&#xff0c;可…

vue3项目实现预览图片、旋转图片功能

一、需求&#xff1a; 在点击图片时&#xff0c;能预览大图&#xff0c;弹出一个包含旋转图片功能按钮的弹窗。用户可通过点击按钮实现对图片的旋转操作 二、思路&#xff1a; 点击图片预览&#xff1a; 用户通过点击图片触发预览功能。接收图片的 URL&#xff0c;弹出一个模…

【GameFramework框架】四、GameFramework框架内置模块

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 【GameFramework框架】系列教程目录&#xff1a; https://blog.csdn.net/q7…

问题:下列关于海关统计项目的表述,正确的有:A.进出境货物的统计重量和数量应以报关单位申报的重量和数 #笔记#职场发展#媒体

问题&#xff1a;下列关于海关统计项目的表述&#xff0c;正确的有&#xff1a;A&#xff0e;进出境货物的统计重量和数量应以报关单位申报的重量和数 下列关于海关统计项目的表述&#xff0c;正确的有&#xff1a; A&#xff0e;进出境货物的统计重量和数量应以报关单位申报的…

echarts使用之地图(五)

1 基本使用 百度地图 API : 使用百度地图的 api , 它能够在线联网展示地图 , 百度地图需要申请 ak 矢量地图 : 可以离线展示地图 , 需要开发者准备矢量地图数据。本文使用该方式。 json格式的数据如下&#xff1a; 格式参照&#xff1a;GeoJSON <!DOCTYPE html&…

Java实现数据可视化的智慧河南大屏 JAVA+Vue+SpringBoot+MySQL

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统展示四、核心代码4.1 数据模块 A4.2 数据模块 B4.3 数据模块 C4.4 数据模块 D4.5 数据模块 E 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的数据可视化的智慧河南大屏&#xff0c;包含了GDP、…