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 = (T4 - T1) - (T3 - T2) $$

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

$$ T4 = T3 - offset + \frac{delay}{2}$$

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

$$ offset = \frac{(T2 - T1) + (T4 - T3)}{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节点的发送链路延迟是$t_{ms}$,Slave节点到Master节点的发送链路延迟是$t_{sm}$,Slave节点和Master之间的时间偏差为offset,于是可以得到:

$$ t2 - t1 = t_{ms} + offset $$

$$ t4 - t3 = t_{sm} - offset $$

$$ (t2 - t1) - (t4 - t3) = (t_{ms} + offset) - (t_{sm} - offset) $$

$$ offset = [(t2 - t1) - (t4 - t3) - (t_{ms} - t_{sm})] / 2 $$

如果$t_{ms} = t_{sm}$,即Master节点和Slave节点之间的收发链路延迟对称,那么:

$$offset = [(t2 - t1) - (t4 - t3)] / 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的发送链路延迟是$t_{reqresp}$,节点1到节点2的发送链路延迟是$t_{respreq}$,可以得到节点2到节点1的总链路往返延迟为:

$$ (t_{reqresp} + t_{respreq}= (t4 - t1) - (t3 - t2) $$

如果$t_{reqresp} = t_{respreq}$,即节点2到节点1之间的收发链路延迟对称,那么节点2和节点1之间的链路平均延迟为:

$$ MeanPathDelay = \frac{[(t4 - t1) - (t3 - t2)]}{2} $$

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

$$ offset = t6 - t5 - MeanPathDelay $$

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时间同步原理
    1. 2.1. E2E同步
    2. 2.2. P2P同步
  3. 3. PTP协议在Linux中是如何实现的
    1. 3.1. ptp4l的实现
    2. 3.2. PTP物理时钟驱动框架
  4. 4. 总结
  5. 5. 参考资料