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 采用系主从层次式结构来同步时钟,实现机制如下图所示:
Linux 网络:PTP 简介_第1张图片
上图中:

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)
Linux 网络:PTP 简介_第2张图片
Linux 网络:PTP 简介_第3张图片
可以看到,普通时钟(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级级联交换设备实现时间同步的原理如下图所示:
Linux 网络:PTP 简介_第4张图片
由上图所示可得,经过 透明时钟(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),将现在使用的集线器或者交换机给替换掉。如下图所示:
Linux 网络:PTP 简介_第5张图片
相对于普通时钟只有一个 PTP 端口,边界时钟有两个以上的 PTP 端口,每个端口可以处于不同的状态。在主从时钟之间布置若干个边时钟,逐级同步,边界时钟既是上级时钟的从时钟,也是下级时钟的主时钟,由不同的端口来实现主从功能。边界时钟能降低非对称性的影响。但边界时钟是通过逐级同步实现不同端口的主从时钟同步的,如果在第一级产生了同步误差,这种误差将被逐级的往下传,造成误差积聚,同步精度不高,稳定性差。将 边界时钟(BC: Boundary Clock) 替换为 透明时钟(TC: Transparent Clock) 后,如下图:
Linux 网络:PTP 简介_第6张图片
透明时钟(TC: Transparent Clock) 对中间设备驻留时间的校正,克服了 边界时钟(BC: Boundary Clock) 逐级同步造成误差逐渐传递的问题。

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

端对端透明时钟(E2ETC: End to End Transparent Clock) 的 时钟模型如下图所示:
Linux 网络:PTP 简介_第7张图片
端对端透明时钟(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 芯片处理,这些报文通常不会再往上传递到内核网络协议栈,其报文格式是如下:
在这里插入图片描述
Linux 网络:PTP 简介_第8张图片
PTP 报文 也可能是封装 L4 层 的 TCP/UDP 报文,其格式如下:
Linux 网络:PTP 简介_第9张图片
Linux 网络:PTP 简介_第10张图片

2.3.1.1 IEEE 1588 v1 报文格式

(待续,暂未比较完整的相关信息,先放一个 Wireshark 抓包)
IEEE 1588 v1 报文 Sync 抓包:
Linux 网络:PTP 简介_第11张图片
IEEE 1588 v1 报文 Follow_Up 抓包:
Linux 网络:PTP 简介_第12张图片

2.3.1.2 IEEE 1588 v2 报文格式

IEEE 1588 v2 报文 必须包含消息头消息体消息扩展字节扩展字节长度可能为 0。看一下 IEEE 1588 v2 报文消息头 的格式:
Linux 网络:PTP 简介_第13张图片
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 报文的格式加以说明。
Linux 网络:PTP 简介_第14张图片
Linux 网络:PTP 简介_第15张图片
Linux 网络:PTP 简介_第16张图片
Linux 网络:PTP 简介_第17张图片
Linux 网络:PTP 简介_第18张图片
Linux 网络:PTP 简介_第19张图片
Linux 网络:PTP 简介_第20张图片

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

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

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

Linux 网络:PTP 简介_第21张图片

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

Linux 网络:PTP 简介_第22张图片

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 简介_第23张图片
在上图中,将 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 工具

Linux 网络:PTP 简介_第24张图片

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

你可能感兴趣的:(#,网络,linux,PTP,IEEE,1588)