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

相關鏈接

gpsd v3.20 源代碼分析
chrony v3.5 源代碼分析
ptp4l v4.2源代碼分析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容