【原創】解BUG-xenomai核心與linux核心時間子系統之間存在漂移

木多發表於2020-09-13

版權宣告:本文為本文為博主原創文章,轉載請註明出處。如有問題,歡迎指正。部落格地址:https://www.cnblogs.com/wsg1100/

一、問題起源

何為漂移?舉個例子兩顆32.768kHz晶振\(C_1\)\(C_2\),由於製造工藝原因或者使用時溫度、輔助元件引數等影響,與他們的實際頻率一定不是相同的,與32.768kHz有不同的偏差,假如\(C_1\)實際使用時頻率32.766kHz,\(C_2\)實際頻率32.770kHz。

假如有那麼兩個電子手錶,使用32.768kHz晶振,每來一個脈衝暫存器計數加1,我們通過這個電路來獲取時間,這樣計算1s的時間暫存器裡應該是32.768kHz(我們認為我們的晶振是沒問題的嘛)。好現在用\(C_1\)來計數就會使我們得到的時間比真實1S長\(\frac{1}{2000}=0.0005\)秒,這樣下來這個手錶會越走越快,即與真實時間的偏移越來越大。同樣\(C_2\)得到的時間比真實1S短\(0.0005\)秒,越走越慢。

兩個手錶在它們的計時週期(這裡舉例1秒)存在的偏差就是漂移。

X86平臺上,linux 4.4.xx之後的版本構建的xenomai,出現linux核心xenomai核心兩者時鐘存在漂移,打xenomai補丁之後,有兩個核心分別有各自的時間子系統,只不過xenomai掌管著底層的硬體timer-event的中斷觸發設定和處理,linux時間子系統的觸發源就退化為xenomai時間子系統管理的軟體timer了(linux是xenomai的idle任務嘛,當然要xenomai來提供時鐘),本質上它們還是使用同一個硬體timer源。

此問題解決時本人還未閱讀xenomai的時間子系統相關原始碼,所以其中有些解釋現在看起來·只見樹木不見森林·,懶得改了,關於xenomai的時間子系統後續會有分析文章,敬請關注!!!。

構建xenomai系統後,linux核心與xenomai核心兩個時間子系統之間的時間漂移可通過xenomai庫編譯出的工具clocktest來檢視,其中的dirft列就是表示該cpu上兩個核心之間的時間漂移。如下:

image-20200702192057651

回到問題本身,xenomai來常用來執行ethercat主站,主站DC模式下同步執行時,出現的現象是主站本地時間永遠無法與參考時鐘同步,導致每週期主站都需要讀回參考時鐘進行調整;

下面分析問題:主站由xenomai實時排程,ethercat主站工作過程中使用的是xenomai時間子系統根據底層硬體timer計算得到的時間,ethercat主站在這個時間上去同步參考時鐘,增加或減少偏移量。先不管硬體timer與真實時間的偏移,非常小先忽略,這不是重點,兩個核心都使用這個硬體timer,現在出現的問題是兩個核心對同一硬體timer的度量不一致,才會存在漂移。

由此可以推斷出xenomai時間子系統對硬體timer的度量計算有問題,下面開始從一步步挖掘分析。

二、 clocktest工具分析

clocktest工具主要用於測試xenomai 時鐘(CLOCK_REALTIMECLOCK_MONOTONICCLOCK_MONOTONIC_RAWCLOCK_HOST_REALTIME、coreclk預設CLOCK_REALTIME),相對於Linux絕對時鐘CLOCK_MONOTONIC之間的漂移,clocktest首先為每個CPU建立一個執行緒cpu_thread,並固定到相應CPU上執行,cpu_thread測試原理為:

  1. 找一個時間點作為測試起始點,此時xenomai時間表示為first_clock,Linux絕對時鐘時間表示為first_tod,它們均為一個數,單位納秒ns。

  2. 讓測量任務睡眠,睡眠時間是一個範圍的隨機數,睡眠範圍為:[1000000, 200000)納秒(這裡的睡眠時間至少1000000是因為讀取linux時鐘的函式(SYS_gettimeofday)精度只能讀取到us級,而xenomai讀取到的時間為ns級,為了使差距與us對齊,所以至少經過1ms,簡而言之計算週期1ms單位的漂移)。

  3. 讀取睡眠後的各自時鐘的計數值,讀取xenomai時鐘讀取到的值為clock_val,Linux時間計數值為first_tod同樣的時間段,xenomai時鐘計數為clock_val-first_clock,Linux時間計數值tod_val-first_tod;這個時間段的偏移率為:

    \[\frac{clock\_val-first\_clock}{tod_val-first_tod} \]

    image-20200702185626887

三、 讀linux時鐘時間

在clock工具中讀取Linux參考時鐘時間使用系統呼叫syscall(SYS_gettimeofday, &tv, NULL);

image-20200702185737947

image-20200702185743615

系統函式do_gettimeofday()讀取全域性時鐘timekeeper的值xtime_sec,然後加上系統上一個tick到此時的納秒數nsecensece是直接呼叫timekeeper使用的clocksouse對應的讀函式讀取clocksouse counter計算得到的,也可看到底層讀取到的精度是納秒級的,只不過在上一個函式將精度丟棄了。

image-20200702185819000

image-20200702185825121

四、 讀xenomai時鐘時間

在clock工具中讀取xenomai時鐘時間的函式是static inline uint64_t read_clock(clockid_t clock_id);

image-20200702185859839

read_clock()函式呼叫系統呼叫函式clock_gettime(),這是一個POSIX標準函式,在xenomai中kernel\xenomai\posix\clock.c實現如下:

image-20200702185924171

image-20200702185930882

clock_gettime()繼續呼叫__cobalt_clock_gettime()來獲取時鐘,在colocktest中傳入的引數是:CLOCK_REALTIME。進而呼叫核心函式xnclock_read_realtime (struct xnclock *clock)讀取時間,再通過ns2ts函式將讀取的到的納秒轉換為需要的timespec結構體中的tv_sectv_nsec

image-20200702190018198

xnclock_read_realtime返回 nkclock的時間加上一個與wallclock的偏移(clock->wallclock_offset), (nkclock是xenomai的時鐘源,型別為struct xnclock,當沒有使用外部時鐘時,時鐘使用X86處理器中的TSC時鐘(早期X86CPU中TSC與CPU的頻率有關,現在的CPU TSC頻率一般是固定的),當使用外部時鐘作為xenomai的時鐘時是另外一回事)。

xnclock_read_monotonic()最終呼叫xnclock_core_read_monotonic()函式:

image-20200702190059340

xnclock_core_read_monotonic()函式中由xnclock_core_ticks_to_ns()函式將xnclock_core_read_raw()函式返回的TSC CPU tick數轉換為納秒ns返回,這就是讀取的xenomai時鐘時間。怎樣獲取CPU的TSC值呢?在X86處理器中有一條指令rdtsc用於讀取TSC值 。

image-20200702190133392

image-20200702190139364

到這,整個xenomai時間讀取流程完了,就是讀取TSC的值,沒其他的了,看似沒有什麼問題。難道真的x86中的TSC不準?注意到讀取的TSC數值還需要轉換才能得到時間,轉換函式xnarch_llmulshft(),涉及到這兩個變數tsc_scale,tsc_shift,懷疑是這兩個值有問題,繼續分析,那這兩個值是幹嘛用的?

image-20200702190207700

當已知頻率F,要將A個cycles數轉換成納秒,具體公式如下:

\[轉換後的納秒數 =\frac{A}{F}*1000000000 \]

這樣的轉換公式需要除法,絕大部分的CPU都有乘法器,但是有些處理器是不支援除法,雖然我們無法將除法操作的程式碼編譯成一條除法的彙編指令,但是也可以用程式碼庫中的其他運算來取代除法。這樣做的壞處就是效能會受影響。把1/F變成浮點數,這樣就可以去掉除法了,但是又引入了浮點運算,kernel是不建議使用浮點運算的。解決方案很簡單,使用移位操作,具體可以參考clocksource_cyc2ns的操作:

image-20200702190518788

通過TSC的xnclock_core_read_raw()函式獲取了tick數目,乘以mult這個因子然後右移shift個bit就可以得到納秒數。這樣的操作雖然效能比較好,但是損失了精度(通過另外驗證下面程式碼算出的值,以這臺機器的2700M算出的值,帶入2700Mcycle得1000000230ns,有200納秒左右的偏移),還是那句話,設計是平衡的藝術,看你自己的取捨。

tsc_scale,tsc_shift在哪裡計算的呢?

具體計算在xnarch_init_llmulshft()函式中計算:

image-20200702190602567

如何獲取最佳的tsc_scaletsc_shift組合?當一個公式中有兩個可變數的時候,最好的辦法就是固定其中一個,求出另外一個,然後帶入約束條件進行檢驗。我們首先固定shift這個引數。mult這個因子一定是越大越好,mult越大也就是意味著shift越大。當然shift總有一個起始值,我們設定為32bit,因此tsc_shift從31開始搜尋,看看是否滿足最大時間範圍的要求。如果滿足,那麼就找到最佳的multshift組合,否則要tsc_shift遞減,進行下一輪搜尋。

先考慮如何計算mult值。根據公式(cycles * mult) >> shift可以得到ns數,由此可以得到計算mult值的公式:

\[mult=\frac{ns<<shift}{cycles} \]

如果我們設定ns數是10^9納秒(也就是1秒)的話,cycles數目就是頻率值。因此上面的公式可以修改為:

\[mult=((10^9<< freq)) \]

image-20200702191058424

看看上面的公式,再對照程式碼,一切就很清晰了。

那到這自然也就想到,這個freq值就是TSC對應的CPU的頻率值了。那上面的函式是由誰呼叫計算的?CPU的頻率值freq在哪獲取的?這些值在xenomai核初始化時計算。

五、xenomai xnclock初始化

在xenomai核啟動函式xenomai_init()中,由mach_setup()函式完成xenomai域相關定時器、中斷、時鐘設定。在mach_setup()函式中首先呼叫ipipe_select_timers()從全域性timers連結串列中為每一個CPU選擇一個具有最高評級的clock_event_device作為該cpu的percpu_timer。而timers是每一個clock_event_device 在register的時候, 由 ipipe_host_timer_register()將該clock_event_device新增到連結串列timers上的。

當一個CPU找到一個合適的clock_event_device的時候,就回撥用install_pcpu_timer()設定該clock_event_device為該CPU的percpu_timer,並配置該ipipe_timer頻率與CPU頻率的轉換因子(引數c2t_integ,c2t_frac, ipipe_timer_set中用到,而ipipe_timer_set常被__xnsched_run呼叫,推測與任務時間片計算有關,有時間再來分析),同時設定CPU的ipipe_percpu.hrtimer_irq為該timer的中斷號。而這裡使用的 CPU頻率值為__ipipe_hrclock_freq,他的定義如下;

image-20200702191326526

接著mach_setup()中呼叫ipipe_get_sysinfo(&sysinfo)獲取系統的資訊,系統online的cpu數,cpu的頻率,這個頻率也是使用上面cpu_khz的值。另外將0號cpu的hrtimer_irq作為系統hrtimer的中斷號,並且設定sys_hrtimer_freq的頻率(這個頻率是具體percpu_timer的頻率lapic-timer或者HPET),同樣sys_hrclock_freq也是cpu_khz的值。這些資訊在下面的初始化中要用到。

image-20200702191450456

接下來將cobalt_pipeline.timer_freq設定為timerfreq_arg也就是sysinfo中的sys_hrtimer_freq

cobalt_pipeline.clock_freq為sysinfo中的那個cpu_khz得來的sys_hrclock_freq

下面註冊兩個虛擬中斷到ipipe,一個為cobalt_pipeline.apc_virq,連結到root_domain,處理linux掛起恢復。另一個為cobalt_pipeline.escalate_virq,註冊域為xnsched_realtime_domain,handler為__xnsched_run_handler,一看這就是與xenomai排程相關的。

下面就是這裡主要的初始化xnclock的xnclock_init()函式了。

image-20200702191609175

image-20200702191613733

需要注意的是xnclock_init()函式傳的引數是cobalt_pipeline.clock_freq,也就是那個cpu_khz得來cobalt_pipeline.clock_freq

首先第一步做的就是求轉換因子tsc_scale, tsc_shift,執行xnclock_update_freq(),到這就是熟悉的xnarch_init_llmulshft(1000000000, freq, &tsc_scale, &tsc_shift),在上面的分析中提到過,下面去看cpu_khz在哪裡獲取到的。其他的不在這裡不分析。

![image-20200702191733579](D:\文件\原始碼筆記\xenomai blogs\實時核心與linux核心時鐘漂移過大原因.assets\image-20200702191733579.png)

六、 TSC init

\arch\x86\kernel\tsc.c

![image-20200702191841659](D:\文件\原始碼筆記\xenomai blogs\實時核心與linux核心時鐘漂移過大原因.assets\image-20200702191841659.png)

這裡很明瞭了,cpu_khtsc_khz是兩個值,而xenomai使用cpu_khz去算tsc與納秒的轉換因子,如果cpu_khtsc_khz相等那沒有問題,但通過新增除錯輸出,這兩個值是不等的。

![image-20200702191953017](D:\文件\原始碼筆記\xenomai blogs\實時核心與linux核心時鐘漂移過大原因.assets\image-20200702191953017.png)

![image-20200702191959548](D:\文件\原始碼筆記\xenomai blogs\實時核心與linux核心時鐘漂移過大原因.assets\image-20200702191959548.png)

剛好下面的條件判斷沒有對tsc_khzcpu_khz不相等做處理。

![image-20200702192026940](D:\文件\原始碼筆記\xenomai blogs\實時核心與linux核心時鐘漂移過大原因.assets\image-20200702192026940.png)

這臺機器上導致計算tsc_scale, tsc_shift時使用的是2700Mhz,而TSC的頻率是2712MHZ,用2700MHZ得來的tsc_scale, tsc_shift去轉換2712MHZ產生的cycles當然不對,每秒就會有12M的漂移,也就是每週期漂移\(\frac{12MHZ}{2700MHZ}=0.004444444\)秒,轉換為(微秒/秒)就是\(4444.4444(us/s)\).與機器上實際測試相符:

image-20200721170932224

解決辦法:

tsc_khz不為0時,直接 cpu_khz=tsc_khz

image-20200702192048023

修改後clocktest測試如下:

image-20200702192057651

附:xenomai核心的一些時鐘資訊:

$cat /proc/xenomai/clock/coreclok
gravity: irq=100 kernel=1341 user=1341
devices: timer=lapic-deadline, clock=tsc
 status: on
 setup: 100
 ticks: 443638843357 (0067 4aef87dd)

修改後:

$cat /proc/xenomai/clock/coreclok
gravity: irq=99 kernel=1334 user=1334
devices: timer=lapic-deadline, clock=tsc
 status: on
 setup: 99
 ticks: 376931548560 (0057 c2defd90)

lapic-deadline 是上面解析的CPU0 的percpu_timer,deadline表示lapic-timer支援deadline事件觸發;

關於xenomai的時間子系統後續會有整理分析文章,敬請關注!!!。

後面一直沒有使用4.14及以上的核心,不知道現在還有沒這個問題。大家可以看看,再提個issue或者啥的.....

相關文章