限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
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)
的概念,后面会进行描述。
PTP IEEE 1588 采用系主从层次式结构
来同步时钟,实现机制如下图所示:
上图中:
T1: 主时钟(master) 发送 【同步报文 Sync】 的时间
T2: 从时钟(slave) 收到 【同步报文 Sync】 的时间
T3: 从时钟(slave) 发送 【延时请求报文 Delay_Req】 的时间
T4: 主时钟(master) 收到 【延时请求应答报文 Delay_Resp】 的时间
另外:
. 主时钟(master) 向 从时钟(slave) 发送 Follow_Up 报文:
Follow_Up 报文 携带 主时钟(master) 发送同步报文 Sync 的时间,传递给 从时钟(slave)。
Follow_Up 报文仅在 Two-Step 模式下使用,而在 One-Step 模式下,Sync 报文自带了时间 T1,
不再需要 Follow_Up 报文。
. 主时钟(master) 记录 收到 从时钟(slave) 发送的 Delay_Req 报文时间 T4,然后通过
Delay_Resp 报文发送给 从时钟(slave)。
这样,经过图中 4 次报文交互,在 从时钟(slave)
一侧,记录了所有的 T1, T2, T3, T4
,通过这 4 个时间,就可以计算出 主从时钟 传输延时
T d e l a y {T}_{delay} Tdelay :
以及 主从时钟之间 的 时间偏差
T o f f s e t {T}_{offset} Toffset :
注意,上面两个公式都假定 master -> slave
和 slave -> master
的发送延时 是相同的。如果 master -> slave
和 slave -> master
的发送延时不对称,则上述计算公式就会由偏差,针对这种问题,IEEE 1588
通过在 PTP 通信报文中嵌入时间校正域(Correction Field)
来解决。
在上一小节 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)
这几个概念。
普通时钟(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
。
边界时钟(BC: Boundary Clock)
有 2个
或 2个以上
端口:一个作 slave
,用于跟上级 master 同步
;一个做 master
,用于给下级slave 提供 基准时钟
。如 2.2.1
小节图中的 Switch
,它就是一个 边界时钟(BC: Boundary 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)
逐级同步造成误差逐渐传递的问题。
端对端透明时钟(E2ETC: End to End Transparent Clock)
的 时钟模型如下图所示:
端对端透明时钟(E2ETC: End to End Transparent Clock)
对 交换机 和 路由器 提出了要求:
转发所有的 非 PTP 报文 和 PTP报文,但对于 PTP 事件报文,每个端口通过事件端口能识别该报文并产生相应的时间戳。
然后该报文通过一个驻留时间桥计算该报文在本点驻留的时间(报文穿过本点所花的时间),驻留时间将累加到报文的校正域
(Correction Field)字段中。
由以上分析可以得出,要实现支持 透明时钟 的 交换机 和 路由器 需要包含以下3个主要功能:
1. 普通 交换机、路由器 的功能;
2. 能识别 PTP 事件报文 并 标记报文 的 收发时间戳 的功能;
3. 完成 驻留时间的计算 及 修改报文 的 Correction Field 字段。
(待续)
PTP 报文
可能是封装的位于 L2 层
的以太网帧,通常经由 以太网 PHY 芯片处理,这些报文通常不会再往上传递到内核网络协议栈,其报文格式是如下:
PTP 报文
也可能是封装 L4 层 的 TCP/UDP 报文,其格式如下:
(待续,暂未比较完整的相关信息,先放一个 Wireshark 抓包)
IEEE 1588 v1
报文 Sync
抓包:
IEEE 1588 v1
报文 Follow_Up
抓包:
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 报文的格式加以说明。
IANA 组织将有些 IP 和 端口号分配给 PTP IEEE 1588 协议使用。
224.0.0.107 | PTP-pdelay | [NIST: IEEE Std 1588][Kang_Lee] | 2007-02-02
对 PTP IEEE 1588 协议的介绍,本文就进行到这里。本文剩余篇幅都是对 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
项目代码为例来进行分析。
PTP 数据报时间戳 可能来源于 (1) 网络设备自带的硬件时钟
和 (2) 系统时钟 CLOCK_REALTIME
。
PTP 硬件时钟,可以实现在 MAC
层,也可以实现 PHY
层,两种方式选其中之一即可。Linux 内核提供 ptp_clock_register()
接口注册 PTP 时钟。PTP 硬件时钟 的作用,从 3.1
小节中的框图可知,提供 /dev/ptpX
设备节点,供用户空间读取时间、调整时间用。下面来看 MAC
层 和 PHY
层的 PTP 时钟的注册过程。
MAC 层注册 PTP 时钟的时机可能是:
. 网卡驱动加载时。如后面例子中的 igb_probe() 。
. 启动网卡设备时。如后面例子中 stmmac_open() 。
下面分别以 intel igb
网卡 和 stmicro
的 MAC 驱动
为例,来说明上述两种情形下的 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);
...
/*
* 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);
以 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);
不管是处于 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;
PTP 硬件时钟的工作,就是用 MAC
或 PHY
自带的硬件计数器的计数值,给收发的 PTP 协议数据报
盖上时间戳。下面分别对实现在 MAC 层
和 PHY 层
的 PTP 硬件时钟,从 收(RX)、发(TX)
两个方向给 PTP 协议数据报
打时间戳 的过程,一一加以说明。
本小节以前文提到的 intel igb
MAC 驱动注册的 PTP 时钟为例,对 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);
...
...
}
...
网卡向外发送数据帧时,支持 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);
本小节以前文提到的 以太网 PHY 芯片 dp83640
驱动注册的 PTP 时钟为例,对 网络包 收(RX)
、发(TX)
打时间戳 的 过程加以说明。
在收到网络数据包时,进入函数 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;
}
}
...
/* 从 网卡驱动的 发送接口 开始 */
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;
}
MAC 层 和 PHY 层 的 时间戳,由于都是在收发时由硬件提供,所以都能够准确的反映收发包的准确时间,同时都可以通过配置过滤器,为指定类型的传入、传出网络包生成时间戳,这是它们彼此的相同点。
由于 MAC 层获取收发时间戳是内存映射
的寄存器读取收发时间戳,相对于 PHY 层通过 MDIO
总线读取寄存器获取收发时间戳,显然 MAC 层获取收发时间戳的速度
要比 PHY 层更快,这是它们彼此的不同点。
对于 传入、传出 网络包,记录硬件时间戳的位置不同
:
sk_buff 的 skb_hwtstamps(skb)
中;socket 的 错误消息队列
中。/*
* 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
...
/*
* 读取 传出网络包 的 硬件时间戳
*/
// 从前面的分析中了解到,传入网络包的时间戳,记录在 套接字的错误消息队列 中,
// 现在将这个时间戳取出来。
// 这里以 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
...
...
...
/*
* 读取 传入网络包 的 硬件时间戳
*/
// 从前面的分析中了解到,传出网络包的时间戳,记录在 `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))
...
...
如果不支持 PTP 硬件时钟,可以用 系统时钟 CLOCK_REALTIME
对 PTP 报文打时间戳。对于 PTP 硬件时钟对 PTP 报文打时间戳,时间点都是在 PTP 报文进出网络设备的时候;而对于用 系统时钟 CLOCK_REALTIME
对 PTP 报文打时间戳时机,根据用户空间 setsockopt()
调用传递的参数不同,可以有多种时机,本文只讨论以下时机给 PTP 数据报打时间戳情形:
. 对接收的数据包:数据报 正由 网卡驱动 进入 网络协议栈 给 PTP 数据报打时间戳
. 对发送的数据包:数据报 正要 传递给网卡硬件缓冲前 给 PTP 数据报打时间戳
接下来,来分别看看在 接收 和 发送 PTP 数据报时,内核是怎么给它们打上时间戳的。
// (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();
}
还是以 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);
对于 传入、传出 网络包,记录 系统时钟 CLOCK_REALTIME 生成的 软件时间戳 的位置不同
:
CLOCK_REALTIME
生成 的 软件时间戳 记录在 sk_buff
中;CLOCK_REALTIME
生成 的 软件时间戳 记录在 socket 的 错误消息队列
中。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.1.3.3.2 读取 传出网络包 的 硬件时间戳
,系统保持了读取软硬件时间戳接口和方式的一致性。
参看 3.2.1.3.3.3 读取 传入网络包 的 硬件时间戳
,系统保持了读取软硬件时间戳接口和方式的一致性。
PTP 硬件时钟
和 系统时钟 CLOCK_REALTIME
各自提供的 硬件、软件 时间戳,可以使用相同的系统接口进行访问,但很明显 硬件时间戳 具有更高的精度,对系统消耗更小。
对 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)
部分。
在开始后续的讨论之前,先来看一看 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
命令行选项参数指定的配置文件,又会覆盖 默认内置配置 和 命令行参数的 同名配置项。
在所有的主机上,我们假设都以如下命令启动 ptp4l
程序:
ptp4l -i eth0 -H -m # -H 指示 ptp4l 使用 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;
上面的代码核心可以总结为:
通过如下代码片段,用户空间可以请求内核在上述进、出时机,对 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);
...
从 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;
...
}
有几种代码路径触发时钟的同步,最终都会进入函数 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;
...
}
}
在所有主机上,假定都使用如下命令启动 ptp4l
程序:
ptp4l -i eth0 -m -S
ptp4l
使用 系统时钟 CLOCK_REALTIME
时间戳,对比 使用 PTP 硬件时钟的情形,没有太大的差异,只不过时钟由 PTP 硬件时钟 变成了 系统时钟 CLOCK_REALTIME
,在此就不再赘述。
在 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
时钟的同步。
$ 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
可以通过 phc2sys
将 PTP 硬件时钟的时间,同步到系统时钟 CLOCK_REALTIME
,或者反过来也可以。
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