1 基本概念
NTP(Network Time Protocol,网络时间协议)是时间同步协议,用来在分布式时间服务器和客户端之间进行时间同步。目前广泛使用的是1992年3月由RFC 1305定义的NTP第3版本,以及最新的在2010年6月由RFC5905定义的NTPv4。
而目前主流的NTP守护程序(ntpd)通常完整支持NTPv4,并且兼容NTPv3。
2 基本原理
NTP基于UDP报文进行传输,使用的UDP端口号为123。传输过程中,需要有一个设备作为时间服务器,其余设备将自己的时间与之同步,单向传输所需时间是1秒。
3 关键流程
假设两个设备A和B,通过NTP进行系统时钟同步,其中设备B作为时间服务器,那么它们的交互过程如下。
1 设备A发送NTP报文给B,报文带有离开A的时间戳10:00:00am,记作T1
2 设备B收到报文,在其中加入此刻的时间戳11:00:01am,记作T2
3 设备B返回报文给A,报文带有离开B的时间戳11:00:02am,记作T3
4 设备A收到响应报文,A此刻的时间戳为10:00:03am,记作T4
那么设备A就可以计算得到以下重要数据:
NTP报文往返时延delay = (T4 - T1) - (T3 - T2) = 2秒
设备A相对设备B的时间差offset = ((T2 - T1) + (T3 - T4)) / 2 = 1小时
于是,设备A可根据上述数据来调整自己的时钟,与设备B同步。
上述计算方法,可参考RFC1305文档定义的以下公式:
4 配置方法
首先需要安装ntp服务,例如yum安装,执行yum install ntp即可。
配置文件是/etc/ntp.conf文件,执行man ntp.conf命令可查看ntpd服务的配置方法,其中常见的配置字段如下。
[ restrict ] 控制相关权限
语法:restrict ip mask x.x.x.x options
标蓝部分是需用户填写的参数,具体含义列举如下表。
参数 |
含义 |
ip |
匹配此规则的主机IP,可以是default,即所有IP。 |
x.x.x.x |
子网掩码,例如255.255.0.0 |
options |
权限配置,有以下选项: ignore :关闭所有的 NTP 联机服务 nomodify:客户端不能更改服务端的时间参数 notrust :不信任未认证的客户端(不适用于NTP 4.2后的版本) noquery :不支持客户端用ntpq,ntpc等命令查询ntp服务器 notrap :不提供trap远端登陆,拒绝提供陷阱服务 nopeer :阻止主机尝试与服务器对等 kod : 访问违规时发送 KoD 包 |
[ server ] 配置上游NTP服务器
作用:设置本地或远程的NTP服务器。与server类似的字段还有pool、peer、broadcast、manycastclient,它们分别应用于ntp的客户端/服务端模式、对等体模式、广播模式和组播模式。这里只列举最常用的server字段所对应的参数。
普通用法:设置远程服务器
server address [ key ] [ burst ] [ iburst ] [ version ] [ prefer ] [ minpoll ] [maxpoll ] [ true ] [ xmtnonce ]
参数 |
含义 |
address |
服务器IP |
key |
取值有key和autokey,各自的含义如下。 key:指定收发包过程中的密钥ID,范围1~65536 autokey:使用自动生成的密钥 |
burst |
当NTP服务器是可连通状态时,连发8个包,间隔2秒。 目的是提高时钟同步的质量。 |
iburst |
当NTP服务器连不上时,连发8个包,间隔2秒。 目的是快速初始化ntp时钟同步相关的信息,并且配合ntpd的-q选项。 |
version |
指定NTP报文里的版本号,范围是1~4,默认值是4。 |
prefer |
与特殊用法的prefer相同 |
minpoll |
与特殊用法的minpoll相同 |
maxpoll |
与特殊用法的maxpoll相同 |
true |
用于标记当前配置的NTP服务器是活跃的敲钟者(truechimer) |
xmtnonce |
指定NTP报文里的发送时间戳的随机值,仅对server和pool字段生效。 |
特殊用法:设置本地参考时钟
server 127.127.t.u [ prefer ] [ mode ] [ minpoll ] [ maxpoll ]
参数 |
含义 |
t |
表示本地的参考时钟类型,取值是一个整数。具体取值参考ntp文档的Reference Clock Drivers章节列举的各类驱动。 |
u |
表示单元编号,取值范围是0~3。 |
prefer |
作为一个常量附加到驱动提供的时间偏移,用于校准时钟 格式:定点十进制数,单位:秒 参考ntp文档的Reference Clock Drivers章节列举的各类驱动。 |
mode |
与time1类似,由驱动自行解析。 |
minpoll |
本地时钟的级别,一般为1。 |
maxpoll |
时钟参考源的标识符,可以是IP或者域名。 |
[ fudge ] 配置本地时钟
作用:调整时钟频率核时间偏移。
语法:fudge 127.127.t.u [ time1 ] [ time2 ] [ stratum ] [ flag1 ] ... [ flag4 ]
常见参数列举如下表。
参数 |
含义 |
t |
表示本地的参考时钟类型,取值是一个整数。具体取值参考ntp文档的Reference Clock Drivers章节列举的各类驱动。 |
u |
表示单元编号,取值范围是0~3。 |
time1 |
作为一个常量附加到驱动提供的时间偏移,用于校准时钟 格式:定点十进制数,单位:秒 参考ntp文档的Reference Clock Drivers章节列举的各类驱动。 |
time2 |
与time1类似,由驱动自行解析。 |
stratum |
本地时钟的级别,一般为1。 |
refid |
时钟参考源的标识符,可以是IP或者域名。 |
flag1 ... flag4 |
取值范围是0或1,用来控制驱动自定义的特性。 |
例如设置本地时钟源层次为5:
fudge 127.127.1.0 stratum 5
4.1 服务端配置
一个典型的服务端配置通常需要设定本地时钟,参见下方粗体标注。
driftfile /var/lib/ntp/drift
restrict default nomodify notrap nopeer noepeer noquery
restrict 127.0.0.1
restrict ::1
server 127.127.1.0 iburst local clock
includefile /etc/ntp/crypto/pw
keys /etc/ntp/keys
disable monitor
4.2 客户端配置
一个典型的客户端配置通常需要2个远端的NTP服务器,一个主服务器(下方绿字)和一个备用服务器(参见下方粗体标注)。
driftfile /var/lib/ntp/drift
restrict default nomodify notrap nopeer noepeer noquery
restrict 127.0.0.1
restrict ::1
server x.x.x.x iburst prefer minpoll 3 maxpoll 6
server x.x.x.x iburst minpoll 3 maxpoll 6
includefile /etc/ntp/crypto/pw
keys /etc/ntp/keys
disable monitor
5 数据结构
涉及ntpd工作原理的主要数据结构是报文格式和工作模式。
5.1 struct pkt报文格式
NTP协议的报文格式如下图所示。
ntpd组件用struct pkt描述此报文格式,具体定义如下。
结构体字段 |
报文字段 |
描述 |
li_vn_mode 的位域 [1:0]
|
LI (Leap Indicator) |
长度为2比特,值为0b11时表示告警状态,时钟未被同步。为其他值时NTP本身不做处理。 |
li_vn_mode 的位域 [4:2]
|
VN (Version Number) |
长度为3比特,表示NTP的版本号,目前的最新版本为3。 |
li_vn_mode 的位域 [7:5]
|
Mode |
长度为3比特,表示NTP的工作模式。不同的值所表示的含义分别是:0未定义、1表示主动对等体模式、2表示被动对等体模式、3表示客户模式、4表示服务器模式、5表示广播模式或组播模式、6表示此报文为NTP控制报文、7预留给内部使用。 |
stratum
|
Stratum |
系统时钟的层数,取值范围为1~16,它定义了时钟的准确度。层数为1的时钟准确度最高,准确度从1到16依次递减,层数为16的时钟处于未同步状态,不能作为参考时钟。 |
ppoll
|
Poll |
轮询时间,即两个连续NTP报文之间的时间间隔。 |
precision
|
Precision |
系统时钟的精度。 |
rootdelay
|
Root Delay |
本地到主参考时钟源的往返时间。 |
rootdisp
|
Root Dispersion |
系统时钟相对于主参考时钟的最大误差。 |
refid |
Reference Identifier |
参考时钟源的标识。 |
reftime |
Reference Timestamp |
系统时钟最后一次被设定或更新的时间。 |
org |
Originate Timestamp |
NTP请求报文离开发送端时发送端的本地时间。 |
rec |
Receive Timestamp |
NTP请求报文到达接收端时接收端的本地时间。 |
xmt |
Transmit Timestamp |
应答报文离开应答者时应答者的本地时间。 |
exten |
Authenticator |
验证信息。 |
通过tcpdump抓包工具可抓取到如下典型的报文。
tcpdump -i enp0s3 udp port 123 -w capture.cap
持续抓取后,可发现本地机器与远程NTP服务器多次交互后,时差缩小。
5.2 MODE_xxxx(工作模式)
NTP支持5种工作模式,定义在include/ntp.h文件内。
它们的含义列举如下表。
模式枚举 |
宏定义 |
含义 |
模式1 |
MODE_ACTIVE |
对等体模式的主动方 |
模式2 |
MODE_PASSIVE |
对等体模式的被动方 |
模式3 |
MODE_CLIENT |
客户端模式 |
模式4 |
MODE_SERVER |
服务端模式 |
模式5 |
MODE_BROADCAST |
广播模式 |
5.3 STA_UNSYNC标志
内核的time_status全局变量记录了NTP相关的重要标注,STA_UNSYNC标志就是其中之一,它的定义如下图,用于标记NTP时钟是否已同步。
【源码路径】
内核源码路径:include/uapi/linux/timex.h
用户使用头文件路径:/usr/include/linux/timex.h
【其他相关标志】
STA_PLL:表示PLL update功能已使能
5.3.1 检测STA_UNSYNC
【用户态】
路径一:ntpq打印STA_UNSYNC标志
ntpq调用kerninfo()接口打印系统配置信息时,如果检测到STA_UNSYNC标志,就打印unsync。
路径二:ntptime打印STA_UNSYNC标志
ntptime调用adjtimex系统调用后,获取到NTP状态,若有STA_UNSYNC标志,则把它打印出来。
【内核态】
sync_hw_clock()通过ntp_synced()检测STA_UNSYNC标志,如果此标志还在,说明NTP状态是未同步,则直接返回,不再继续设置RTC,反之则同步硬件时钟(RTC)。
5.3.2 设置STA_UNSYNC
【用户态】
ntp组件主要是通过adjtimex和settimeofday系统调用设置STA_UNSYNC标志。
路径一:ntpd使用adjtimex系统调用
ntpd的全局变量kern_enable默认是1,它是决定ntpd的loop_config()接口是否设置STA_UNSYNC标志的前提条件之一。
如果nptd检测到REQ_SET_SYS_FLAG或者REQ_CLR_SYS_FLAG命令码,则调用ntp_codes[ ]里注册的set_sys_flag()或者clr_sys_flag()接口,其内部的调用路径是:
set_sys_flag() -> setclr_flags() -> loop_config(LOOP_KERN_CLEAR, 0.0) -> ntp_adjtime()
clr_sys_flag() ---↗
随后 ntp_adjtime()通过adjtimex系统调用,把STA_UNSYNC标志传给内核。
路径二:ntpd使用settimeofday系统调用
ntpd服务检测到时间差后会与NTP服务器交互,进行校时,该过程中会使用settimeofday系统调用。
【内核态】
内核里面设置STA_UNSYNC标志的,主要是2个系统调用:adjtimex和settimeofday和其他辅助接口。
* 路径1:处理adjtimex系统调用 *
内核调用__do_adjtimex()接口处理adjtimex系统调用,该接口除了实现校时功能以外,还会调用process_adj_status()设置STA_UNSYNC标志。前提条件是内核已记录了STA_PLL标志,且用户指定的标志不包含STA_PLL,参见下图代码。
* 路径2:处理settimeofday系统调用 *
内核调用do_settimeofday64()接口处理settimeofday系统调用,该过程中涉及STA_UNSYNC标志的主要流程如下。
1 调用timekeeping_update(),传入TK_CLEAR_NTP标志
2 当timekeeping_update()检测到TK_CLEAR_NTP,就执行ntp_clear()接口
3 由ntp_clear()设置STA_UNSYNC标志
注:与上述流程类似的接口列举如下。
timekeeping_inject_offset()
change_clocksource()
timekeeping_inject_sleeptime64()
* 路径3:内核初始化 *
内核启动阶段会设置STA_UNSYNC标志,该过程调用路径如下。
start_kernel() -> timekeeping_init() -> ntp_init() -> ntp_clear()
最终ntp_clear()就会设置STA_UNSYNC标志。
* 路径4:定时滴答里面设置STA_UNSYNC标志 *
当系统滴答的中断到来后,中断处理过程中会调用timekeeping_advance()接口。
timekeeping_advance()可能间接调用second_overflow()设置STA_UNSYNC标志。
5.3.3 清除STA_UNSYNC
【用户态】
ntpd有多处调用ntp_adjtime()接口,其内部是adjtimex系统调用,可对内核的time_status进行修改。调用ntp_adjtime()的相关接口列举如下:
(1) 在local_clock()接口内,若kern_enable已开启,根据其他某些条件不同,可能设置STA_PLL、STA_INS、STA_DEL中的某个标志,但最终附带的效果都是清除STA_UNSYNC标志。
(2) 在direct_freq()接口内,若kern_enable已开启,则清除所有标志
(3) 在set_freq()接口内,若kern_enable已开启,则清除所有标志
(4) 在loop_config()接口内,若条件允许,则设置STA_PLL,顺带清除STA_UNSYNC
【内核态】
内核的process_adj_status()接口内对 time_status做以下修改:
1 设置STA_RONLY
由于STA_RONLY不包含STA_UNSYNC,所以相当于清除了STA_UNSYNC标志
2 设置用户通过adjtimex系统调用传来的标志
如果用户传递的标志没有STA_UNSYNC,则STA_UNSYNC依然是被清除
6 关键函数
6.1 rtc_set_time()设置RTC
路径一:系统调用ioctl
用户态通过ioctl()向 /dev/rtc 设备传递RTC_SET_TIME命令,内核的rtc_dev_ioctl()解析这个命令后,调用rtc_set_time()
路径二:系统调用adjtimex
内核收到adjtimex系统调用后,通过do_adjtimex()执行校时,调用链:
do_adjtimex() -> ntp_notify_cmos_timer()
内核ntp_notify_cmos_timer()注册了一个struct delayed_work结构体sync_work,其中带有sync_hw_clock(),它对rtc_set_time()的调用路径如下。
sync_hw_clock() -> sync_rtc_clock() -> rtc_set_ntp_time() -> rtc_set_time()
sync_hw_clock()的主要流程如下:
1 调用ntp_synced()检测STA_UNSYNC标志
若STA_UNSYNC标志还在,则说明NTP未同步,直接返回,否则继续处理
2 调用sync_cmos_clock()同步CMOS存储器上的时钟
如果系统不支持同步CMOS的功能或者没有CMOS存储器,则继续
3 调用sync_rtc_clock()同步RTC芯片上的时钟(需开启CONFIG_RTC_SYSTOHC)
其中sync_cmos_clock( )和sync_rtc_clock()都会调用sched_sync_hw_clock()以设置一个sync_work工作项目,它按照约为11分钟的周期入队列(参见下图659秒)。
周期到来后,sync_hw_clock()将再次被调用。所以当ntpd同步了系统时钟(墙上时钟),内核将每11分钟左右同步一次硬件时钟,直到STA_UNSYNC被置起。
在sync_hw_clock()内增加探测点测试,确认内核可以每隔约11分钟同步RTC,如下图所示。
从上图还可注意到设置RTC不是一蹴而就,而是尝试多次。这是因为rtc_set_ntp_time()有时会返回错误码(例如下图所示的EPROTO错误码71),此时内核会尽快重试,直到结果符合预期(返回值是0)。
6.2 rtc_read_time()读取RTC
内核启动过程中rtc_hctosys()读取RTC芯片的时钟,设置到内核的墙上时钟。
6.3 ktime_get_real_ts64()读取墙上时钟
ktime_get_real_ts64()从tk_core.timekeeper.xtime_sec读取时间戳。
路径一:clock_gettime系统调用
用户态调用clock_gettime()后,do_clock_gettime()调用ktime_get_real_ts64()。
路径二:gettimeofday系统调用
用户态调用gettimeofday()后,内核调用ktime_get_real_ts64()。
6.4 do_settimeofday64()设置墙上时钟
do_settimeofday64()设置时间戳到tk_core.timekeeper.xtime_sec
注:tk_core结构体的成员是8字节的seq和280字节的timekeeper
7 选项分析
对ntpd传入-x选项后,ntpd里面会检测到此选项。
随后执行以下调用路径:
getCmdOpts() -> loop_config(LOOP_MAX, 600) -> select_loop(FALSE)
这里select_loop()会对全局变量kern_enable赋值为FALSE,即0。
8 调试工具
ntpd组件中常用的调试方法是使用ntpd -d或者ntpq工具。
8.1 ntpd -d调试ntp通信流程
调试前须关停当前已有的ntpd服务。开始调试后,可看到以下关键信息。
1 当前系统时间、时钟精度和内核功能特性
2 配置文件的解析情况
3 监听123端口
4 与远程NTP服务器通信,最终成功校时
8.2 ntpdc交互界面
ntpdc大部分功能已被ntpq替代,但某些功能依然有用,例如修改STA_UNSYNC标志。
8.3 ntpq交互界面
ntpq继承了ntpdc的部分命令行工具,进入ntpq交互界面后,可以输入help查看支持执行哪些命令。
如果不想在交互界面执行命令,也可以用ntpq -c [命令]这种方式来执行。
8.4 ntpq查看NTP服务信息
输入ntpq -p后,ntpq将调用dogetpeers()获取各个peer的信息,并通过doprintpeers()把NTP服务状态和时钟状态等信息打印出来。
一个典型的日志参见下图。
图中第一行的各个字段含义列举如下表。
字段 |
含义 |
remote |
本机和上层ntp的ip或主机名,“*”表示良好且正在使用的NTP服务器,“+”表示良好的候选NTP服务器。 |
refid |
refid的含义取决于NTP服务器所处的stratum层级参考时钟源的主机。 对于stratum 0层,refid的值是KoD代码主要有以下情况: (1) .INIT. 表示关联初始化 (2) .STEP. 表示间隔时长改变,大于125ms小于1000ms 对于stratum 1层,refid是1到4字节的标识符,表示参考时钟: (1) .WWVB. 标准时间无线电接收器 (2) .GPS. USA GPS (3) .OLEG. 北斗邦泰的时钟源产品,使用授时中心的NTP服务器ntp.ntsc.ac.cn就会看到此标识。 对于stratum 2-15层,refid是一个IP或者服务器节点名称。 对于stratum 16层,此时的NTP服务器是未同步的,不可使用。从实测情况看,refid有以下情况: (1) KoD代码 (2) .XFAC. 根据ntp源码的peer_refresh_interface()接口,如果系统内原先与ntp服务器绑定的网络接口发生变化,那么调用peer_clear()清除crypto信息后,将会设置XFAC标识。 |
st |
stratum,NTP服务器的层级,这个值初始是16,表示NTP服务器未同步,不可使用。随着当前系统与NTP服务器逐渐建立通信,最终确定此NTP服务器的层级,那么st会被设置为0~15以内的值。 |
t |
目标节点类型,该信息通过decodeaddrtype()接口解析后获取。 它的取值定义在ntpq/libntpq.h或者手册内,列举如下。 u : unicast / manycast client s : symmetric ( peer ) b : broadcast client A : manycast server m : multicast client B : broadcast server p : pool source M : multicast server l : local (reference clock) - : unknown type
|
when |
多久前曾经同步过时间,单位:秒 |
poll |
下次更新在多久后,单位:秒 |
reach |
已经多少次向上层ntp服务器要求更新 |
delay |
网络延迟,单位:毫秒 |
offset |
时间补偿,单位:毫秒 |
jitter |
系统时间与bios时间差,单位:毫秒 |
8.5 ntpq查看kerninfo信息
这些信息在ntpq/ntpq-subs.c的kerninfo()接口内被打印输出。
8.6 ntpq查看sysinfo信息
8.7 ntpq查看时间精度等信息
9 编译安装
1 拉取代代码
2 如果代码来自github仓库,通常需要执行./bootstrap用于生成configure脚本
3 执行./configure进行配置
如果需要静态编译,则配置./configure LDFLAGS="--static"
如果需要支持-u选项,则增加配置--enable-clockctl
4 执行make进行编译
可能的问题:
1 找不到lynx命令
解决:yum install lynx
2 提示无法确定ctx的大小
解决:通常是github仓库的代码容易出错,换成已发布的源码会比较可靠
10 术语
名词 |
解释 |
PPM |
parts per million,1ppm = 0.0001% 在NTP应用场景中,PPM常用来表征时钟质量。 |
TAI |
International Atomic Time |
PLL |
Phase-Locked Loop,锁相环 |