JasonWang's Blog

时间同步协议PTP那些事

字数统计: 6k阅读时长: 25 min
2025/08/16

现代人的生活已经离不开时间了,无论是出门上班,还是外出旅行,都需要准确的知道我们所处位置的时间。日常生活中,往往分钟、秒级的时间精确度就够用了,但在工程技术中,比如飞机巡航、机器控制、网络管理都需要更高精度的时间测量。我们需要准确的知道两个事件之间发生的时间。精确的测量时间是一件非常复杂的技术活儿。在世界各地,要在不同网络与设备之间同步时间是一件非常具有挑战的事情。首先,需要解决的问题是如何精确测量时间,其次是将时间准确的同步到其他系统或者设备。第一个问题可以通过原子钟(atomic clocks)来解决,比如标准时间的采用的​​铯原子钟​​误差可以达到1亿年1秒;卫星导航系统如GPS,北斗都会搭载一个原子钟用于高精度的导航,因此GPS信号也可以作为一个时钟源用于授时;第二个时间同步一般通过标准的协议来实现,本文重点介绍使用较为普遍的一种同步协议PTP(Precise Time Protocol)

时间同步协议如NTP(Network Time Protocol)或者SNTP(Simple Network Time Protocol)本质上是一种基于UDP协议(协议端口号123)的同步协议,用于同步世界时钟(UTC)与主机的时间。NTP最早在1981年提出,经过多个版本的迭代优化,最新NTPv4版本同时支持IPV4/IPV6,并提供加密认证的流程,在安全性上有比较大的提升。

NTP的版本历史

NTP使用分布式、分层的时间同步架构,每层的时间同步被称为stratum(层),比如最高层stratum0一般是原子钟或者GPS时钟,作为整个系统的时钟源。具体来说,时间同步协议采用客户端-服务端架构,客户端通过向NTP服务器发送时间同步请求(NTP服务器则通过原子钟或者GPS进行授时),时间同步的具体步骤简述如下:

  1. 客户端首先向服务端发送一个NTP请求报文,其中包含了该报文离开客户端的时间戳T1
  2. NTP请求报文到达NTP服务器,此时NTP服务器的时刻为T2。当服务端接收到该报文时,NTP服务器处理之后,在T3时刻发出NTP应答报文。该应答报文中携带报文离开NTP客户端时的时间戳T1、到达NTP服务器时的时间戳T2、离开NTP服务器时的时间戳T3
  3. 客户端在接收到响应报文时,记录报文返回的时间戳T4

根据上述4个时间戳,客户端可以计算出NTP报文从客户端到服务端的延迟

delay=(T4T1)(T3T2)

假定客户端与服务端之间的时间差为offset,可以得到:

T4=T3offset+delay2

因此我们可以计算出时间差offset为:

offset=(T2T1)+(T4T3)2

客户端基于该时间差来调整自己的系统时间,完成最终的时间同步。

PTP协议基本概念

NTP一般用于操作系统的时间同步,如WindowsAndroid等系统都支持NTP时间同步,但是由于NTP是一个应用层的同步协议,因此会受系统调度延迟的影响,而且会因为网络不对称、系统RTC(Real-Time Clock)时钟温漂、老化等因素,导致时间同步的精度下降,一般只能达到ms级别的精度。而在工业自动化如机器人控制,5G通信,高频金融交易以及电力系统中,需要更高精度的时间同步,为此IEEE2002年发布了一个新的时间同步协议1588v1,之后在2008年又发布了第二个版本1588v22019发布了一个改进版本1588v2.1,增强了安全性与兼容性。目前常用的时间同步协议PTP都是基于1588v2版本实现,如gPTP(Generalized PTP)协议就是基于1588v2扩展而来。

PTP协议主要有如下几个核心的概念:

  • PTP域: 应用了PTP协议的网络称为一个PTP域;PTP域内有且只有一个同步时钟,域内的其他设备需要与该时钟保持同步;域内负责同步时间的节点称为master,而接收时间同步的设备节点称为slave
  • PTP域中有几种不同类型的时钟:
    • OC(Ordinary Clock)普通时钟,只有一个物理端口用于时间同步,可以作为首节点(Grandmaster Clock)向下游节点发布时间,也可以作为末节点(slave clock)从上游节点同步时间
    • BC(Boundary Clock)边界时钟:该时钟节点有多个物理端口可以用于网络通讯,其中一个端口用于从上游设备同步时间,其余端口向下游设备发布时间
    • TC(Transparent Clock)透明时钟,节点有多个物理端口可以进行网络通讯,不过不用于同步时间,只负责处理与转发PTP协议报文,透明时钟节点有两种类型,一种是E2E(End-to-End),一种是P2P(Peer-to-Peer),区别在于E2E TC转发报文时,会测量报文经过时的转发延迟,并修正到PTP报文中;P2P TC不仅修正转发延迟,还会测量并修正该节点每个端口相连链路的时延(链路传递的延迟)。

PTP clock types

PTP时间同步原理

PTP协议在时间同步之前,一般需要从同步域中选择一个最优时钟Grandmaster Clock, GM),即整个PTP同步域中的时间源。最优时钟可以通过静态配置制定,也可以通过BMC(Best Master Clock)算法动态选举得到:

  1. 各个时钟节点通过Announce包围报告端口上的时钟源信息(最终时钟优先级、时间等级、时间精度、本地晶振的稳定性等),维护本地获得的时钟数据组,按照严格的时钟等级选择最佳时间源,并确定端口状态。通过时钟选举过程,整个PTP域内构建出一颗无环、全连通,以GM为根的生成树。
  2. 此后,master节点会定时发送Announce报文给其他节点,如果网络发生变化,或者从节点没有收到来自主节点的Announce报文,需重新进行最优时钟的选择

在上文中提到,PTP时间同步协议中,有两种不同的同步机制,一种是端到端E2E(End-to-End),另一种是点对点P2P(Peer-to-Peer),两者的差异在于主时钟节点(master)与从时钟节点(slave)的链路延迟测量机制不同:

  • E2E会直接测量两个OC或者BC之间的总链路延迟,包括其间的所有中间TC节点。
  • P2P仅限于测量两个直连相连的OCBC或者TC节点之间的逐点链路延迟

E2E同步

E2E时间同步基于主从节点(master-slave)的方式,通过SyncDelay_ReqDelay_Resp报文交互,从节点计算出与主节点的时间差,从而完成系统时间的同步。具体的流程如下:

  1. Master节点在t1时刻发送Sync报文(如果配置为双步模式(two-step),需要发送Follow_Up报文;单步模式下(one-step),无需发送Follow_Up报文),并将t1时间戳携带在Sync报文(或Follow_Up报文)中
  2. Slave节点在t2时刻接收到Sync报文,在本地产生t2时间戳,并从报文中提取t1时间戳
  3. Slave节点在t3时刻发送Delay_Req报文,并在本地产生t3时间戳
  4. Master节点在t4时刻接收到Delay_Req报文,并在本地产生t4时间戳,然后将t4时间戳携带在Delay_Resp报文中,回传给Slave
  5. Slave节点接收到Delay_Resp报文,从报文中提取t4时间戳。最后Slave节点得到了一组时间戳(t1,t2,t3,t4)

PTP E2E

假设Master节点到Slave节点的发送链路延迟是tmsSlave节点到Master节点的发送链路延迟是tsmSlave节点和Master之间的时间偏差为offset,于是可以得到:

t2t1=tms+offset

t4t3=tsmoffset

(t2t1)(t4t3)=(tms+offset)(tsmoffset)

offset=[(t2t1)(t4t3)(tmstsm)]/2

如果tmstsm,即Master节点和Slave节点之间的收发链路延迟对称,那么:

offset=[(t2t1)(t4t3)]/2

这样Slave节点就可以根据t1,t2,t3,t4四个时间戳计算出自己和Master节点之间的时间偏差offset,完成了Slave节点与Master节点的时间同步。但如果Master节点和Slave节点之间的收发链路延迟存在不对称,会存在同步误差,误差的大小为两个方向链路延迟差值的二分之一。因此,对于一些高精度同步场景,需要对MasterSlave之间的收发链路延迟不对称进行补偿。

P2P同步

P2P时间同步模式下,所有节点都会与相连节点进行报文交互,这样每个节点都可以计算出与其他连接节点的链路延迟;但真正的时间同步,依然只存在于Master节点与Slave节点之间。类似于E2E的模式,每个节点发送报文也分为单步与双步两种方式,对于单步方式,Pdelay_Resp报文带有本报文发送时刻的时间戳;而双步方式,Pdelay_Resp报文并不带有本报文发送时刻的时间戳,只是记录本报文发送时的时间,本报文发送时刻的时间戳由后续报文Pdelay_Resp_Follow_Up携带。各个节点的链路延迟测量步骤如下:

PTP P2P

  1. 节点2在t1时刻发送Pdelay_Req报文
  2. 节点1在t2时刻接收到Pdelay_Req报文,生成该报文的接收时间戳t2
  3. 节点1在t3时刻发送Pdelay_Resp报文,生成该报文的发送时间戳t3
  • 对于单步方式,把t3 – t2携带在Pdelay_Resp报文中
  • 对于双步方式,把t3 – t2携带在Pdelay_Resp_Follow_Up报文中,或者Pdelay_Resp报文携带t2Pdelay_Resp_Follow_Up报文携带t3
  1. 节点2在t4时刻接收到Pdelay_Resp报文,在本地产生t4时间戳;最后节点2得到了一组时间戳(t1,t2,t3,t4)

假设节点2到节点1的发送链路延迟是treqresp,节点1到节点2的发送链路延迟是trespreq,可以得到节点2到节点1的总链路往返延迟为:

treqresp+trespreq=(t4t1)(t3t2)

如果treqresp=trespreq,即节点2到节点1之间的收发链路延迟对称,那么节点2和节点1之间的链路平均延迟为:

MeanPathDelay=[(t4t1)(t3t2)]2

上述过程只是不断地实时计算和更新相连节点之间的链路延迟,并不进行时间同步。时间同步,还需要有Master节点与Slave通过交互Sync/Pdelay_Req/Pdelay_Resp报文计算得到(如下图所示):Master节点向Slave节点周期发送Sync报文(Slave节点得到t5/t6两个时间戳)。最终,Slave节点与Master节点之间的时间偏差为:

offsett6t5MeanPathDelay

PTP time-sync

PTP协议在Linux中是如何实现的

PTP是一个通用的时间同步协议,可用于不同的网络环境,其支持UDP(V4/V6)协议发送同步报文,也可以基于L2(MAC)进行时间的同步。从上面的PTP协议时间同步的机制来看,时间同步的精度主要依赖于如下几个个关键的因素:

  • 报文的时间戳标记的层级,时间戳的位置离硬件层级越近,时间戳的精度越高;如果依赖于软件时间戳,则会受到软件调度与网络协议栈的波动影响
  • 两个同步节点之间的延迟波动,如果节点之间的延迟不对称,则可能影响时间精度;类似地,如果中间节点存在延迟波动,也会影响时间精度
  • 系统时钟与MAC/PHY中的硬件时钟(PHC, PTP Hardware Clock)的精度,容易受温度、电压、环境等因素的影响

Linux中有一个开源的PTP协议栈(简称LinuxPTP),主要包括两个核心的工具,一个是ptp4l,主要用于发送、接收PTP报文,完成时间同步;一个是phc2sys,用于同步系统中不同的时间域的时间,比如PHC时钟与RTC时钟的同步。为了实现纳秒级别时间精度,通常需要使用物理时间戳,也就是以太网网卡MAC/PHY中增加一个TSU(TimeStamping Unit)模块,用于专门解析PTP报文,并将报文的时间戳用PHC物理时钟的时间替代,以尽可能降低系统带来的精度波动(jitter),比如IEEE 802.1AS的时间同步协议gPTP就要求必须支持物理时间戳。

如下图所示,PTP时间同步主要有如下几个核心模块:

  • LinuxPTP协议栈,实现了IEEE1588v2协议,包含ptp4lphc2sys两个核心工具
  • MAC/PHY中的TSU(TimeStamping Unit)模块,提供物理时间戳能力
  • PHC时钟,为TSU模块提供时钟源参考
  • Linux内核驱动,包括PTP时钟驱动,posix时钟驱动,以及UDP协议栈

Linux PTP stack

要查看以太网网卡是否支持物理时间戳,可以使用ethtool命令:ethtool -T eth0,如果结果中显示有hardware-transmit/hardware-receive能力,则表示网卡是支持物理时间戳。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
~$ ethtool -T enp0s31f6
Time stamping parameters for enp0s31f6:
Capabilities:
hardware-transmit
software-transmit
hardware-receive
software-receive
software-system-clock
hardware-raw-clock
PTP Hardware Clock: 0
Hardware Transmit Timestamp Modes:
off
on
Hardware Receive Filter Modes:
none
all
ptpv1-l4-sync
ptpv1-l4-delay-req
ptpv2-l4-sync
ptpv2-l4-delay-req
ptpv2-l2-sync
ptpv2-l2-delay-req
ptpv2-event
ptpv2-sync
ptpv2-delay-req

接下来,我们结合LinuxPTP协议栈与Linux内核的源码看一看PTP协议的实现;主要分为两个部分,一个是ptp4l代码的实现,一个是Linux内核驱动部分包括PTP时钟的实现。

本文使用的LinuxPTP版本为V4.2

ptp4l的实现

ptp4l除了常规的命令行参数之外,还可以通过一个配置文件来设定时间同步的参数;以配置文件为例,常见的参数主要有如下几个

  • logSyncInterval: PTP时间同步间隔,更低的间隔通常能改善本地时间的精度
  • delay_mechanism: 时间同步的方式,有E2E/P2P/Auto三种,默认是E2E
  • network_transport: 网络传输方式,有UDPv4/UDPv6/L2三种,默认是UDPv4
  • twoStepFlag: 是否支持双步同步,默认是开启双步同步
  • masterOnly: 是否只支持master节点,默认是false
  • ptp_dst_mac: PTP报文目的MAC地址,如果选择L2的通讯方式,则需要指定目的MAC地址
  • clock_type: 时钟类型,有OC/BC/P2P_TC/E2E_TC几种,默认是OC
  • BMCA:最佳时钟选择算法,用于配置masterslave节点,如果设定为noop,则会跳过常规的BMCA过程,使用静态的配置

LinuxPTP源码的目录(configs/automotive-master.cfg),已有部分ptp4l的配置文件可以参考,以车载网络中的master节点配置为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

#
# Automotive Profile example configuration for master containing those
# attributes which differ from the defaults. See the file, default.cfg, for
# the complete list of available options.
#
[global]
# Options carried over from gPTP.
gmCapable 1
priority1 248
priority2 248
logSyncInterval -3
syncReceiptTimeout 3
neighborPropDelayThresh 800
min_neighbor_prop_delay -20000000
assume_two_step 1
path_trace_enabled 1
follow_up_info 1
transportSpecific 0x1
ptp_dst_mac 01:80:C2:00:00:0E
network_transport L2
delay_mechanism P2P
#
# Automotive Profile specific options
#
BMCA noop
serverOnly 1
inhibit_announce 1
asCapable true
inhibit_delay_req 1

ptp4l的源代码主要有几个关联的部分:

  • port: 对应于以太网网卡的网口,一个网口可能有好几个状态enum port_state,比如初始化、运行、监听等
  • clock: PTP时钟对象,可能包含了很多的port,也保存了时间同步的一些状态信息
  • PTP时间同步协议消息的封装与发送,系统不同时钟之间的处理

我们找到ptp4l的入口函数是main()函数,可以看到,其核心逻辑主要有如下几个步骤:

  • clock_create: 根据用户指定的参数与配置文件,创建PTP时钟对象,同时会创建时钟对应的port对象clock_add_port
  • clock_poll: 持续监控port状态,根据port的时间类型进行状态转换与处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

int main(int argc, char *argv[])
{
char *config = NULL, *req_phc = NULL, *progname;
enum clock_type type = CLOCK_TYPE_ORDINARY;
int c, err = -1, index, print_level;
struct clock *clock = NULL;
struct option *opts;
struct config *cfg;

if (handle_term_signals())
return -1;

cfg = config_create();
if (!cfg) {
return -1;
}
...

print_set_progname(progname);
print_set_tag(config_get_string(cfg, NULL, "message_tag"));
print_set_verbose(config_get_int(cfg, NULL, "verbose"));
print_set_syslog(config_get_int(cfg, NULL, "use_syslog"));
print_set_level(config_get_int(cfg, NULL, "logging_level"));

assume_two_step = config_get_int(cfg, NULL, "assume_two_step");
sk_check_fupsync = config_get_int(cfg, NULL, "check_fup_sync");
sk_tx_timeout = config_get_int(cfg, NULL, "tx_timestamp_timeout");
sk_hwts_filter_mode = config_get_int(cfg, NULL, "hwts_filter");

ptp_hdr_ver = config_get_int(cfg, NULL, "ptp_minor_version");
ptp_hdr_ver = (ptp_hdr_ver << 4) | PTP_MAJOR_VERSION;

if (config_get_int(cfg, NULL, "clock_servo") == CLOCK_SERVO_NTPSHM) {
config_set_int(cfg, "kernel_leap", 0);
config_set_int(cfg, "sanity_freq_limit", 0);
}

if (STAILQ_EMPTY(&cfg->interfaces)) {
fprintf(stderr, "no interface specified\n");
usage(progname);
goto out;
}

type = config_get_int(cfg, NULL, "clock_type");
switch (type) {
case CLOCK_TYPE_ORDINARY:
if (cfg->n_interfaces > 1) {
type = CLOCK_TYPE_BOUNDARY;
}
break;
case CLOCK_TYPE_BOUNDARY:
if (cfg->n_interfaces < 2) {
fprintf(stderr, "BC needs at least two interfaces\n");
goto out;
}
break;
case CLOCK_TYPE_P2P:
if (cfg->n_interfaces < 2) {
fprintf(stderr, "TC needs at least two interfaces\n");
goto out;
}
if (DM_P2P != config_get_int(cfg, NULL, "delay_mechanism")) {
fprintf(stderr, "P2P_TC needs P2P delay mechanism\n");
goto out;
}
break;
case CLOCK_TYPE_E2E:
if (cfg->n_interfaces < 2) {
fprintf(stderr, "TC needs at least two interfaces\n");
goto out;
}
if (DM_E2E != config_get_int(cfg, NULL, "delay_mechanism")) {
fprintf(stderr, "E2E_TC needs E2E delay mechanism\n");
goto out;
}
break;
case CLOCK_TYPE_MANAGEMENT:
goto out;
}

clock = clock_create(type, cfg, req_phc);
if (!clock) {
fprintf(stderr, "failed to create a clock\n");
goto out;
}

err = 0;

while (is_running()) {
if (clock_poll(clock))
break;
}

...
}


限于篇幅,感兴趣的可以参考源码中的clock.cport.c等文件。接下来,我们简单看看PTP时钟内核的实现框架。

PTP物理时钟驱动框架

PTP物理时钟的驱动框架主要分为两个部分,一个是提供公共接口与驱动框架的,相关的类型定义都放在include/linux/ptp_clock_kernel.h中;PTP物理时钟对应一个结构体struct ptp_clock_info,包含了PTP时钟的配置以及获取、设置时钟参数的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

struct ptp_clock_info {
struct module *owner;
char name[16];
s32 max_adj;
int n_alarm;
int n_ext_ts;
int n_per_out;
int n_pins;
int pps;
struct ptp_pin_desc *pin_config;
int (*adjfine)(struct ptp_clock_info *ptp, long scaled_ppm);
int (*adjfreq)(struct ptp_clock_info *ptp, s32 delta);
int (*adjphase)(struct ptp_clock_info *ptp, s32 phase);
int (*adjtime)(struct ptp_clock_info *ptp, s64 delta);
int (*gettime64)(struct ptp_clock_info *ptp, struct timespec64 *ts);
int (*gettimex64)(struct ptp_clock_info *ptp, struct timespec64 *ts,
struct ptp_system_timestamp *sts);
int (*getcrosststamp)(struct ptp_clock_info *ptp,
struct system_device_crosststamp *cts);
int (*settime64)(struct ptp_clock_info *p, const struct timespec64 *ts);
int (*enable)(struct ptp_clock_info *ptp,
struct ptp_clock_request *request, int on);
int (*verify)(struct ptp_clock_info *ptp, unsigned int pin,
enum ptp_pin_function func, unsigned int chan);
long (*do_aux_work)(struct ptp_clock_info *ptp);
};


对于支持PTP物理时钟的以太网驱动来说,需要在初始化的时候调用ptp_clock_register注册PTP时钟,并在驱动卸载的时候调用ptp_clock_unregister进行反注册。以英特尔的一个千兆以太网驱动ethernet/intel/igb/igb_main.c为例,可以看到网卡驱动在执行初始化igb_probe时,会调用igb_ptp_init注册PTP时钟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

/**
* igb_ptp_init - Initialize PTP functionality
* @adapter: Board private structure
*
* This function is called at device probe to initialize the PTP
* functionality.
*/
void igb_ptp_init(struct igb_adapter *adapter)
{
struct e1000_hw *hw = &adapter->hw;
struct net_device *netdev = adapter->netdev;
int i;

switch (hw->mac.type) {
case e1000_82576:
snprintf(adapter->ptp_caps.name, 16, "%pm", netdev->dev_addr);
adapter->ptp_caps.owner = THIS_MODULE;
adapter->ptp_caps.max_adj = 999999881;
adapter->ptp_caps.n_ext_ts = 0;
adapter->ptp_caps.pps = 0;
adapter->ptp_caps.adjfreq = igb_ptp_adjfreq_82576;
adapter->ptp_caps.adjtime = igb_ptp_adjtime_82576;
adapter->ptp_caps.gettimex64 = igb_ptp_gettimex_82576;
adapter->ptp_caps.settime64 = igb_ptp_settime_82576;
adapter->ptp_caps.enable = igb_ptp_feature_enable;
adapter->cc.read = igb_ptp_read_82576;
adapter->cc.mask = CYCLECOUNTER_MASK(64);
adapter->cc.mult = 1;
adapter->cc.shift = IGB_82576_TSYNC_SHIFT;
adapter->ptp_flags |= IGB_PTP_OVERFLOW_CHECK;
break;
case e1000_82580:
case e1000_i354:
case e1000_i350:
snprintf(adapter->ptp_caps.name, 16, "%pm", netdev->dev_addr);
adapter->ptp_caps.owner = THIS_MODULE;
adapter->ptp_caps.max_adj = 62499999;
adapter->ptp_caps.n_ext_ts = 0;
adapter->ptp_caps.pps = 0;
adapter->ptp_caps.adjfine = igb_ptp_adjfine_82580;
adapter->ptp_caps.adjtime = igb_ptp_adjtime_82576;
adapter->ptp_caps.gettimex64 = igb_ptp_gettimex_82580;
adapter->ptp_caps.settime64 = igb_ptp_settime_82576;
adapter->ptp_caps.enable = igb_ptp_feature_enable;
adapter->cc.read = igb_ptp_read_82580;
adapter->cc.mask = CYCLECOUNTER_MASK(IGB_NBITS_82580);
adapter->cc.mult = 1;
adapter->cc.shift = 0;
adapter->ptp_flags |= IGB_PTP_OVERFLOW_CHECK;
break;
case e1000_i210:
case e1000_i211:
for (i = 0; i < IGB_N_SDP; i++) {
struct ptp_pin_desc *ppd = &adapter->sdp_config[i];

snprintf(ppd->name, sizeof(ppd->name), "SDP%d", i);
ppd->index = i;
ppd->func = PTP_PF_NONE;
}
snprintf(adapter->ptp_caps.name, 16, "%pm", netdev->dev_addr);
adapter->ptp_caps.owner = THIS_MODULE;
adapter->ptp_caps.max_adj = 62499999;
adapter->ptp_caps.n_ext_ts = IGB_N_EXTTS;
adapter->ptp_caps.n_per_out = IGB_N_PEROUT;
adapter->ptp_caps.n_pins = IGB_N_SDP;
adapter->ptp_caps.pps = 1;
adapter->ptp_caps.pin_config = adapter->sdp_config;
adapter->ptp_caps.adjfine = igb_ptp_adjfine_82580;
adapter->ptp_caps.adjtime = igb_ptp_adjtime_i210;
adapter->ptp_caps.gettimex64 = igb_ptp_gettimex_i210;
adapter->ptp_caps.settime64 = igb_ptp_settime_i210;
adapter->ptp_caps.enable = igb_ptp_feature_enable_i210;
adapter->ptp_caps.verify = igb_ptp_verify_pin;
break;
default:
adapter->ptp_clock = NULL;
return;
}

spin_lock_init(&adapter->tmreg_lock);
INIT_WORK(&adapter->ptp_tx_work, igb_ptp_tx_work);

if (adapter->ptp_flags & IGB_PTP_OVERFLOW_CHECK)
INIT_DELAYED_WORK(&adapter->ptp_overflow_work,
igb_ptp_overflow_check);

adapter->tstamp_config.rx_filter = HWTSTAMP_FILTER_NONE;
adapter->tstamp_config.tx_type = HWTSTAMP_TX_OFF;

igb_ptp_reset(adapter);

adapter->ptp_clock = ptp_clock_register(&adapter->ptp_caps,
&adapter->pdev->dev);
if (IS_ERR(adapter->ptp_clock)) {
adapter->ptp_clock = NULL;
dev_err(&adapter->pdev->dev, "ptp_clock_register failed\n");
} else if (adapter->ptp_clock) {
dev_info(&adapter->pdev->dev, "added PHC on %s\n",
adapter->netdev->name);
adapter->ptp_flags |= IGB_PTP_ENABLED;
}
}

函数ptp_clock_register主要完成了如下几个工作:

  1. 创建一个字符设备,设置设备号与名称,注册完成后可以在/dev/ptpx上访问到PTP时钟
  2. 如果时钟本身支持PPS(Pulse-per-Second),那么还需要通过pps_register_source注册PPS
  3. 最后将PTP时钟注册为一个标准的posix时钟,这样可以通过标准的posix接口方式来访问PTP时钟
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

struct ptp_clock *ptp_clock_register(struct ptp_clock_info *info,
struct device *parent)
{
struct ptp_clock *ptp;
int err = 0, index, major = MAJOR(ptp_devt);

if (info->n_alarm > PTP_MAX_ALARMS)
return ERR_PTR(-EINVAL);

/* Initialize a clock structure. */
err = -ENOMEM;
ptp = kzalloc(sizeof(struct ptp_clock), GFP_KERNEL);
if (ptp == NULL)
goto no_memory;

index = ida_simple_get(&ptp_clocks_map, 0, MINORMASK + 1, GFP_KERNEL);
if (index < 0) {
err = index;
goto no_slot;
}

ptp->clock.ops = ptp_clock_ops;
ptp->info = info;
ptp->devid = MKDEV(major, index);
ptp->index = index;
spin_lock_init(&ptp->tsevq.lock);
mutex_init(&ptp->tsevq_mux);
mutex_init(&ptp->pincfg_mux);
init_waitqueue_head(&ptp->tsev_wq);

if (ptp->info->do_aux_work) {
kthread_init_delayed_work(&ptp->aux_work, ptp_aux_kworker);
ptp->kworker = kthread_create_worker(0, "ptp%d", ptp->index);
if (IS_ERR(ptp->kworker)) {
err = PTR_ERR(ptp->kworker);
pr_err("failed to create ptp aux_worker %d\n", err);
goto kworker_err;
}
}

err = ptp_populate_pin_groups(ptp);
if (err)
goto no_pin_groups;

/* Register a new PPS source. */
if (info->pps) {
struct pps_source_info pps;
memset(&pps, 0, sizeof(pps));
snprintf(pps.name, PPS_MAX_NAME_LEN, "ptp%d", index);
pps.mode = PTP_PPS_MODE;
pps.owner = info->owner;
ptp->pps_source = pps_register_source(&pps, PTP_PPS_DEFAULTS);
if (IS_ERR(ptp->pps_source)) {
err = PTR_ERR(ptp->pps_source);
pr_err("failed to register pps source\n");
goto no_pps;
}
}

/* Initialize a new device of our class in our clock structure. */
device_initialize(&ptp->dev);
ptp->dev.devt = ptp->devid;
ptp->dev.class = ptp_class;
ptp->dev.parent = parent;
ptp->dev.groups = ptp->pin_attr_groups;
ptp->dev.release = ptp_clock_release;
dev_set_drvdata(&ptp->dev, ptp);
dev_set_name(&ptp->dev, "ptp%d", ptp->index);

/* Create a posix clock and link it to the device. */
err = posix_clock_register(&ptp->clock, &ptp->dev);
if (err) {
pr_err("failed to create posix clock\n");
goto no_clock;
}

return ptp;
...
}
EXPORT_SYMBOL(ptp_clock_register);


总结

高精度的时间同步实现起来是一个比较复杂的事情,从最初的NTP到如今的PTP,时间同步的精度已经可以达到微妙级别,但随着实时音视频、自动驾驶等场景对时间同步精度的要求日益提高,基于PTP协议衍生出了新的时间同步方式,比较常见的有:

参考资料

原文作者:Jason Wang

更新日期:2025-08-19, 13:56:17

版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. PTP协议基本概念
  2. 2. PTP时间同步原理
  3. 3. PTP协议在Linux中是如何实现的
  4. 4. 总结
  5. 5. 参考资料