X86-Linux下高精度延時方案的實現(10us誤差)

語言: CN / TW / HK

點擊上方“嘉友創信息科技”,選擇關注,乾貨福利,第一時間奉上。

Linux實現高精度延時,網上大部分方法只能實現50us左右的延時精度,今天我們來看下董總是如何解決的,將延時精度提升到10us。

0 1
問題描述

朋友最近項目上在開發Ethercat主站,需要用到高精度的延時機制,設計需求是1000us週期下,誤差不能超過1%(10us)

由於項目硬件方案是intel的處理器X86,熟悉linux的人都知道這個很難實現,當時評估方案的時候有些草率,直接用的PREEMPT_RT補丁+內核hrtimer+signal通知的方式來評估的。當時驗證的結果也很滿意,於是興沖沖的吿訴領導説方案可行,殊不知自己挖了一個巨大的坑。。。

實際項目開始的時候,發現這個方案根本行不通,有兩個原因:

  • signal通知只能通知到進程,而目前移植的方案無法做到被通知的進程中無其他線程。這樣高頻的signal發過來,其他線程基本上都會被幹掉。(補充説明:這裏特指的是內核驅動通知到應用層,在用户層中是有專門的函數可以通知不同線程的。並且這個問題經過研究,可以通過設置線程的sigmask來解決,但是依舊無法改變方案行不通的結論)

  • 這也是主要原因,Ethercat的同步週期雖然可以在程序開始時固定,但是實際運行時運行週期是需要動態調整的,調整範圍在5us以內。這樣一來,動態調整hrtimer的開銷就變得無法忽略了,換句話説,我們需要的是一個延時機制,而不是定時器。

所以這個方案被PASS了。

0 2
解決思路

既然signal不行,那隻能通過其他手段來分析。總結下來我大致進行了如下的嘗試:

一、sleep方案的確定:嘗試過usleep,nanosleep,clock_nanosleep,cond_timedwait,select等,最終確定用clock_nanosleep,選它的原因並不是因為它支持ns級別的精度因為經過測試發現,上述幾個調用在週期小於10000us的情況下,精度都差不多,誤差主要都來自於上下文切換的開銷選它的主要原因是因為它支持選項叫TIME_ABSTIME,這個選項的意思是支持絕對時間這裏舉個簡單的例子,解釋一下為什麼要用絕對時間:

while(1){  do_work();  sleep(1);  do_post();}

假設上面這個循環,我們目的是讓do_post的執行以1s的週期執行一次,但是實際上,不可能是絕對的1s,因為sleep()只能延時相對時間,而目前這個循環的實際週期是do_work的開銷+sleep(1)的時間。所以這種開銷放在我們需求的場景中,就變得無法忽視了。而用clock_nanosleep的好處就是一方面它可以選擇時鐘源,其次就是它支持絕對時間喚醒,這樣我在每次do_work之前都設置一下clock_nanosleep下一次喚醒時的絕對時間,那麼clock_nanosleep實際執行的時間其實就會減去do_work的開銷,相當於是鬧鐘的概念。

二、改用實時線程:將重要任務的線程改成實時線程,調度策略改成FIFO,優先級設到最高,減少被搶佔的可能性。

三、設置線程的親和性:對應用下所有線程進行規劃,根據負載情況將幾個負載比較重的任務線程分別綁定到不同的CPU核上,這樣減少切換CPU帶來的開銷。

四、減少不必要的sleep調用:由於很多任務都存在sleep調用,我用strace命令分析了整個應用sleep系統調用的比例,高達98%,這種高頻次休眠+喚醒帶來的開銷勢必是不可忽略的。所以我將main循環中的sleep改成了循環等待信號量的方式,因為pthread庫中信號量的等待使用了futex,它使得喚醒線程的開銷會小很多。其他地方的sleep也儘可能的優化掉。這個效果其實比較明顯,能差不多減少20us的誤差

五、絕招:從現有應用中剝離出最小任務,減少所有外界任務的影響

經過上述五點,1000us的誤差從一開始的±100us,控制到了±40us。但是這還遠遠不夠。。。
黔驢技窮的我開始漫長的Google+Baidu ing。。。。
這期間也發現了一些奇怪的現象,比如下面這張圖。

圖片是用python對抓包工具的數據進行分析生成的,參考性不用質疑。縱軸代表實際這個週期所耗費的時間。可以發現很有意思的現象:

1. 每隔一定週期,會集中出現規模的誤差抖動

2. 誤差不是正態分佈,而是頻繁出現在±30us左右的地方

3. 每次產生較大的誤差時,下個週期一定會出現一次反向的誤差,而且幅度大致相同(這點從圖上看不出來,通過其他手段分析的)。

簡單描述一下就是假設這個週期的執行時間是980us,那下個週期的執行時間一定會在1020us左右。

第1點和第2點可以經過上面的4條優化措施消除,第3點沒有找到非常有效的手段,我的理解可能內核對這種誤差是知曉的並且有意在彌補,如果有知道相關背後原理的大神歡迎分享一下。

針對這個第三點奇怪的現象我也嘗試做了手動的干預,比如設一個閾值,當實際程序執行的誤差大於這個閾值時,我就在設置下一個週期的喚醒時間時,手動減去這個誤差,但是運行效果卻大跌眼鏡,更差了。。。

0 3
柳暗花明

在嘗試了200多次參數調整,被這個問題卡了一個多禮拜之後,也不知道當時打了什麼搜索的關鍵字,偶然發現了一篇dell的文檔。終於解決了這個難題,文檔標題是:

隨後經過一番針對性的查找終於摸清了來龍去脈:
原來Intel的cpu為了節能,有很多功耗模式,簡稱C-states。

當程序運行的時候,CPU是在C0狀態,但是一旦操作系統進入休眠,CPU就會用Halt指令切換到C1或者C1E模式,這個模式下os如果進行喚醒,那麼上下文切換的開銷就會變大!

這個選項按道理BIOS是可以關掉的,但是坑的地方就在於版本相對較新的linux內核版本,默認是開啟這個狀態的,並且是無視BIOS設置的!這就很坑了!

針對性查找之後,發現網上也有網友測試,2.6版本的內核不會默認開啟這個,但是3.2版本的內核就會開啟,而且對比測試發現,這兩個版本內核在相同硬件的情況下,上下文切換開銷可以相差10倍,前者是4us,後者是40-60us。

0 4
解決辦法

一、久修改可以修改linux的引導參數,修改/etc/default/grub文件中的GRUB_CMDLINE_LINUX_DEFAULT選項,改成下面的內容:

intel_idle.max_cstate=0 processor.max_cstate=0 idle=poll

然後使用update-grub命令使參數生效,重啟即可。

二、動態修改可以通過往/dev/cpu_dma_latency這個文件中寫值,來調整C1/C1E模式下上下文切換的開銷。我選擇是寫0,就直接關閉。當然你也可以選擇寫一個數值,這個數值就代表上下文切換的開銷,單位是us。比如你寫1,那麼就是設置開銷為1us。當然這個值是有範圍的,這個範圍在/sys/devices/system/cpu/cpuX/cpuidle/stateY/latency文件中可以查到,X代表具體哪個核,Y代表對應的idle_state。

至此,這個性能問題就得到了完美的解決,目前穩定測試的性能如下圖所示:

實現了X86-Linux下高精度延時1000us精確延時,精度10us。

謝謝關注,下期更精彩。

收藏、點贊、在看一鍵三連

-- END --


本文分享自微信公眾號 - 嵌入式技術筆記(fensnote)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閲讀的你也加入,一起分享。