chrony v3.5 源代码分析

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()派发到期的定时器。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容