選單

Linux實時性措施探討

有人質疑Linux實時性的必要性

有人說加個微控制器處理實時性

有人說把Linux玩成了RTOS

工業多軸伺服控制系統

EtherCat主站等應用

汽車控制相關應用

這是董總第二篇關於

Linux實時性

的研究,極具參考價值,最後的經驗總結恰到好處。

翻譯前言

全篇的內容來自NASA的文章《Challenges Using Linux as a Real-Time Operation System》,覺得內容實在太乾貨了,所以在自己理解的基礎上總結提煉了一下,分享給大家。鑑於個人水平有限,有些專業的場景由於自己沒有接觸過所以可能存在誤解,有疑問的可以看原文。

核心搶佔

1、主線版本和CONFIG_PREEMPT_RT補丁

CONFIG_PREEMPT_NONE

:最常見模式,核心無法搶佔,在現代ARM處理器上的典型切換延時低於200us,但是同時核心無法保證排程的最大延遲,通常在幾百ms左右

CONFIG_PREEMPT_VOLUNTARY

:核心仍舊無法搶佔,但是提供了某些狀態下自動放棄處理器的機制,添加了一些重排程點,減少排程延時

CONFIG_PREEMPT

:核心支援搶佔,排程延遲遠小於非搶佔核心。現代ARM處理器的開銷在10us,最大到100us左右

Linux發行版中,伺服器版本通常使用前兩個選項,為了達到最大的效能。桌面版則通常使用後兩個選項,為了達到更好的使用者體驗。

CONFIG_PREEMPT_RT

專案是一個基於Linux主線的補丁,它使核心支援完全搶佔,事實上

CONFIG_PREEMPT是CONFIG_PREEMPT_RT

的一個子集。

CONFIG_PREEMPT_RT

補丁主要做了以下幾點:

用實時互斥鎖(rtmutex)代替核心原有的鎖方案,使核心鎖支援搶佔

自旋鎖和讀寫鎖保護的臨界區也支援搶佔

對於核心的自旋鎖和訊號量,支援優先順序繼承

將中斷處理過程移入可搶佔的核心執行緒

核心定時器進行分頻,提高核心定時器的精度,副作用是使用者態的POSIX定時器精度也會一起提高

在CONFIG_PREEMPT_RT補丁下,核心的一些資源管理函式,尤其是記憶體管理,還是會包含不確定的開銷。所以,CONFIG_PREEMPT_RT會假設實時應用在開始實時任務之前,已經獲取了想要的記憶體資源。在ARM處理器平臺上評估結果顯示,

CONFIG_PREEMPT_RT

下最大的中斷響應時間比

CONFIG_PREEMPT

快了37%~72%;任務切換時間則差不多。

OSADL組織測試了不同穩定版本linux核心打了

CONFIG_PREEMPT_RT

補丁之後的系統延遲,根據硬體效能的不同,從40us-400us不等。

硬體選型和實時性

儘管CONFIG_PREEMPT_RT補丁提供了一個支援完全搶佔的核心,但是系統的實時性依舊受硬體平臺、硬體配置以及驅動的影響。這些措施大部分都適用於所有的實時系統。舉一些例子:

電源管理功能,如:CPU睿頻,超執行緒技術等,都會使得系統的執行時間和響應時間變得不確定。

如果硬體裝置偶發的獨佔一個資源,比如PCI匯流排,那也會使得系統的實時性下降。

韌體,例如BIOS,也會無法預估的掛起作業系統來做一些底層的操作。最常見的就是X86處理器是系統管理模式,很多功能都會牽扯到系統管理模式,比如電源管理,裝置控制,事務管理,裝置模擬,軟體糾錯等。這些呼叫的開銷經常需要100us左右。

裝置驅動不能包含禁止系統搶佔的程式碼,慶幸的是現在的Linux核心驅動都會設計成上半部和下半部兩部分,上半部會盡快釋放資源,下半部則放在核心執行緒中去處理,去處理一些更佔用資源的任務。這樣的設計完美契合了CONFIG_PREEMPT_RT理念。

記憶體管理

一個實時的應用程式常常需要在初始化階段就獲取和鎖定它需要的記憶體資源。雖然linux也提供類上述兩個功能,但是linux的兩個功能使得它們的工作方式和其他作業系統不同,嚴格限制了預分配記憶體。這兩個功能一個是記憶體對映和按需分頁,另一個就是過需分配記憶體。

1、記憶體對映和按需分頁(Demand Paging)和記憶體鎖定(Memory Locking)

記憶體對映和按需分頁是很多作業系統都用到的技術,原理是

只有當應用程式實際訪問到對應的頁時,才會將虛擬地址裝載到對應的物理地址

。然而,在按需分頁的早期的實現機制中,應用程式中不在物理地址上的那部分虛擬地址,是對映到檔案系統的swap檔案的。而其他系統也會用swap技術來將可執行頁(主要是堆疊)對映到檔案系統。Linux的按需分頁做的更進一步,它將堆疊頁對映到一個單獨的只寫頁(初始化為0)。應用程式的虛擬記憶體區只有當發生了寫請求之後,才會對映到對應的物理地址或者swap檔案。這個過程就會產生一個

缺頁中斷

,Linux接收到中斷後就會做出響應,去重新建立虛擬地址到物理地址的對映(原先是對映到一個全0的區域),但是與此同時,

這個虛擬頁也有了重新對映到swap檔案的許可權

。這樣一來,當應用程式需要申請一大片記憶體時,它就不能立馬消費對應的物理記憶體或者swap檔案區,不管它是否馬上就要用到。這種策略應對大部分應用場景有很好的效果,但是對於實時應用程式卻不友好。

所以,一個實時應用程式在設計的時候,就需要儘可能的在初始化階段就分配好它所需要的記憶體資源,這樣就避免在實時任務執行時去動態請求記憶體,因為這種操作的開銷往往不可預計。

然而這還遠遠不夠,上面的措施只能保證初始化階段獲取的虛擬記憶體有確切的物理記憶體的對映,而一個使用按需分頁的實時系統需要提供一套機制,保證應用程式中所有的虛擬記憶體都有完整的物理記憶體對映。

換句話說,實時系統需要保證程式執行起來之後,不會重新將虛擬地址映射回檔案系統,這種機制就叫記憶體鎖定。

Linux提供了對應的系統呼叫

mlockall()

,一個實時應用應該在剛啟動程序或者執行緒的時候就呼叫

mlockall()

,同時要配合

MCL_CURRENT|MCL_FUTURE

引數,這兩個引數保證當前和未來分配到當前程序(或執行緒)的頁都會鎖定到實際的物理記憶體。

注意,即使系統被配置成不支援swap,也是需要呼叫這個函式的,因為Linux仍然會把可執行檔案映象swap到檔案系統。

在2。6。9核心之前,只有超級許可權的程序可以鎖定記憶體,而較新的核心版本支援普通許可權的程序可以鎖定一定數量的記憶體,支援的大小由宏RLIMIT_MEMLOCK來約定,預設值是64K位元組。

儘管如此,mlockall()也只能鎖定由Linux分配的頁,而最開始那些對映到只讀的0頁的虛擬記憶體,仍舊會第一次寫需求的時候產生缺頁中斷,此時Linux的系統開銷就會由頁分配和記憶體鎖定同時影響。

Linux不提供用來關閉按需分頁的核心引數或者選項。與此同時,實時程式必須做到以下三點,來保證在任務真正開始前,獲取到的記憶體都是可用的:

應用在堆上分配記憶體時,必須向請求的記憶體中的每一頁都主動進行一次寫操作

應用必須包含一個函式,這個函式呼叫時需要把整個任務週期內需要用到的最大的棧空間給用滿

應用程式需要充分使用核心提供給使用者空間的buffer,比如socket通訊用到的buffer

(PS:說實話這三點我沒有理解的特別深刻,因為我沒有考慮過也沒有見過其他人做過這種操作,所以我懷疑自己可能翻的有誤,有疑問的建議看原文)

2、過需分配記憶體(overcommit memory)

透過記憶體對映技術,

Linux可以承諾給應用程式分配比實際作業系統能提供的更多的虛擬記憶體,這個功能就叫做過需分配記憶體

。因為作業系統假設所有的應用程式不會同時用盡他們所請求的所有記憶體。但是這樣一來,過需分配記憶體實際上對於實時程式是有負面影響的,這增加了判斷記憶體是否過度分配的難度。這樣一來,當記憶體第一次超額申請的時候,申請的結果仍然能成功返回。

預設配置下,Linux允許分配的虛擬記憶體大小等於

swap分割槽的大小加上1.5倍的物理記憶體大小。

當一個記憶體寫操作發生時,如果此時系統的虛擬記憶體用完了,才會出現記憶體過度分配的現象。一旦這種情況出現,那麼核心就會開始kill程序,當然這個程序不會是正在執行寫操作的程序。因此這種操作通常不會發生在root許可權下的程序,更多會發生在其他程序。

此外,在檢驗這個狀態時,需要重新檢驗一下所有被應用程式分配的記憶體。這樣一來會導致系統會低估記憶體的利用率。系統預估應用的記憶體使用率時,只計算實際分配的頁,不會考慮稍後延遲分配的頁,那麼最高的記憶體使用率就很明顯只會出現在壓力測試的時候。

幸運的是,提供了一個引數overcommit_memory,可以選擇關閉過需分配記憶體,

在實時應用中這個選項是需要關閉的。

3、系統呼叫和缺頁異常

有些系統呼叫會執行記憶體分配,這會導致,在實時應用中,這些呼叫是必須避免的。很不幸,linux沒有一份完整的清單來列舉所有會造成缺頁中斷的系統呼叫,函式的使用手冊也多半不會提及。只能從表面來推測一個系統呼叫是否會造成缺頁中斷,比如透過它執行的功能,或者使用的引數項。最常見的一個例子就是基於IP通訊的相關呼叫。

因此,實時應用應該避免使用IP通訊,或者在初始化階段去執行通訊功能。

定時器

1、軟體時鐘和高效能時鐘

Linux定義了一個軟體時鐘來管理系統事件,例如排程,休眠,超時以及測量CPU時間。從2。6。20開始,這個軟時鐘的週期支援100hz(也就是10ms),250hz,300hz,1000hz。在X86或者PowerPC上,這個週期是1ms。所以任何和時間定時相關的系統呼叫,時間粒度都不能超過這個週期。

然而,從2。6。1開始,Linux加入了高效能定時器的支援,它支援納秒級別的精度,但是實際的效能還是依賴於硬體。可以透過核心引數CONFIG_HIGH_RES_TIMERS來使能高效能定時器,當開啟時,核心就使用高效能定時器來管理大部分事件。並且像select、poll、epoll、nanosleep、futex等這種系統呼叫預設就是使用高效能定時器的。他們的精度都遠高於1ms。

2、定時器懶惰(timer slack)

引入高效能定時器會更頻繁的喚醒CPU,增加系統開銷,增加功耗。為了平衡這點,2。6。22之後,引入了timer slack機制,slack表示了有一小段時間,預設是50us,每個執行緒可單獨設定。當執行緒中有個定時器任務時,核心檢查定時器是否已經在的範圍內結束,如果是,就會在設定時間之後一小段時間內執行定時器事件,因此,timer slack會增加時鐘週期的計數次數。

不過實時執行緒的排程策略是不支援timer slack的,只有普通執行緒支援。儘管如此它還是會影響普通執行緒的非同步性,如果有地方因為等待普通執行緒的非同步資源而受到阻塞,那麼應考慮改用實時執行緒。

程序排程和CPU隔離

對應Linux 排程器來說,程序和執行緒是一樣的,所以下面統一以來描述所有的程序和執行緒。

1、排程策略和優先順序

Linux排程策略分實時策略和普通策略:

1。1、實時策略

SHCED_FIFO和SCHED_RR,3。14之後引入了SCHED_DEADLINE。前兩者使用靜態優先順序,1最小,99最大,而普通任務的靜態優先順序是0。所以實時任務的優先順序總是比普通任務高。SCHED_FIFO策略只有當任務主動結束或者遇到更高優先順序的任務搶佔時才會終止並讓出CPU,換句話說,SCHED_FIFO沒有時間片的概念。所以這個策略下一個任務可以永久佔用CPU(當然linux有機制避免這種情況傳送),哪怕有一個通優先順序的任務進來,也無法搶佔它。

SCHED_RR則是在SCHED_FIFO的基礎上加入了時間片的概念,每個任務只能在自己的時間片內佔用CPU,時間片用完後就會被同優先順序的任務搶佔。在3。9核心之前,預設的時間片大小固定是100ms,之後可以透過核心引數sched_rr_timeslice_ms來修改。並且,Linux還提供了nice值用來動態調整實時任務的時間片。動態調整範圍從+19(10ms)到-20(200ms)

SCHED_DEADLINE策略給在排程週期的範圍內,每個任務設定一個相對的deadline期限。然後排程器需要評估一個週期內所有任務可以執行的最長時間,然後執行時在每個週期,先執行最早到期限的任務。並且每次插入新任務的時候,也會執行一次檢查,看看有沒有任務需要排程。

所有使用策略的任務都被當成一個組,比其他排程策略的任務優先順序更高,以確保到deadline時任務可以被排程。

2。6。12之前,只有特定的任務可以執行實施策略,之後的版本中,普通的任務也可以使用SCHED_RR和SCHED_FIFO策略,但是優先順序的範圍不能超過RLIMIT_RTPRIO,SCHED_DEADLINE只支援特定的任務。

1。2、普通策略

有三種普通排程策略SCHED_OTHER、SCHED_BATCH和SCHED_IDLE

SCHED_IDLE策略只有在處理器空閒的時候才會執行,該策略下所有任務輪轉執行,無優先順序

SCHED_OTHER和SCHED_BATCH都受動態優先順序影響,動態優先順序是排程器根據任務的響應性和公平性來估算的,並且受nice值的影響。兩者的區別主要在於,SCHED_BATCH會被排程器預設為非互動式的任務,並且被認為是CPU密集型任務。

在大部分排程演算法下,

SCHED_OTHER

更受歡迎,因為大部分排程演算法是為互動型任務而設計的。

實時系統中的週期性任務和時間敏感性任務應該使用實時排程策略,下面介紹一下排程器的工作原理和注意細節

1。3、CFS排程器

2。6。23之後,普通優先順序的任務使用的預設排程器就是CFS排程器,設計目的是完全公平排程。簡單概括一下就是對於所有同nice值的任務,CFS排程器會把處理器的時間片均勻的分給這些任務。

CFS會取一個週期來分配時間片,這個週期被認為是

CFS的排程延時

,你也可以認為這是一個任務排程需要等待的最長時間。隨著核心版本的演變,CFS分配的預設時間片值也不斷的在改變。在3。2和3。14版本中,預設值是基於CPU數量的計算公式。當然,為了避免產生過多的上下文切換的開銷,CFS約定了一個最小執行時間,這段時間內任務是無法被搶佔的。這個值的計算公式是0。75ms*(1+log2(cpu數量))。

當然如果參與排程的任務超過了8個,那預設用來分割的週期也會增大。為了保證每個任務公平的享用處理器,CFS追蹤每個人任務的執行時間,並且用紅黑樹進行排序。

為了確保引入nice值並且保證公平性,CFS定義了一個。

nice值會影響一個任務的虛擬時間,nice值更小的任務(優先順序更高)的虛擬執行時間會增長的比實際執行時間慢。相反,nice值越大的任務虛擬執行時間增長的比實際執行時間更快。

儘管CFS讓每個任務公平的訪問處理器資源,但是CFS會隱式的提升那些經常阻塞休眠的任務的優先順序,用一個現象來描述就是一個I/O密集型任務在喚醒時會搶佔一個CPU密集型任務。

1。4、非同步任務應該用普通還是實時策略?

非同步任務使用實時策略有以下優勢:

使用實時策略可以增加任務執行的可重複性(我的理解是週期的穩定性)

排程效率比CFS高

實時策略不傾向於公平分配CPU,所以它更青睞CPU密集型任務

實時策略允許使用者選擇時間片排程和非時間片排程,CFS必須依賴時間片

實時策略可以更好的控制任務的執行順序

實時任務不受影響,定時精度也更高

當然開發者也要注意不要過多的使用實時任務,詳細在下面

實時頻寬限制

會介紹。

2、排程事件

通常情況下Linux核心會在下列情況發生排程或者打斷:

時鐘計數到期,X86下通常是1ms

排程器呼叫的高效能定時器到期。如果排程器發現任務即將耗盡自己的時間片,那麼會在時間片耗盡時用高效能定時器來觸發排程

任務結束,阻塞,休眠或者掛起CPU,此時任務會被移出排程器的執行佇列

任務被喚醒或者插入新任務時

只有在上述事件發生時,任務才可以被搶佔或者切換到執行狀態,這些事件也可以打算當前執行的任務。並且,排程器也會週期性的打算執行任務,

核心執行緒訪問時鐘計數器的行為大概會消耗CPU1%的效能

3。10核心以後,核心提供了一個選項,可以關閉只執行一個任務的CPU核心的時鐘計數,這個宏是CONFIG_NO_HZ_FULL,

這個選項可以減少時鐘週期性打算計算密集型或者實時任務,提升單個處理器上任務的效能

當然開啟這個選項也有一些限制:

在啟動的cpu上是無法被禁用的,通常是CPU0

只有當cpu的執行佇列上只有0或者1個任務時,核心才會關閉時鐘計數

這個選項並不是完全關閉時鐘計數,而是設定成1HZ,這樣是為了維持排程器的正常工作

其他細節可以見核心文件《NO_HZ: Reducing Scheduling-Clock Ticks》

3、排程延遲

這裡討論的排程延遲主要是指高優先順序任務喚醒時和任務開始執行時的開銷。對於Linux核心來說,排程延遲主要受硬體影響。現代ARM處理器上評估的最大任務切換的排程延遲在75us左右。OSADL日常檢測的資料顯示,這個開銷在10us~100us左右,支援系統管理模式的硬體這個開銷會增大到1ms。所以實時應用的開發者必須測量他們目標硬體上的排程延遲,以此來決定任務的規劃策略。排程延遲帶來的影響會左右開發者決定實時任務在結束一個週期任務時是使用休眠還是忙等待。

4、休眠還是忙等待?

任務休眠時會掛起CPU,交給其他執行任務,此時當任務喚醒時,就會受到排程延遲的影響。

當然如果處理器上只有一個任務執行的話,這種延遲是可以忽略不計的。

當任務休眠的時間小於排程延遲時,忙等待的效能會比使用nanosleep()要更好。

此外,如果休眠時間較長但是又必須將喚醒的延遲壓縮到最小,那麼任務可以組合使用sleep和忙等待。任務可以在休眠時間的基礎上,減去排程延遲的時間,然後當休眠醒來時,再呼叫忙等待到目標時間。在這種情況下,如果距離下一次執行的時間大於兩倍的排程延遲,那麼應該只執行sleep。否則,受到排程延遲的影響,在此期間其他在此休眠期間會被排程的任務,可能在再次被搶佔之前什麼工作也做不了。

ps:斜體子這段翻譯的感覺不是很好,建議對照原文看

然而,用這種忙等待的方式強制佔用CPU也不是個好辦法,受系統版本,硬體驅動或者其他應用程式的影響,這種強制佔用CPU的忙等待會引起不穩定或者異常。所以核心提供了來避免這種強制佔用CPU的問題

5、實時頻寬限制

預設狀態下,Linux核心會將所有實時任務歸為一個組,並且限制它們的CPU使用率,這個值預設是95%,每1秒執行一次。

核心引數sched_rt_period_us和sched_rt_runtime_us可以配置這個實時頻寬。sched_rt_period_us設定的是核心執行這種限制的週期(預設1000000us),sched_rt_runtime_us設定的是一個執行週期之內,實時任務的時間頻寬上限(預設950000us)。上述引數是作用於所有CPU的。所以在預設情況下,實時任務會發現它們每1秒會被cpu掛起50us。

因此,一個硬實時系統應該關閉這個配置,將

sched_rt_runtime_us

設定成。如果實際執行時出現了不穩定或者異常情況,那麼開發者應該確保實時執行緒會及時的釋放cpu,以確保實際的cpu佔用率不會超過95%。

6、控制組和實時組排程

控制組是核心提供的一個功能,讓超級管理員可以限制某一組任務的資源訪問率

。上面提到的實時頻寬限制和非同步任務應該用普通還是實時策略就描述了這種情況,這些機制確保了實時排程組的程式不會使其他普通程序餓死。

7、核心軟中斷

軟中斷主要是為驅動程式的下半部設計的,不過核心也會用它來推遲一些高優先順序的任務。由於設計上的原因,

軟中斷必須和觸發它的硬體中斷執行在同一個CPU上,並且,軟中斷是不允許休眠的

核心維護了一個容納32個軟中斷的陣列,

軟中斷號在編譯核心時就確定好了,並且是連續的

。每個軟中斷在註冊的時候會對應一個函式入口和引數指標。

雖然軟中斷在編譯時就規劃好了,但是軟中斷的註冊卻是在執行時

。通常情況下會預留兩個軟中斷號給tasklet,tasklet機制可以讓第三方驅動在不需要單獨繫結軟中斷號的情況下,可以執行它們的中斷下半部程式。

核心觸發軟中斷執行的情況包括:

中斷服務返回

某些核心子系統,比如網路系統,呼叫了下半部操作

ksoftirqd執行緒被排程執行

當軟中斷被觸發時,不單單是相關的處理工作會被延遲,而是所有掛起的軟中斷都會被觸發。舉個例子,如果串列埠硬體中斷觸發,那麼在它返回時,可能同時會觸發串列埠驅動程式的下半部和網路堆疊中的某些延時任務

軟中斷使得實時任務被核心打斷的時長變得難以預估

在不可搶佔的核心中,軟中斷的執行有單獨的優先順序,軟中斷只能被硬體中斷服務程式打斷。為了確保每個硬體中斷都能被軟中斷下半部處理程式接收到,核心對每個硬體中斷進行了計數。設想一下,高頻的硬體中斷就會使得CPU的佔用率很高。為了避免這種情況,核心限制了高優先順序下軟中斷呼叫次數,核心會將未執行的軟中斷處理任務移入核心執行緒ksoftirqd,這個執行緒每個cpu都有一個,排程策略是SCHED_OTHER,nice值是19。

而在支援搶佔的核心中,處理方式是不一樣的。

2。6。x核心中,CONFIG_PREEMPT_RT補丁下,會在每個CPU上,為每個軟中斷入口建立一個軟中斷執行緒,這些執行緒的排程策略是SCHED_FIFO,優先順序根據不同補丁的版本會有區別。通常是1,24或者50。然而,這種執行緒的激增早期被認為是效能問題。從3。6版本開始,每個軟中斷入口被放到了軟中斷最近一次觸發的上下文中。

因此,一個使用者態的實時程式,如果觸發了軟中斷(比如呼叫了網路子系統的功能),就會進入到上下文中去處理軟中斷,或者被其他觸發了相同軟中斷的任務打斷。

並且,

當不同優先順序的任務觸發相同的軟中斷時,軟中斷就會被修改,防止優先順序反轉的發生

8、核心排程策略和優先順序

任何使用者態的實時任務,即使綁定了cpu親和性,也會和其他核心任務共享cpu。

並且,雖然CONFIG_PREEMPT_RT保證了linux的實時性,但是畢竟Linux不是按照實時性設計的,所以裝置驅動不能以實時任務的思路來寫

因此,一些系統配置經常將實時核心任務保持在一個比較高的優先順序,當有使用者實時任務的優先順序等於或大於這些實時核心任務時,他們的排程行為也是類似的。換句話說,

任何優先順序大於49的使用者實時任務都應該確保自己佔用CPU的比例在一個很小的比例

(比如I/O密集型任務或者休眠比較多的任務)

然而這些建議往往只是理想狀態。實際在航天領域中,使用的實時程式往往是CPU密集型並且長時間佔用的任務,這些任務中有對時間要求非常高的部分,甚至它們的優先順序應該高於包括核心在內的所有任務。在這種情況下,開發者可以用配置了上限的鎖來提升它的優先順序,確保時間敏感區域不會被搶佔。

當然,

任何優先順序為99(最高)的時間敏感部分的任務,都應該避免或者謹慎設計迴圈、遞迴或者其他在發生錯誤或者中斷時可能會無限執行的邏輯,因為這會導致時間敏感區的任務阻塞其他可以恢復處理的任務

將使用者實時任務的優先順序設定成高於軟中斷執行緒或許是“安全”的。在主線核心版本中,高負載會導致軟中斷經歷長延時,直到CPU休眠。這種“安全”也只意味著邏輯上的安全,這種長時間的延遲時間仍然會導致資料丟失。

此外,如果應用程式依賴於軟中斷來完成某些功能,那一旦軟中斷被阻塞,那麼程式就會出問題,透過乙太網進行大資料塊的同步傳輸就是這樣一個例子。

在這種情況下,如果資料塊超過了上下文傳輸到NIC的限制,那麼只有一部分資料會在上下文中傳輸,核心將會觸發

來傳輸剩餘的資料。和其他所有的軟中斷一樣,被觸發的軟中斷僅限於在提升它的處理器上執行,這樣一來,軟中斷和應用程式就會出現爭奪處理器的情況。

在2。6和早期3。x核心版本中,如果應用程式的優先順序高於軟中斷執行緒,那麼軟中斷不會執行,直到應用程式阻塞、休眠或者呼叫了其他會觸發軟中斷的系統呼叫。

事實上,如果應用程式的優先順序高於軟中斷,那麼待處理的軟中斷就會不斷積累;相反,而過應用程式在軟中斷處理完成後就執行,那麼增加應用程式的抖動。在上述的例子中,為了保證資料傳輸,可以將軟中斷的優先順序設定成高於應用程式。

然而,早期的3。x版本的CONFIG_PREEMPT_RT補丁中,NET_TX_SOFTIRQ的優先順序並不能單獨設定,並且由ksoftirqd執行的軟中斷執行緒的優先順序都會高於應用程式。這就使得如果系統的中斷觸發過頻繁的話,應用程式就會受到嚴重的影響。

在3。6版本之後,大部分的軟中斷處理都是在觸發軟中斷的執行緒上下文中完成,這使得平衡應用程式和NET_TX_SOFTIRQ的優先順序變得更加困難,這需要程式設計師能夠識別所有能夠觸發NET_TX_SOFTIRQ的執行緒。

此外當系統遇到大量的外部中斷時,核心可能會將所有軟中斷從IRQ執行緒移入ksoftirqd任務。因此,如果應用程式為了保證NET_TX_SOFTIRQ的優先順序而將優先順序設定的比ksoftirqd低,那依然會遭受不可預估的延遲。應用程式需要允許ksoftirqd執行緒臨時提高自己的優先順序高於應用程式。一個較為簡單的辦法就是在執行同步寫操作之後,讓應用程式短暫的休眠一段時間。這樣就能讓核心去執行的任務以及NET_TX_SOFTIRQ下掛起的任務。

9、防止優先順序反轉

CONFIG_PREEMPT_RT補丁中替換了核心中很多鎖的實現,替換後的鎖實現了

優先順序繼承

以防止

優先順序反轉

。通常情況下優先順序反轉不會有什麼問題,在使用者程式中,核心提供了一個支援優先順序繼承的POSIX執行緒互斥鎖作為可選項,使用這個鎖可以確保應用程式中資源競爭和程式碼執行順序不會受到優先順序反轉的影響。

10、CPU隔離

Linux提供了兩種CPU隔離性的設定方式。

第一種是使用啟動引數isocpus,這個引數為任務設定了親和性掩碼,這樣排程器和負載均衡器就不會將任務分配給標識的CPU。

第二種是使用cpuset,這個機制可以將一組控制組的或者一系列程序Id繫結在某幾個特定的CPU上(設定親和性)。cpuset有副作用,最明顯的就是,

在某些情況下,在建立

cpuset

之前,cpu上已經存在的程序會一直保留在cpu上執行,這其中包括不斷在觸發的軟中斷以及預先設定的定時器.另外,boot之後設定的

cpuset

,在將程序從排斥的CPU遷移到親和的CPU上時也會有一定的延遲。

當然也有優點,可以手動修改負載平衡的效能,並且對每個cpu的程序列表進行修改。

isocpus和cpuset不是互斥的,一個cpuset可以包括所有被孤立的cpu。使用者態的程式只能透過修改親和性掩碼或者設定cpuset來將任務繫結到獨立的cpu上。不過在這種情況下,

一旦一個程序被分配到一個孤立的CPU,它就不能再遷移到其他CPU了

然而這兩種機制只能保證獨立的CPU上沒有其他應用程式,但是

核心可以選擇性忽視這兩個機制

,尤其是下列的場景,核心是一定會忽視的:

時鐘計數器、排程器、核間中斷、全域性工作佇列、中斷向量入口、軟中斷、per-CPU執行緒

因此,新版本的核心中,減少了一些中斷,下面列舉一些核心中斷源:

11、時鐘計數器和排程器

這部分在上面排程事件章節已經詳細介紹過。這裡再補充一些。當計數器被限制在1hz時,排程器仍然是可以用高效能定時器來確保在某個時刻喚醒任務的。正在執行的任務阻塞、休眠、終止、喚醒都會觸發排程器執行,這些中斷時無法避免的。但是可以透過只為CPU分配一個SCHED_FIFO的任務來減少核心中斷的產生。

12、Per-CPU執行緒

在多核系統中,核心會建立系列Per-CPU執行緒,來負責每個CPU上的任務管理以及負載均衡。Per-CPU執行緒可以是實時或者是普通任務。普通策略的任務不會打斷實時策略下的使用者應用。

然而,很多Per-CPU執行緒往往具有最高實時優先順序,所以強烈建議不要講實時應用程式設定成最高優先順序,因為這些最高優先順序的核心任務時很關鍵的。

因此,每個per-CPU執行緒偶爾出現中斷的情況是無法避免的。並且為了保證核心的穩定性,實時應用程式應該儘可能保證一個較低的CPU使用率,以確保per-CPU執行緒中較低優先順序的執行緒有機會得以執行。

13、中斷執行緒和軟中斷

單獨的處理器核是否有許可權處理中斷是由底層硬體決定的。某些平臺上,只有CPU0有許可權處理中斷。在現代Intel平臺上,APIC可以允許核心設定指定的CPU來處理中斷。其他平臺可能會是兩個方案的折中,例如透過韌體來配置不同的中斷給不同的CPU

Intel平臺上,linux透過APIC來實現中斷的負載均衡,這和排程器的負載均衡是不同的。只不過Linux實現中斷的負載均衡也是透過設定每個中斷的CPU親和性來實現的。所以

中斷的負載均衡是不違背CPU隔離性的

14、核間中斷和全域性工作佇列

核心使用核間中斷和工作佇列來管理一些任務,特別是記憶體,它保證了多處理器之間資源的一致性。這部分的開銷無法避免,也無法配置。

15、是否需要為作業系統保留CPU0

對於Linux實時系統,常見的做法會避免應用程式在CPU0上執行,並且保留給作業系統。這種建議有兩種原因導致,一是由於歷史性的原因,很多舊的平臺是由CPU0來負責處理中斷的。二是有一種猜測,當CPU0執行核心時,如果應用程式也執行在CPU0上,那延遲會更高。

然而,基於CONFIG_PREEMPT_RT補丁的測試已經論證了,即使所有實時程式都執行在CPU0上,延遲方面也並沒有顯著的區別。當然開發者仍然可以選擇將所有的中斷放在CPU0上,並將實時程式繫結在其他CPU上,以最大幅度的減少這些中斷。

並且,為核心保留CPU0仍然能夠提供一些保障措施,防止核心一些低優先順序的活動受到實時任務的影響。

所以綜上所述:

為核心保留CPU0仍然是個比較好的建議,除非其他核上的佔用率都達到了預計的最大值。儘管如此,CPU0上的使用率也必須限制在70%以下(假設核心的佔用率在30%

)。

16、CPU親和性,排程策略和排程優先順序

用fork()建立的子程序繼承了父程序的親和性、排程策略和優先順序,並且這一點呼叫函式也無法干預,需要留給被建立的子程序自己去修改。Linux POSIX提供的執行緒操作也會繼承這些特性,但是排程策略和優先順序可以在呼叫pthread_create()時透過引數傳遞來修改,親和力則仍然需要交給子執行緒自己去修改。

總結

Linux雖然有實時補丁,但是本質上還是一個非實時系統,無法完美滿足硬實時的需求,更多的只是在效能和延遲上的權衡。

當然開發者們需要了解哪些條件可能會對系統的實時性產生負面的影響,這樣才能做出針對性的最佳化。下面就是總結出來的幾點建議:

使用可搶佔核心,可以使用CONFIG_PREEMPT核心選項或者CONFIG_PREEMPT_RT補丁。

為了減少分配頁時的開銷,應用程式在初始化階段應該對每個在堆記憶體上請求的頁進行一次寫操作,執行一次可以用滿最大棧空間的函式,並執行依賴核心快取區的系統呼叫。

關閉過需分配記憶體。

時間敏感的任務使用實時排程策略。對於沒有特殊許可權的實時程式可以修改RLIMIT_RTPRIO來提升實時效能。

平衡實時任務和核心任務的優先順序,包括中斷和軟中斷

關閉實時頻寬限制,將sched_rt_runtime_us設定成-1

實時任務在空閒的時候儘量使用休眠而不是忙等待,如果要把開銷降到最低,可以使用休眠+短暫的忙等待。

isocpus和cpuset可以設定CPU親和性。

在硬體平臺支援的情況下,均衡配置中斷負載,將中斷處理的CPU和實時任務繫結的CPU儘量隔離開,或者選擇性將中斷關聯到對應裝置處理任務的處理器上。

禁用單個實時任務處理器上的始終計數器,減少週期性排程的中斷。

注意子任務繼承的排程屬性。

關注當前核心版本的軟中斷機制,當實時任務用到依賴軟中斷的裝置驅動或者服務時,注意實現方式對效能的影響。

謝謝關注,下期更精彩。

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

-- END --