Linux 网络:PTP 简介

news2024/11/18 13:58:44

文章目录

  • 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_Resp

0x04-7: Reserved

通用消息(General Message):
0x08: Follow_Up
0x09: Delay_Resp
0x0A: Pdelay_Resp_Follow_Up
0x0B: Announce
0x0C: Signaling
0x0D: Management

0x0E-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 */
					else
						skb->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_TSTAMP
 					if (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); // 初始化 时钟 上的一个 port
	port_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.coloradmin.cn/o/1435275.html

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

相关文章

Redis 命令大全

文章目录 启动与连接Key&#xff08;键&#xff09;相关命令String&#xff08;字符串&#xff09;Hash&#xff08;哈希&#xff09;List&#xff08;列表&#xff09;Set&#xff08;集合&#xff09;Sorted Set&#xff08;有序集合&#xff09;其他常见命令HyperLogLog&…

FPGA解码MIPI视频:Xilinx Artix7-35T低端FPGA,基于MIPI CSI-2 RX Subsystem架构实现,提供工程源码和技术支持

目录 1、前言免责声明 2、相关方案推荐我这里已有的 MIPI 编解码方案本方案在Xilinx Artix7-100T上解码MIPI视频的应用本方案在Xilinx Kintex7上解码MIPI视频的应用本方案在Xilinx Zynq7000上解码MIPI视频的应用本方案在Xilinx Zynq UltraScale上解码MIPI视频的应用纯VHDL代码解…

docker-学习-5

docker-学习第五天 docker-学习第五天1. 昨天的练习回顾1.1. 练习11.2. 练习2 2. 命令2.1. 看镜像的详细信息 3. Dockerfile指令3.1. 常见的指令3.2. ENTRYPOINT和CMD的区别3.3. RUN中的set指令 4. 镜像的原理4.1. 为什么 Docker 镜像要采用这种分层结构呢&#xff1f;4.2. doc…

无人机集群协同导航构型自适应选择算法

无人机集群协同导航构型自适应选择算法 Evandworld E-mail&#xff1a;evandworldfoxmail.com 摘要 针对卫星定位系统用于无人机集群时成本高、精度低等问题&#xff0c;本文提出一种基于卡尔曼滤波和概率的无人机集群构型自适应选择算法。在自适应扩展卡尔曼滤波的基础上&a…

忘记 RAG:拥抱Agent设计,让 ChatGPT 更智能更贴近实际

RAG&#xff08;检索增强生成&#xff09;设计模式通常用于开发特定数据领域的基于实际情况的ChatGPT。 然而&#xff0c;重点主要是改进检索工具的效率&#xff0c;如嵌入式搜索、混合搜索和微调嵌入&#xff0c;而不是智能搜索。 这篇文章介绍了一种新的方法&#xff0c;灵感…

Windows11安装运行Linux(Ubuntu)

一、安装windows支持 输入windows打开界面 选择虚拟机监控程序平台、适用于linux的子系统、虚拟机平台 在 Windows 系统中&#xff0c;"虚拟机平台"和"虚拟机监控程序平台"是两个与虚拟化相关的功能&#xff0c;但它们各自有着不同的作用和用途。 虚拟机…

如何使用VSCode上运行Jupyter,详细案例过程出可视化图

Python作为最受AI喜欢的语言之一&#xff0c;我们与大家共同学习下如何在VS Code上运行Jupyter&#xff0c;并且用简单案例实现出图。 环境 VS Code version: 1.80.1 Python: 3.12.0 小白安装过程&#xff1a; 在准备好基础环境&#xff0c;小白心想&#xff0c;AI可是霸占科…

42、WEB攻防——通用漏洞文件包含LFIRFI伪协议编码算法代码审计

文章目录 文件包含文件包含原理攻击思路文件包含分类 sessionPHP伪协议进行文件包含 文件包含 文件包含原理 文件包含其实就是引用&#xff0c;相当于C语言中的include <stdio.h>。文件包含漏洞常出现于php脚本中&#xff0c;当include($file)中的$file变量用户可控&am…

University Program VWF仿真步骤__全加器

本教程将以全加器为例&#xff0c;选择DE2-115开发板的Cyclone IV EP4CE115F29C7 FPGA&#xff0c;使用Quartus Lite v18.1&#xff0c;循序渐进的介绍如何创建Quartus工程&#xff0c;并使用Quartus Prime软件的University Program VWF工具创建波形文件&#xff0c;对全加器的…

【MATLAB源码-第134期】基于matlab的SAR合成孔径雷达成像仿真,对比CS,RD,RMA三种算法成像效果。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 合成孔径雷达&#xff08;SAR&#xff09;是一种高分辨率的雷达成像技术&#xff0c;它通过在不同的时间和位置收集目标的雷达回波数据&#xff0c;来模拟一个远大于实际物理孔径大小的雷达系统。这种技术可以在任何天气条件…

spring boot学习第九篇:操作mongo的集合和集合中的数据

1、安装好了Mongodb 参考&#xff1a;ubuntu安装mongod、配置用户访问、添删改查-CSDN博客 2、pom.xml文件内容如下&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns…

记录下ibus-libpinyin输入法的重新安装

目前的版本为&#xff1a; 首先把现在的ibus-libpinyin卸了 sudo apt-get --purge remove ibus-libpinyin sudo apt-get autoremove 安装教程请参考 Installation libpinyin/ibus-libpinyin Wiki GitHub yilai sudo apt install pkg-config sudo apt-get install lib…

Micro micro controller一览

https://www.microchip.com.cn/&#xff0c; Microchip中文网站 https://www.microchip.com.cn/newcommunity/index.php?mSearch&adosearch&moduleDownload&keyworddsPIC33&p3 Microcontrollers and microProcessors dsPIC33 Digital Signal Controllers (D…

客户端会话技术-Cookie

一、会话技术 1.1 概述 会话&#xff1a;一次会话中包含多次**请求和响应** 一次会话&#xff1a;浏览器第一次给服务器资源发送请求&#xff0c;此时会话建立&#xff0c;直到有一方断开为止 会话的功能&#xff1a;在一次会话的范围内的多次请求间&#xff0c;共享数据 …

升级Oracle 单实例数据库19.3到19.22

需求 我的Oracle Database Vagrant Box初始版本为19.3&#xff0c;需要升级到最新的RU&#xff0c;当前为19.22。 以下操作时间为为2024年2月5日。 补丁下载 补丁下载文档参见MOS文档&#xff1a;Primary Note for Database Proactive Patch Program (Doc ID 888.1)。 补丁…

超越sd的绘图开源软件fooocus

Fooocus 是一款图像生成软件&#xff08;基于Gradio&#xff09;。 Fooocus 是对 Stable Diffusion 和 Midjourney 设计的重新思考&#xff1a; 学习自Stable Diffusion&#xff0c;该软件是离线、开源、免费的。 从Midjourney了解到&#xff0c;不需要手动调整&#xff0c;用户…

【【制作100个unity游戏之24】unity制作一个3D动物AI生态系统游戏(附项目源码)

最终效果 文章目录 最终效果前言导入AI导航系统导航烘培添加羊添加捕食者动画控制随着地面法线旋转在地形上随机生成动物不同部位颜色不同最终效果源码完结前言 欢迎来到【制作100个Unity游戏】系列!本系列将引导您一步步学习如何使用Unity开发各种类型的游戏。在这第24篇中,…

新增同步管理、操作日志模块,支持公共链接分享,DataEase开源数据可视化分析平台v2.3.0发布

2024年2月5日&#xff0c;DataEase开源数据可视化分析平台正式发布v2.3.0版本。 这一版本的功能升级包括&#xff1a;新增“同步管理”功能模块&#xff0c;用户可通过此模块&#xff0c;将传统数据库中的数据定时同步到Apache Doris中&#xff0c;让数据分析更快速&#xff1…

第5节、S曲线加减速转动【51单片机+L298N步进电机系列教程】

↑↑↑点击上方【目录】&#xff0c;查看本系列全部文章 摘要&#xff1a;本节介绍步进电机S曲线相关内容&#xff0c;总共分四个小节讨论步进电机S曲线相关内容 5-1、S曲线加减速简介   根据上节内容&#xff0c;步进电机每一段的速度可以任意设置&#xff0c;但是每一段的…

【Oracle云】基于 OKE 的 Cluster AutoScaler 扩/缩容

Oracle Kubernetes Engine&#xff08;OKE&#xff09;为用户提供了便捷而强大的Kubernetes服务&#xff0c;而集群自动伸缩&#xff08;Cluster AutoScaler&#xff09;则是OKE中的一项重要功能。该功能允许根据工作负载的需要自动调整集群的大小&#xff0c;确保资源的最佳利…