现代人的生活已经离不开时间了,无论是出门上班,还是外出旅行,都需要准确的知道我们所处位置的时间。日常生活中,往往分钟、秒级的时间精确度就够用了,但在工程技术中,比如飞机巡航、机器控制、网络管理都需要更高精度的时间测量。我们需要准确的知道两个事件之间发生的时间。精确的测量时间是一件非常复杂的技术活儿。在世界各地,要在不同网络与设备之间同步时间是一件非常具有挑战的事情。首先,需要解决的问题是如何精确测量时间,其次是将时间准确的同步到其他系统或者设备。第一个问题可以通过原子钟(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使用分布式、分层的时间同步架构,每层的时间同步被称为stratum(层),比如最高层stratum0一般是原子钟或者GPS时钟,作为整个系统的时钟源。具体来说,时间同步协议采用客户端-服务端架构,客户端通过向NTP服务器发送时间同步请求(NTP服务器则通过原子钟或者GPS进行授时),时间同步的具体步骤简述如下:
客户端首先向服务端发送一个NTP请求报文,其中包含了该报文离开客户端的时间戳T1
NTP请求报文到达NTP服务器,此时NTP服务器的时刻为T2。当服务端接收到该报文时,NTP服务器处理之后,在T3时刻发出NTP应答报文。该应答报文中携带报文离开NTP客户端时的时间戳T1、到达NTP服务器时的时间戳T2、离开NTP服务器时的时间戳T3
客户端在接收到响应报文时,记录报文返回的时间戳T4
根据上述4个时间戳,客户端可以计算出NTP报文从客户端到服务端的延迟
假定客户端与服务端之间的时间差为offset,可以得到:
因此我们可以计算出时间差offset为:
客户端基于该时间差来调整自己的系统时间,完成最终的时间同步。
PTP协议基本概念 NTP一般用于操作系统的时间同步,如Windows、Android等系统都支持NTP时间同步,但是由于NTP是一个应用层的同步协议,因此会受系统调度延迟的影响,而且会因为网络不对称、系统RTC(Real-Time Clock)时钟温漂、老化等因素,导致时间同步的精度下降,一般只能达到ms级别的精度。而在工业自动化如机器人控制,5G通信,高频金融交易以及电力系统中,需要更高精度的时间同步,为此IEEE在2002年发布了一个新的时间同步协议1588v1,之后在2008年又发布了第二个版本1588v2;2019发布了一个改进版本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时间同步原理 PTP协议在时间同步之前,一般需要从同步域中选择一个最优时钟Grandmaster Clock, GM),即整个PTP同步域中的时间源。最优时钟可以通过静态配置制定,也可以通过BMC(Best Master Clock)算法动态选举得到:
各个时钟节点通过Announce包围报告端口上的时钟源信息(最终时钟优先级、时间等级、时间精度、本地晶振的稳定性等),维护本地获得的时钟数据组,按照严格的时钟等级选择最佳时间源,并确定端口状态。通过时钟选举过程,整个PTP域内构建出一颗无环、全连通,以GM为根的生成树。
此后,master节点会定时发送Announce报文给其他节点,如果网络发生变化,或者从节点没有收到来自主节点的Announce报文,需重新进行最优时钟的选择
在上文中提到,PTP时间同步协议中,有两种不同的同步机制,一种是端到端E2E(End-to-End),另一种是点对点P2P(Peer-to-Peer),两者的差异在于主时钟节点(master)与从时钟节点(slave)的链路延迟测量机制不同:
E2E会直接测量两个OC或者BC之间的总链路延迟,包括其间的所有中间TC节点。
P2P仅限于测量两个直连相连的OC,BC或者TC节点之间的逐点链路延迟
E2E同步 E2E时间同步基于主从节点(master-slave)的方式,通过Sync,Delay_Req,Delay_Resp报文交互,从节点计算出与主节点的时间差,从而完成系统时间的同步。具体的流程如下:
Master节点在t1时刻发送Sync报文(如果配置为双步模式(two-step),需要发送Follow_Up报文;单步模式下(one-step),无需发送Follow_Up报文),并将t1时间戳携带在Sync报文(或Follow_Up报文)中
Slave节点在t2时刻接收到Sync报文,在本地产生t2时间戳,并从报文中提取t1时间戳
Slave节点在t3时刻发送Delay_Req报文,并在本地产生t3时间戳
Master节点在t4时刻接收到Delay_Req报文,并在本地产生t4时间戳,然后将t4时间戳携带在Delay_Resp报文中,回传给Slave
Slave节点接收到Delay_Resp报文,从报文中提取t4时间戳。最后Slave节点得到了一组时间戳(t1,t2,t3,t4)
假设Master节点到Slave节点的发送链路延迟是,Slave节点到Master节点的发送链路延迟是,Slave节点和Master之间的时间偏差为offset,于是可以得到:
如果,即Master节点和Slave节点之间的收发链路延迟对称,那么:
这样Slave节点就可以根据t1,t2,t3,t4四个时间戳计算出自己和Master节点之间的时间偏差offset,完成了Slave节点与Master节点的时间同步。但如果Master节点和Slave节点之间的收发链路延迟存在不对称,会存在同步误差,误差的大小为两个方向链路延迟差值的二分之一。因此,对于一些高精度同步场景,需要对Master和Slave之间的收发链路延迟不对称进行补偿。
P2P同步 P2P时间同步模式下,所有节点都会与相连节点进行报文交互,这样每个节点都可以计算出与其他连接节点的链路延迟;但真正的时间同步,依然只存在于Master节点与Slave节点之间。类似于E2E的模式,每个节点发送报文也分为单步与双步两种方式,对于单步方式,Pdelay_Resp报文带有本报文发送时刻的时间戳;而双步方式,Pdelay_Resp报文并不带有本报文发送时刻的时间戳,只是记录本报文发送时的时间,本报文发送时刻的时间戳由后续报文Pdelay_Resp_Follow_Up携带。各个节点的链路延迟测量步骤如下:
节点2在t1时刻发送Pdelay_Req报文
节点1在t2时刻接收到Pdelay_Req报文,生成该报文的接收时间戳t2
节点1在t3时刻发送Pdelay_Resp报文,生成该报文的发送时间戳t3
对于单步方式,把t3 – t2携带在Pdelay_Resp报文中
对于双步方式,把t3 – t2携带在Pdelay_Resp_Follow_Up报文中,或者Pdelay_Resp报文携带t2,Pdelay_Resp_Follow_Up报文携带t3
节点2在t4时刻接收到Pdelay_Resp报文,在本地产生t4时间戳;最后节点2得到了一组时间戳(t1,t2,t3,t4)
假设节点2到节点1的发送链路延迟是,节点1到节点2的发送链路延迟是,可以得到节点2到节点1的总链路往返延迟为:
如果,即节点2到节点1之间的收发链路延迟对称,那么节点2和节点1之间的链路平均延迟为:
上述过程只是不断地实时计算和更新相连节点之间的链路延迟,并不进行时间同步。时间同步,还需要有Master节点与Slave通过交互Sync/Pdelay_Req/Pdelay_Resp报文计算得到(如下图所示):Master节点向Slave节点周期发送Sync报文(Slave节点得到t5/t6两个时间戳)。最终,Slave节点与Master节点之间的时间偏差为:
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协议,包含ptp4l和phc2sys两个核心工具
MAC/PHY中的TSU(TimeStamping Unit)模块,提供物理时间戳能力
PHC时钟,为TSU模块提供时钟源参考
Linux内核驱动,包括PTP时钟驱动,posix时钟驱动,以及UDP协议栈
要查看以太网网卡是否支持物理时间戳,可以使用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:最佳时钟选择算法,用于配置master与slave节点,如果设定为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 [global] 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 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.c、port.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 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主要完成了如下几个工作:
创建一个字符设备,设置设备号与名称,注册完成后可以在/dev/ptpx上访问到PTP时钟
如果时钟本身支持PPS(Pulse-per-Second),那么还需要通过pps_register_source注册PPS源
最后将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); 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; 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; } } 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); 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协议衍生出了新的时间同步方式,比较常见的有:
参考资料