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()派發到期的定時器。