美文网首页
chrony v3.5 源代码分析

chrony v3.5 源代码分析

作者: RonZheng2010 | 来源:发表于2024-06-03 15:20 被阅读0次

    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()取出下一个关键词。根据关键词调用相应的处理函数。
    • 对于poolserver,调用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保存被依赖的时钟源的名字。比如前面配置文件中,时钟源PPSlock 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()派发到期的定时器。

    相关文章

      网友评论

          本文标题:chrony v3.5 源代码分析

          本文链接:https://www.haomeiwen.com/subject/yojeqjtx.html