Linux 网络:PTP 简介
Linux, PTP, IEEE 1588
文章目录
- 1. 前言
- 2. PTP(Precision Time Protocol) IEEE 1588 协议简介
- 3. Linux PTP 协议栈
- 4. Linux PTP 相关工具
- 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
时间同步的实现包括以下步骤:
1. 选举最优时钟
通过 Announce 报文,选举最优时钟(主时钟),确定时钟主从关系。
2. 频率同步
通过 Sync, Follow_Up 报文,实现从时钟频率与主时钟同步。频率同步是相位同步的前提和基础。
3. 相位同步
通过 Sync, Follow_Up, Delay_Req, Delay_Resp 报文,实现从时钟相位与主时钟同步,使得从
时钟的时间(从节点的本地时间)和主时钟的时间保持一致。
2.1.1 选举最优时钟
最优时钟
可以通过手工指定,也可以通过 BMC
算法动态选举。BMC
是 IEEE 1588 v2
协议规定的一种确定网络中各时钟主从层级的算法。通过这种算法,将网络中的时钟划分为主、从时钟
,从时钟跟踪主时钟的频率和时间
,在网络变化
,或网络中某个时钟源的属性发生改变
时,BMC
算法能重新选择最佳主时钟
,使全网的频率和相位达到同步。选举的具体过程如下:
1. 各时钟节点之间通过交互 Announce 报文,会依据 Announce 报文中所携带的时钟的
第一优先级、时间等级、时间精度和第二优先级的次序依次进行比较,获胜者将成为
最优时钟,比较规则如下图所示。
2. 主节点定期发送Announce报文给从节点,如果在一段时间内,从节点没有收到主节点
发来的 Announce 报文,便认为该主节点失效,于是重新选举最优时钟。
2.1.2 时钟 频率、相位 同步
在确定主时钟
后,从时钟
需要将自身的时钟频率、相位
同步到主时钟
。下图展示了 频率同步(Frequency synchronization)
和 相位同步(Phase synchronization)
:
时钟频率同步(Frequency synchronization)
实现机制如下图所示:
假设 时钟 A
要同步到 时钟 B
,不考虑路径延时和驻留时间的变化,如果 A
和 B
的时钟频率相等,则在相同的时间间隔内,A
和 B
的时间累积的偏差应该是一样的,也就是说。如果
t
2
N
{t}_{2N}
t2N -
t
20
{t}_{20}
t20 大于
t
1
N
{t}_{1N}
t1N -
t
10
{t}_{10}
t10,说明 A
的时钟频率比 B
快,要调慢 A
的时钟频率;反之,则说明 A
的时钟频率比 B
慢,则要调快 A
的时钟频率。其中,
t
1
N
{t}_{1N}
t1N 为 B
的 第 N
个 Sync
报文的发送时间,
t
2
N
{t}_{2N}
t2N 为 A
接收的第 N
个 Sync
报文的时间点。
时钟相位同步(Phase synchronization)
实现机制如下图所示:
上图中:
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)。
最后:
. 对于 Sync 和 Delay_Req 报文,不管是 发送端 还是 接收端,在发送、接收数据时,
都会给报文打上时间戳:发送端打上发送时间的时间戳,接收端打上接收时间的时间戳。
这样,经过图中 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 -> slave
和 slave -> master
的发送延时 是相同的。如果 master -> slave
和 slave -> master
的发送延时不对称,则上述计算公式就会由偏差,针对这种问题,IEEE 1588
通过在 PTP 通信报文中嵌入时间校正域(Correction Field)
来解决。
在音频应用场景下,相位同步(Phase synchronization)
可避免时钟抖动
引起的音频信号失真,如下图:
这种失真也被称为数字信号的相位噪声
,它在高频信号上更加明显。抖动引起的失真会导致多声道的空间感丢失。
而 频率同步(Frequency synchronization)
可避免 时钟漂移
,如下图:
这里的时钟漂移是时钟一个快一个慢,随着时间的推移,差异会越来越大,最终导致同步失败。同时,播放与采集设备间的时钟漂移也会导致 AEC(Acoustic Echo Cancellation,回声消除)
算法无法收敛。
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)
从网络 接收
基准时钟
。下面图中标记为 master
和 slave
的,全都是 普通时钟(OC: Ordinary Clock)
:
可以看到,普通时钟(OC: Ordinary Clock)
可以在拓扑中任何位置。其中,在交换机 Switch
上,进口网口的时钟
作为 Grandmaster
的 slave
,出口网卡的时钟
作为 末端设备
的 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 报文,在转发 Sync、Delay_Req 报文时,通过一个
驻留时间桥计算 Sync、Delay_Req 报文在本节点驻留的时间(报文穿过本节点所花的时间),驻留
时间将累加到报文的校正域(Correction Field)字段中。适用于时钟节点数量较少的 PTP 网络。
由以上分析可以得出,要实现支持 透明时钟
的交换机
、路由器
需要包含以下 3
个主要功能:
1. 普通 交换机、路由器 的功能;
2. 能识别 PTP 事件报文 并 标记报文 的 收发时间戳 的功能;
3. 完成 驻留时间的计算 及 修改报文 的 Correction Field 字段。
2.2.3.2 点对点透明时钟(P2PTC: Peer to Peer Transparent Clock)
直接转发 PTP 协议报文中的 Sync、Follow_Up、Announce 报文,并在转发 Sync 报文时,携带
Sync 报文在本设备内的驻留时间以及本设备和上游时钟节点之间的链路传输延时(驻留时间用于
对时间偏差进行校正,链路传输延时用于计算时间偏差),终结其他 PTP 协议报文。相对 E2ETC,
P2PTC 转发的 PTP 报文类型减少了,更适用于时钟节点数量较多的 PT P网络。但 P2PT C需要计
算链路传输延时,实现较 E2ETC 复杂。
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
和端口号
分配给 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
对 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
网卡 和 stmicro
的 MAC 驱动
为例,来说明上述两种情形下的 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 硬件时钟的工作,就是用 MAC
或 PHY
自带的硬件计数器的计数值,给收发的 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;
}
/*
* DP83640 PHY 芯片 对 发送包 打时间戳的流程,需要阅读芯片手册才能够理解:
* The DP83640 implements a packet-based status mechanism that allows the PHY to queue up events and
* pass them to the microcontroller through the receive data interface. The packet, called a PHY Status
* Frame, may be used to provide IEEE 1588 status for transmit packet timestamps, receive packet
* timestamps, event timestamps, and trigger conditions. In addition the device can generate status
* messages indicating packet buffering errors and to return data read using the PHY Control Frame register
* access mechanism.
* 从这里我们要了解的核心内容,先将 发送包 的 时间戳数据 数据帧 传给 PHY 芯片
* 的微控制器,然后 PHY 芯片的微控制器再将数据传递给 MAC 的 RX, 从而生成一个
* MAC RX 中断:
*
* PHY Status Frame(带有传出包的时间戳) -> PHY microcontroller -> MAC RX 接口
*
* 接下来的处理流程就和 MAC 接收普通数据帧一样,只不过因为这个数据帧的特殊性
* (带有发送包的时间戳数据),要做特殊处理: MAC 驱动接收数据时, 如果发现是 PTP
* 协议帧(PHY Status Frame 封装为 PTP 协议帧格式),则将数据传递给 PHY 驱动的
* .rxtstamp 接口处理,在这里也就是传递给 dp83640_rxtstamp() 处理,这也就是下
* 面的分析的流程。这里的特殊帧在 DP86340 里面称为 PHY Status Frames 。更多的
* 细节,请参考 DP83640 芯片手册章节 5.4.7 PHY Status Frames
*
* 发生 MAC RX 中断时,接收处理 带有 传出包时间戳数据 的 PHY Status Frames 流程:
*/
netif_receive_skb_internal()
...
skb_defer_rx_timestamp()
...
// 判定为 PTP 协议帧,则调用 .rxtstamp = dp83640_rxtstamp() 处理
dp83640_rxtstamp()
// decode_txts() 处理 dp83640_txtstamp() 放入到 @dp83640->tx_queue
// 队列的 PTP 协议传出帧, 对它们打上时间戳。
dp83640_rxtstamp()
if (is_status_frame(skb, type)) { // 带有传出包的时间戳 PHY Status Frame
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.2.2.3 PHY 层对传入、传出帧打时间戳过程小结
前两小节的分析中,没有仔细交待时间戳来源于哪里。对于 DP83640
而言,时间戳数据来自 DP83640
自动生成的 PHY Status Frame
,然后 DP83640
的 microcontroller 将它们封装成 PTP 协议帧格式
并传递给 MAC
的 RX
;PHY Status Frame
的到来导致 MAC 发生 RX 中断
,然后 MAC
处理 RX 中断
,收取这些 PHY Status Frame
时,发现这些帧的格式 为 PTP 协议帧
,于是又将它们传递给 PHY 驱动的 .rxtstamp
接口处理,绕了一大圈,有点低效。简单的看下流程:
netif_receive_skb_internal()
...
skb_defer_rx_timestamp()
...
// 仅处理 传入 PTP 协议帧
phydev->drv->rxtstamp(phydev, skb, type) = dp83640_rxtstamp(phydev, skb, type)
if (is_status_frame(skb, type)) { // 处理 PHY Status Frame
decode_status_frame(dp83640, skb);
kfree_skb(skb);
return true; // 帧已处理,不必再麻烦 MAC 驱动了
}
这个过程中,涉及到两个帧:第一个帧
是网络传入的 PTP 协议帧
;第二个帧
是 PHY 自动生成的
、为 第一个帧传入的 PTP 协议帧提供时间戳
的、封装成 PTP 协议帧格式
的 PHY Status Frame
。这两个帧的到来在时间上没有一定先后关系:第一个帧来源于网络,第二个帧来源于 PHY 芯片自身生成的数据。为什么不像 MAC
层的时间戳那样,用直接读取寄存器的方式,而要设计成这样?一方面可能因为一个时间戳通常有 64-bit
(毕竟是高精度的嘛),对于 PHY
来讲,要读 4 个 16-bit
的寄存器,对于 MDIO 这样的低速总线来讲,还是有点慢(还是因为高精度嘛,必须得读得快),而对于 MAC
,读时间戳数据通常是访问 IO 映射内存,这速度比起 MDIO 的 PHY 寄存器访问,要快上很多;另一方面,可能因为可能因为前面提到的两个帧是异步生成的,可能这种方式比起通过 MDIO 访问 PHY 寄存器,要快。读 PHY 时间戳寄存器
和 生成额外一个带时间戳的 PHY Status Frame
,这两种方式,孰优孰劣,要从实际测试结果来看。另外,非常值得注意的,生成额外一个带时间戳的 PHY Status Frame
这种方式,有可能使得某些 PTP 协议帧无法打上时间戳,因为 带时间戳的 PHY Status Frame
的生成的时间,必须贴近 PTP 协议帧
接收的时间才有意义。如果一个 PTP 协议帧收到超过一定时间,仍然没有合适的 带时间戳的 PHY Status Frame
,PHY 驱动可能不会为其打上时间戳(至少 DP83640 的驱动是这样实现的)。
笔者见过的通过 PHY 层提供 PTP 支持的芯片较少,而 MAC 层打时间戳的芯片居多,可能就是因为上面说到的这些原因吧。
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 总线读取寄存器
获取收发时间戳、 或 通过一个异步的 PHY Status Frame 提供时间戳
的方式,显然 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 硬件时钟(PTP 硬件时钟存在的情形)
或 系统时钟 CLOKC_REALTIME(PTP 硬件时钟不存在的情形)
,以达到与主时钟(master)
同步的目的。
linuxptp
是一个工具集,其中最核心的工具是 ptp4l
,它完成了 Linux PTP
协议栈用户空间的工作。接下来以 ptp4l
的代码为例,来分析 Linux PTP
协议栈用户空间的工作细节。ptp4l
实现了 普通时钟(OC: Ordinary Clock)
、透明时钟(TC: Transparent Clock)
、边界时钟(BC: Boundary Clock)
,本文只关注 普通时钟(OC: Ordinary Clock)
部分。
3.3.1 linuxptp 的配置
在开始后续的讨论之前,先来看一看 ptp4l
的配置。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,
×tamping, 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;
上面的代码核心可以总结为:创建处理 GENERAL,EVENT 类型 PTP 协议包的两个套接字,配置、启用 网卡设备(MAC 或 PHY) 的硬件时间戳功能
。通过如下代码片段,用户空间可以请求内核在上述进、出时机,对 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 硬件时钟
有两种代码路径触发时钟的同步:一个是处理 Sync 包,另一个是处理 Follow_Up 包:
bc_event() / e2e_event() / p2p_event()
process_sync() / process_follow_up()
port_syfufsm()
port_synchronize()
两种路径最终都会进入函数 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 使用范例
在 master
和 slave
主机上都用如下命令启动 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] https://support.huawei.com/enterprise/zh/doc/EDOC1100174722/d7011e99
[3] PTP技术白皮书
[4] White Paper Precision Clock Synchronization The Standard IEEE 1588
[5] IEEE1588v2 透明时钟研究与实现
[6] 比NTP还牛逼的时间同步协议:1588v2,亚微秒级!
[7] IEEE-1588 Standard for a Precision Clock Synchronization Protocol for Networked Measurement and Control Systems
[8] IEEE1588 verision 2 报文介绍
[9] 1588v2(PTP)报文通用格式
[10] IEEE 1588 报文封装
[11] 时钟同步原理
[12] 数字音频传输
Linux 内核 PTP 相关文档
:
[13] 内核文档: timestamping
[14] Precision Time Protocol on Linux ~ Introduction to linuxptp
[15] PTP Clock Manager for Linux
本文涉及的支持 IEEE 1588 的芯片文档
:
[16] Intel Ethernet Controller I350 Datasheet
[17] DP83640 Precision PHYTER™ - IEEE 1588 Precision Time Protocol Transceiver
LinuxPTP 工具相关文档
:
[18] LinuxPTP Project
[19] ptp4l(8): PTP Boundary/Ordinary/Transparent Clock
[20] phc2sys(8): synchronize two or more clocks
[21] 用ptp4l和phc2sys实现系统时钟同步
[22] Synchronizing Time with Linux PTP
[23] 更精准的时延:使用软件时间戳和硬件时间戳
[24] 网络时钟同步IEEE 1588/802.1AS
[25] 如何在 Linux 使用 PTP 进行时间同步
[26] Linux PTP 高精度时间同步实践
[27] linux ptp /ptp4l PTP 时钟如何同步配置
[28] 第 20 章 使用 ptp4l 配置 PTP
[29] Linuxptp使用总结
[30] [补充:以 ptp4l、E2E 为例的 Linuxptp 代码分析
[31] 剖析Linuxptp中ptp4l实现–OC
[32] IEEE 1588标准文档与linuxptp代码的映射
[33] 以 ptp4l、E2E 为例的 Linuxptp 代码分析
更多推荐
所有评论(0)