1 数组
ARR_Instance定义数组。
typedef struct ARR_Instance_Record *ARR_Instance;
它最终的定义是ARR_Instance_Record,它管理一个动态分配的数组。
- elem_size是元素大小
- alloated是元素个数
- data是真实的数组地址
- used是实际使用的元素个数
2 日志
2.1 LOG模块
LOG模块保存一般的日志。
日志可以写入用户指定的日志文件中。如果没有指定,则可以写入系统日志中。
- file_log保存打开的日志文件。调用LOG_OpenFileLog()可以打开指定的文件。
- system_log是指定是否写系统日志的标志,调用Log_OpenSystemLog()可以打开系统日志。
- LOG()打印一般的日志。
- LOG_FATAL()打印日志并退出程序。
- DEBUG_LOG()打印用于调试的程序。要打印这类日志,需要在编译时指定--eanble-debug选项,并且在运行时指定-d选项。
2.2 LogFile
除了一般的日志,某些模块,如refclocks,rtc等还有自己独立的日志文件。LogFile用于保存这些日志。
- name是文件名
- banner 一般LogFile保存的是一组统计数据的数据表,banner是这个表的表头。
- file是打开的文件
- writes是写日志的次数
- LOG_FileOpen()打开模块的日志文件。
- LOG_FileWrite()写模块的日志文件。
LogFile实例保存在数组logfiles中,每个模块一个实例。
struct LogFile logfiles[6];
3 配置文件
3.1 CNF_ParseLine()
如下是chrony配置文件的一个例子。
# chrony.conf
pool ntp.ubuntu.com iburst maxsources 4
pool 0.ubuntu.pool.ntp.org iburst maxsources 1
pool 1.ubuntu.pool.ntp.org iburst maxsources 1
pool 2.ubuntu.pool.ntp.org iburst maxsources 2
refclock PPS /dev/pps0 lock NMEA refid PPS
refclock SHM 0 refid NMEA noselect
rtcsync
CNF_ParseLine()解析配置文件中的一行。
- CPS_NormalizeLine()删除注释和多余的空格符。
- 调用CPS_SplitWord()取出下一个关键词。根据关键词调用相应的处理函数。
- 对于
pool
、server
,调用parse_source(); - 对于
refclock
,调用parse_refclock()。 - 对于
rtcsync
,调用parse_null()设置rtc_sync的值。
3.1 parse_refclock()
在parse_refclock()中,
- 在for()循环中,调用CPS_SplitWord得到关键字并解析,这样得到refclock的各项参数。
- 调用ARR_GetNewElement()从数组refclock_sources得到一个可用的实例,并根据以上参数初始化。refclock_sources的类型是RefclockParameters,它是在CNF_Initialize()中创建的。
static ARR_Instance refclock_sources;
RefclockParameters保存参考时钟源的配置参数。
- driver_name是时钟类型
- driver_parameter是时钟参数
- ref_id是时钟名字,这是一个uint32_t值,所以名字最多允许4个字符。
- lock_ref_id 如果这个时钟源依赖其他时钟源,则lock_ref_id保存被依赖的时钟源的名字。比如前面配置文件中,时钟源
PPS
的lock NMEA
选项,表示它依赖时钟源NMEA
。 - sel_options是在选择时钟源时的选项,比如指定是否可以被选择。比如,时钟源NMEA的noselect选项,表示它不应该选择为时钟源。这里它只用作PPS的参考时钟源,提供时间戳。
关于以上配置文件的其他说明。
-
refclock PPS
意味着这个时钟源基于PPS设备,也就是/dev/pps0
。PPS设备只提供每秒一次的脉冲,它本身不提供时间戳,所以它依赖时钟源NMEA
。 -
reflock SHM 0
意味着NMEA时钟源是SHM类型。后面可以看到,这是名字为”NTP0”的一块共享内存,里面分为若干小块(看gpsd的代码,是8个小块),用共享内存的起始地址加一个索引值来引用。这里的0
表示从第一个小块。每个小块可以存放一组时间戳数据。gpsd这样的生产者将从RTK接收的数据写入这个小块,而gpsmon/cgps/chronyd这样的消费者程序从这个小块读。 -
pool
指定的是NTP时间源。如果希望只从RTK设备授时,建议注释掉这几行,以免有干扰。 -
rtcsync
表示是不是定期将系统时间同步到rtc硬件。
3 定时器
3.1 TimerQueueEntry
TimerQueueEntry保存定时器。
- 成员next、prev将多个定时器串连成一个链表。
- 成员id保存唯一的定时器编号
- ts是定时器的超时时间。
- handler是定时器的处理函数,arg是函数的参数
tqe_free_list保存可用的定时器,而timer_queue保存使用中的定时器。n_timer_queue_entries是timer_queue中的定时器数量。
为了方便给定时器分配id, next_tqe_id保存当前定时器最大编号。
TimerQueueEntry timer_queue;
unsigned long n_timer_queue_entries;
TimerQueueEntry *tqe_free_list;
SCH_TimeoutID next_tqe_id;
allocate_tqe()从tqe_free_list中得到一个可用的定时器。如果tqe_free_list还没有分配,会先分配它。
3.2 SCH_AddTimeout()
SCH_AddTimeout()启动一个定时器。
- 调用allocate_tqe() 从tqe_free_list得到一个可用的定时器。
- 调用get_new_tqe_id()得到一个未使用的定时器id,给定时器编号
- timer_queue中定时器是按照到期时间排序的。这里遍历timer_queue,调用LCL_CompareTimespecs()比较,得到一个合适的插入位置。
- 将新的定时器插入timer_queue。
SCH_AddTimeout()的参数指定的时间戳是一个绝对时间,而SCH_AddTimeoutByDelay()指定了一个相对时间。
- 调用LCL_ReadRawTime()得到当前时间,再调用LDC_AddDoubleToTimespec()得到绝对时间,然后把后面的工作委托给SCH_AddTimeout()。
3.3 dispatch_timeouts()
dispatch_timeouts() 派发到期的定时器。在while()循环中,
- 调用LCL_ReadRawTime()得到当前时间
- 调用UTI_CompareTimespec(),检查timer_queue堆定时器是否超时。
- 如果定时器到期,调用它的处理函数。调用SCH_RemoveTimeout(),从timer_queue移除定时器。其中调用release_tqe(),将它重新放回tqe_free_list。
4 参考时钟源 Reference Clock
4.1 RCL_Instance_Record 与RefclockDriver
参考时钟源保存在全局数组reflocks中。
ARR_Instance refclocks;
它的类型是RCL_Instance_Record。
- 成员driver保存这个时钟源类型的驱动模块RefclockDriver。这个模块负责创建创建时钟源实例。这个实例需要保存它的相关数据,data保存这个数据。
- driver_parameter保存额外的参数,比如
refclock SHM 0
,SHM用于指定时钟源处理器,0保存在driver_parameter中。
RefclockDriver可以是如下的驱动模块:SHM类型对应RCL_SHM_driver、PPS类型对应RCL_PPS_driver,SOCK类型对应RCL_SOCK_driver。
- init()负责初始化驱动模块
- chronyd定期调用poll(),驱动模块处理。
4.2 CNF_AddRefclocks()
CNF_AddReflocks() 根据数组refclock_sources创建参考时钟,也就是RCL_Instance_Record实例。
- 调用ARR_GetElement()从refclock_soures得到一组配置项,也就是RefclockParameters实例。
- 调用RCL_AddRefclock()增加一个新的参考时钟源实例。
在RCL_AddRefclock()中,
- 调用MallocNew()创建RCL_Instance_Record实例,保存到数组refclocks中。
- 根据参考时钟源的名字,也就是RefclockParameters->driver_name,确定时钟源的驱动模块,也就是RCL_Instance_Record::driver值。比如
SHM
对应RCL_SHM_driver。 - 将RefclockParameters配置项的值,复制到RCL_Instance_Record的相应成员,比如 从RefclockParameters->driver_parameter到RCL_Instance_Record::driver_parameter。
- 调用RefclockDriver::init(),对时钟源驱动模块初始化。
- 调用SPF_CreateInstance(),创建一个样本过滤器(sample filter)实例。SPF模块负责统计时间戳信息,并由此决定时间源是否有效,哪个参考时钟源更好。
- 调用SRC_CreateNewInstance(),创建一个SRC_Instance_Record实例。SRC模块保存时间源的信息。
4.3 RCL_SHM_driver
RCL_SHM_driver是SHM类型时钟源的驱动模块。
它的init()函数是shm_initialise()。
- 调用RCL_CheckDriverOptions(),从选项参数得到访问共享内存块的权限。
- 调用RCL_GetDriverParameter(),从RCL_Instance_Record::driver_parameter,得到共享内存块的索引值。
- 调用shmget()获取共享内存块的句柄。共享内存块由SHMKEY加索引值指定。
#define SHMKEY 0x4e545030 // 这实际上是字符串”NTP0”
- 内存块保存的结构是shmTime。
gpsd
写入这个结构定义的数据,chronyd
读出。
- 调用 shmat()得到这个共享内存块的地址。
- 调用RCL_SetDriverData()将这个地址保存到RCL_Instance_Record::data。
shm_poll() 从共享内存块读时间戳数据的原始样本,并累积。
- 调用RCL_GetDriverData()得到共享内存块的地址。检查其中包括的shmTime结构中的数据有效性。如果无效。则中止处理。
- 调用UTI_NormaliseTimespec()规范化shmTime的时间戳,包括clock_ts和receive_ts。clock_ts是解析RTK设备消息得到的时间戳,receive_ts是接收到消息时的系统时间。
- 调用UTI_DiffTimespecsToDouble()得到clock_ts与receive_ts的差值。
- 调用RCL_AddSample(),其中调用accumulate_sample(),它又调用SPF_AccumulateSample()。SPF模块负责统计样本,而这个函数将样本保存到SPF_Instance_Record::samples[]数组中。SPF_Instance_Record是一个SPF实例。
4.4 RCL_PPS_driver
PPS类型时钟源的init()是pps_initialise()。
- 调用RCL_GetDriverParameter(),得到PPS设备的路径,如
/dev/pps0
。 - 调用RCL_CheckDriverOptions(),从选项参数得到PPS设备的访问模式mode。
- 调用open()打开PPS设备。得到其句柄fd。
- 调用time_pps_create()从句柄得到一个handle。用这个handle调用time_pps_getcap(),time_pps_getparams(),time_pps_setparams(),根据mode设置新的模式。
- 创建pps_instance实例,保存在RCL_Instance_Record::data中。
pps_poll() 在每次PPS脉冲来到时,从它参考的时钟源累积原始样本。
- 调用RCL_GetDriverData()得到保存的pps_instance实例。
- 调用time_pps_fetch()读pps信号,失败的话,打印超时提示并退出。成功的话,获取此时的系统时间。
- 调用RCL_AddPulse()记录这次脉冲信号,它又调用RCL_AddCookedPulse()。
RCL_AddCookedPulse()结合参考时钟源的时间戳,累积原始样本。
refclock SHM 0 refid NMEA noselect
refclock PPS /dev/pps0 lock NMEA refid PPS
-
lock NMEA
意味着参考时间源NMEA的时间戳原始样本。这时REC_Instance_Record::lock_ref的值是参考时钟源在数组refclocks中的索引值。 - 调用get_refclock()得到参考时钟源。
- 调用SPF_GetLastSample()得到参考时钟源最新的一个原始样本。
- 调用UTI_DiffTimespecsToDouble(),计算PPS脉冲的接收时间与样本时间的差值。
- 调用accumulate_sample()累积样本时间,参数是PPS脉冲的接收时间和这个差值。
- 调用SPF_AccumulateSample()累积样本,保存到SPF_Instance_Record::samples[]数组中。
4.5 RCL_StartRefclocks()
RCL_StartRefclocks()启动参考时钟源。
- 调用ARR_GetSize()得到数组refclocks中时钟源个数。
- 遍历refclocks,
- 调用get_refclock()得到时钟源,
- 调用SRC_SetActive(),标记RCL_Instant_record::source为激活状态。
- 启动定时器,设置其处理函数为poll_timeout()。
4.5 poll_timeout()
定时器超时时,poll_timeout()被调用。
- 调用时钟源驱动模块的的poll()。对于RCL_SHM_driver是shm_poll(), 对于RCL_PPS_driver是pps_poll()。每调用一次,RCL_Instance_Record::driver_polled递增1。
- 如前面所说,调用poll()会累积样本,而调用SPF_GetFilteredSample()过滤处理这些样本。当RCL_Instance_Record::driver_polled超过指定阈值,调用SPF_GetFilteredSample(),也就是必须累积指定数量的样本,才会开始过滤。
- 当过滤的结果有效,达到用于授时的要求时,调用SRC_UpdateReachability(),其中设置source的可用性值,也就是SRC_Instance_Record::reachability。
- 调用SRC_AccumulateSample()针对这个source开始累积这些过滤的样本。
- 调用SRC_SelectSource()尝试重新选择当前的参考时钟源。如果这个source被选中,则会尝试给系统授时。这一点后面再讲。
- 当过滤的结果无效,调用SRC_UpdateReachability()清除这个source的可用性值。如果这个source就是当前的时钟源,则清除这个装填,尝试重新选择时间源。
- 由于定时器是one-shot模式,最后需要调用SCH_AddTimeoutByDelay()再次启动它。
4.6 SPF_GetFilteredSample()
SPF_GetFilteredSample() 过滤原始样本,计算可以用于授时的样本。
- 调用select_samples()得到一组合适的原始样本
- 调用combine_selected_samples(),从这组样本计算一个授时可用的样本。
- 这时原始样本不需要了,调用SPF_DropSamples()清除,重新开始累积。
4.6 LCL_SetSyncStatus()
系统时间是否授时成功有一个状态标志,可以使用timedatectl,可以查看这个状态,如下面图中的System clock synchronizaition。
LCL_SetSyncStatus()设置这个状态。
- 它最终调用adjtimex()设置状态。
- 值得一提的是
rtcsync
选项。CNF_GetRtcSync()得到这个值。如果要设置同步状态为true,但是如果这个值为true,则会同步状态改成false。也就是仍然保持未同步状态。 - rtcsync选项的目的是:系统时间将定时同步到硬件rtc,那时会同时更新时间同步状态,所以这里不更新了。
# /etc/default/chrony.conf
rtcsync
chronyd启动时调用REF_Initialize()。
- 调用REF_SetUnsynchronized()。它调用 LCL_SyncStatus()将时间同步状态设置为false。
4.7 SRC_SelectSource()
SRC_SelectSource()比较备选参考时间源,根据打分选择一个作为当前时间源。
- 多次遍历数组sources中的时间源,根据设置排除某些时间源。比如,如果时间源有noselect选项,则忽略它;
refclock SHM 0 refid NMEA noselect
- 检查source的统计数据,检查是否能作为备选,相应设置其状态 source.status。如果可以,设置其状态为SRC_OK。
- 遍历sources,给每个source计算分值score,找出分值最大的source。选择这个source作为当前时间源,设置器状态为SRC_SELECTED。
- 调用REF_SetRefrence()调整系统时间,并设置同步状态。
4.8 SRC_SetReference()
REF_SetReference()调整系统时间,并设置同步状态。在同步状态为false时,可能直接调整到位,在状态为true时,会逐步接近。
- 如果要调整的偏移太大,maybe_log_offset()会打印“System clock wrong ...”的错误提示。调用LCL_ApplyStepOffset(),它最终调用SYS_Timex_Adjust()直接调整系统时间。
- 调用LCL_SetSyncStatus()设置系统时间同步状态为true。
5 main()
main()函数的步骤如下。
- 调用LOG_OpenFileLog()/LOG_OpenSystemLog(),设置LOG模块。
- 调用CNF_ReadFile(),读取配置文件并解析得到chronyd的配置选项,比如参考时钟源的配置。
- 初始REF/SYS/SCH等各个模块。比如REF_Initialise()设置时间状态为false。
- 调用SCH_MainLoop()。其中调用dispatch_timeouts()派发到期的定时器。