x86 Linux 下實現 10us 誤差的高精度延時|軟體開發

x86 Linux 下實現 10us 誤差的高精度延時|軟體開發

導讀:

在 Linux 下實現高精度延時,網上所能找到的大部分方法只能實現 50us 左右的延時精度。

本文字數:4554,閱讀時長大約:7分鐘

在 Linux 下實現高精度延時,網上所能找到的大部分方法只能實現 50us 左右的延時精度。今天讓我們來看下嘉友創資訊科技的董文會是如何解決這個問題的,將延時精度提升到 10us。

問題描述

最近在開發一個專案,需要用到高精度的延時機制,設計需求是 1000us 週期下,誤差不能超過 1%(10us)。

由於專案硬體方案是用英特爾的 x86 處理器,熟悉 Linux 硬體的人都知道這個很難實現。當時評估方案時候有些草率,直接採用了 “PREEMPT_RT 補丁+核心 hrtimer+訊號通知” 的方式來評估。當時驗證的結果也很滿意,於是興沖沖的告訴領導說方案可行,殊不知自己挖了一個巨大的坑……

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

訊號通知只能通知到程序,而目前移植的方案無法做到被通知的程序中無其他執行緒。這樣高頻的訊號發過來,其他執行緒基本上都會被幹掉。(補充說明:這裡特指的是核心驅動通知到應用層,在使用者層中是有專門的函式可以通知不同執行緒的。並且這個問題經過研究,可以透過設定執行緒的 sigmask 來解決,但是依舊無法改變方案行不通的結論)

這也是主要原因,專案中需要用的 Ethercat 的同步週期雖然可以在程式開始時固定,但是實際執行時的執行週期是需要動態調整的,調整範圍在 5us 以內。這樣一來,動態調整 hrtimer 的開銷就變得無法忽略了,換句話說,

我們需要的是一個延時機制,而不是定時器。

所以這個方案被否定了。

解決思路

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

1、sleep方案的確定

嘗試過 、、、、 等,最終確定用 ,選它的原因並不是因為它支援 ns 級別的精度。因為經過測試發現,上述幾個呼叫在週期小於 10000us 的情況下,精度都差不多,誤差主要都來自於上下文切換的開銷。選它的主要原因是因為它支援 選項,即支援絕對時間。這裡舉個簡單的例子,解釋一下為什麼要用絕對時間:

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

2、改用實時執行緒

將重要任務的執行緒改成實時執行緒,排程策略改成 FIFO,優先順序設到最高,減少被搶佔的可能性。

3、設定執行緒的親和性

對應用下所有的執行緒進行規劃,根據負載情況將幾個負載比較重的任務執行緒分別繫結到不同的 CPU 核上,這樣減少切換 CPU 帶來的開銷。

4、減少不必要的sleep呼叫

由於很多工都存在 呼叫,我用 命令分析了整個系統中應用 呼叫的比例,高達 98%,這種高頻次休眠+喚醒帶來的開銷勢必是不可忽略的。所以我將 迴圈中的 改成了迴圈等待訊號量的方式,因為 pthread 庫中訊號量的等待使用了 ,它使得喚醒執行緒的開銷會小很多。其他地方的 也儘可能的最佳化掉。這個效果其實比較明顯,能差不多減少 20us 的誤差。

5、絕招

從現有應用中剝離出最小任務,減少所有外界任務的影響。

經過上述五點,1000us 的誤差從一開始的 ±100us,控制到了 ±40us。但是這還遠遠不夠……

黔驢技窮的我開始漫長的搜尋研究中……

這期間也發現了一些奇怪的現象,比如下面這張圖。

x86 Linux 下實現 10us 誤差的高精度延時|軟體開發

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

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

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

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

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

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

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

柳暗花明

在嘗試了 200 多次引數調整,被這個問題卡了一個多禮拜之後,偶然發現了一篇戴爾的技術文件《Controlling Processor C-State Usage in Linux》,受到這篇文章的啟發,終於解決了這個難題。

隨後經過一番針對性的查詢終於摸清了來龍去脈:

原來英特爾的 CPU 為了節能,有很多功耗模式,簡稱 C-state。

圖表來自DELL

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

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

x86 Linux 下實現 10us 誤差的高精度延時|軟體開發

針對性查詢之後,發現網上也有網友測試,2。6 版本的核心不會預設開啟這個,但是 3。2 版本的核心就會開啟,而且對比測試發現,這兩個版本核心在相同硬體的情況下,上下文切換開銷可以相差 10 倍,前者是 4us,後者是 40-60us。

解決辦法

1、永久修改

可以修改 Linux 的引導引數,修改 檔案中的 選項,改成下面的內容:

然後使用 命令使引數生效,重啟即可。

2、動態修改

可以透過向 這個檔案中寫值,來調整 C1/C1E 模式下上下文切換的開銷。我選擇寫 直接關閉。當然你也可以選擇寫一個數值,這個數值就代表上下文切換的開銷,單位是 us。比如你寫 ,那麼就是設定開銷為 1us。當然這個值是有範圍的,這個範圍在 檔案中可以查到,X 代表具體哪個核,Y 代表對應的 idle_state。

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

x86 Linux 下實現 10us 誤差的高精度延時|軟體開發

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

本文作者董文會,經授權轉載。

題圖來源:網路。

TAG: 執行緒開銷延時誤差這個