Linux實時調(diào)度實戰(zhàn):優(yōu)化你的應用性能
在如今數(shù)字化浪潮洶涌的時代,無論是工業(yè)自動化生產(chǎn)線的精準操控,還是多媒體應用中流暢的音視頻播放體驗,又或是醫(yī)療設備對生命體征的實時監(jiān)測,背后都離不開一個關鍵因素 —— 系統(tǒng)的實時響應能力。而 Linux 系統(tǒng),作為開源世界的中流砥柱,在眾多領域廣泛應用。但默認情況下,Linux 的進程調(diào)度主要側(cè)重于公平性,對于那些對時間極為敏感的實時任務而言,原生的調(diào)度機制顯得有些力不從心。
那么,如何才能挖掘 Linux 系統(tǒng)的潛力,讓它在實時場景中大放異彩,顯著優(yōu)化應用性能呢?今天,就帶大家走進 Linux 實時調(diào)度的實戰(zhàn)世界,一同探索其中的奧秘,掌握讓應用性能飛躍的實用技巧 。
一、Linux調(diào)度概述
在日常生活中,我們常常會遇到排隊的場景。比如在超市結賬時,顧客們會在收銀臺前依次排隊等待結賬。在這個過程中,收銀員會按照排隊的順序為顧客服務,這就是一種簡單的調(diào)度方式。而在 Linux 操作系統(tǒng)中,也存在著類似的調(diào)度機制,它負責管理系統(tǒng)中的進程,決定哪個進程可以獲得 CPU 資源并運行。
進程是計算機中的程序關于某數(shù)據(jù)集合上的一次運行活動,是系統(tǒng)進行資源分配和調(diào)度的基本單位。在單處理器系統(tǒng)中,同一時刻只有一個進程能夠在 CPU 上運行,就像只有一個收銀員為顧客服務一樣。而在多處理器系統(tǒng)中,雖然可以有多個進程同時在不同的 CPU 上運行,但總體上仍然需要對進程進行合理的調(diào)度,以確保系統(tǒng)的高效運行。
Linux 系統(tǒng)中的進程可以分為不同的類型,其中實時進程對時間要求較高,它們需要在規(guī)定的時間內(nèi)完成任務。實時進程又可以進一步分為硬實時進程和軟實時進程。硬實時進程必須在絕對的時間窗口內(nèi)完成任務,否則可能會導致系統(tǒng)失效或災難性后果,比如航空航天控制、醫(yī)療設備等領域的任務。軟實時進程雖然也追求在規(guī)定時間內(nèi)完成任務,但偶爾的超時通常不會導致系統(tǒng)完全失效,只會影響系統(tǒng)的服務質(zhì)量或用戶體驗,像多媒體處理、網(wǎng)絡通信等場景中的任務。除了實時進程,還有普通進程,它們對時間的要求相對較低,在系統(tǒng)資源分配中處于相對次要的地位。
為了實現(xiàn)對進程的有效調(diào)度,Linux 系統(tǒng)采用了多種調(diào)度算法。其中,時間片輪轉(zhuǎn)調(diào)度算法是一種常見的調(diào)度方式。它將 CPU 的時間劃分為一個個固定長度的時間片,每個進程輪流獲得一個時間片來運行。當一個進程的時間片用完后,即使它還沒有完成任務,也會被暫停,然后被放入就緒隊列的末尾,等待下一輪調(diào)度。這種調(diào)度方式就像是超市里的顧客們輪流在收銀臺結賬,每個人都有機會得到服務,從而保證了系統(tǒng)的公平性和響應性。
實時調(diào)度器主要為了解決以下四種情況:
- 在喚醒任務時,待喚醒的任務放置到哪個運行隊列最合適(這里稱為pre-balance);
- 新喚醒任務的優(yōu)先級比某個運行隊列的當前任務更低時,怎么處理這個更低優(yōu)先級任務;
- 新喚醒任務的優(yōu)先級比同一運行隊列的某個任務更高時,并且搶占了該低優(yōu)先級任務,該低優(yōu)先級任務怎么處理?
- 當某個任務降低自身優(yōu)先級,導致原來更低優(yōu)先級任務相比之下具有更高優(yōu)先級,這種情況怎么處理。
對于情況2和情況3,實時調(diào)度器采用push操作。push操作從根域中所有運行隊列中挑選一個運行隊列(一個cpu對應一個運行隊列),該運行隊列的優(yōu)先級比待push任務的優(yōu)先級更低。運行隊列的優(yōu)先級是指該運行隊列上所有任務的最高優(yōu)先級。
對于情況4,實時調(diào)度器采用pull操作。當某個運行隊列上準備調(diào)度時,候選任務比當前任務的優(yōu)先級更低時,實時調(diào)度器檢查其他運行隊列,確定是否可以pull更高優(yōu)先級任務到本運行隊列。還有,當某個運行隊列上發(fā)生調(diào)度時,該運行隊列上沒有任務比當前任務優(yōu)先級高,實時調(diào)度器執(zhí)行pull操作,從其他運行隊列中pull更高優(yōu)先級任務到本運行隊列。
每CPU變量運行隊列rq,包含一個rt_rq數(shù)據(jù)結構。rt_rq結構體主要內(nèi)容如下:
struct rt_rq {
struct rt_prio_array active;
...
unsigned long rt_nr_running; // 可運行實時任務個數(shù)
unsigned long rt_nr_migratory; // 該運行隊列上可以遷移到其他運行隊列的實時任務個數(shù)
unsigned long rt_nr_uninterruptible;
int highest_prio;
int overloaded;
};
實時任務優(yōu)先級范圍為0到99。這些實時任務組織成優(yōu)先級索引數(shù)組active,該優(yōu)先級數(shù)組的數(shù)據(jù)結構類型為rt_prio_arry。rt_prio_arry數(shù)據(jù)結構由兩部分組成,一部分是位圖,另一部分是數(shù)組。
struct rt_prio_arry {
unsigned long bitmap[BITS_TO_LONGS(MAX_RT_PRIO+1)];
struct list_head queue[MAX_RT_PRIO];
}
二、實時調(diào)度策略
Linux內(nèi)核中提供了兩種實時調(diào)度策略:SCHED_FIFO和SCHED_RR,其中RR是帶有時間片的FIFO。這兩種調(diào)度算法實現(xiàn)的都是靜態(tài)優(yōu)先級。內(nèi)核不為實時進程計算動態(tài)優(yōu)先級。這能保證給定優(yōu)先級別的實時進程總能搶占優(yōu)先級比他低得進程。linux的實時調(diào)度算法提供了一種軟實時工作方式。
實時優(yōu)先級范圍從0到MAX_RT_PRIO減一。默認情況下,MAX_RT_PRIO為100(定義在include/linux/sched.h中),所以默認的實時優(yōu)先級范圍是從0到99。SCHED_NORMAL級進程的nice值共享了這個取值空間,它的取值范圍是從MAX_RT_PRIO到MAX_RT_PRIO+40。也就是說,在默認情況下,nice值從-20到19直接對應的是從100到139的優(yōu)先級范圍,這就是普通進程的靜態(tài)優(yōu)先級范圍。在實時調(diào)度策略下。schedule()函數(shù)的運行會關聯(lián)到實時調(diào)度類rt_sched_class。
2.1SCHED_FIFO:獨占 CPU 的 “霸王龍”
在 Linux 的實時調(diào)度策略中,SCHED_FIFO 就像是恐龍時代的霸王龍,霸氣十足。它采用先進先出(FIFO)的調(diào)度方式,這種方式非常直接,沒有復雜的時間片輪轉(zhuǎn)機制。一旦一個進程被調(diào)度,只要沒有更高優(yōu)先級的進程出現(xiàn),它就會一直霸占著 CPU 運行下去,運行時長不受任何限制。這就好比在一場比賽中,只要沒有更強的對手出現(xiàn),當前的冠軍就可以一直保持領先,不會被替換下場。
以音頻處理場景為例,在實時音頻錄制和播放中,就經(jīng)常會用到 SCHED_FIFO 策略。在錄制音頻時,需要保證音頻數(shù)據(jù)的連續(xù)性和及時性,不能有絲毫的延遲或中斷。如果采用 SCHED_FIFO 策略,音頻錄制進程一旦獲得 CPU 資源,就會持續(xù)運行,將麥克風采集到的音頻數(shù)據(jù)及時地寫入存儲設備。在播放音頻時,音頻播放進程也會獨占 CPU,按照順序?qū)⒁纛l數(shù)據(jù)從存儲設備中讀取出來,并發(fā)送到音頻輸出設備進行播放。這樣可以確保音頻的流暢播放,不會出現(xiàn)卡頓或雜音的情況,為用戶帶來高品質(zhì)的音頻體驗。
SCHED_FIFO 策略的優(yōu)點顯而易見,它可以為那些對時間要求極為嚴格的實時進程提供穩(wěn)定且可預測的執(zhí)行時間,這對于一些需要精確控制時間的系統(tǒng)來說至關重要,比如工業(yè)控制系統(tǒng)、機器人控制等領域。在這些系統(tǒng)中,任務的執(zhí)行時間必須是可預測的,否則可能會導致嚴重的后果。
然而,SCHED_FIFO 策略也存在明顯的缺點。由于它沒有時間片的概念,一旦一個低優(yōu)先級的進程先獲得了 CPU 資源,并且一直不主動放棄,那么其他優(yōu)先級較低的進程就可能會一直處于饑餓狀態(tài),無法獲得 CPU 資源來執(zhí)行。這就好比一群人在排隊等待服務,但是排在前面的人一直占用著服務資源不離開,后面的人就只能一直等待,這顯然是不公平的。
2.2SCHED_RR:公平輪替的 “時間掌控者”
與 SCHED_FIFO 不同,SCHED_RR 像是一位公平的時間掌控者,采用時間片輪轉(zhuǎn)的調(diào)度機制。在這種策略下,每個進程都會被分配一個固定的時間片。當進程運行時,時間片會逐漸減少。一旦進程用完了自己的時間片,它就會被放入就緒隊列的末尾,同時釋放 CPU 資源,讓其他相同優(yōu)先級的進程有機會執(zhí)行。這就像一場接力比賽,每個選手都有固定的跑步時間,時間一到就把接力棒交給下一位選手,保證了每個選手都有公平的參與機會。
以動畫渲染場景為例,在制作動畫時,通常會有多個任務同時進行,比如模型渲染、材質(zhì)處理、光影計算等。這些任務可能具有相同的優(yōu)先級,需要合理地分配 CPU 資源。如果采用 SCHED_RR 策略,每個渲染任務都會被分配一個時間片。在自己的時間片內(nèi),任務可以充分利用 CPU 資源進行計算和處理。當時間片用完后,任務會暫停,將 CPU 資源讓給其他任務。這樣可以確保每個渲染任務都能得到及時的處理,不會因為某個任務長時間占用 CPU 而導致其他任務延遲,從而保證了動畫渲染的高效進行。
SCHED_RR 策略在保證實時性的同時,還兼顧了公平性。它通過時間片的輪轉(zhuǎn),讓每個進程都能在一定的時間內(nèi)獲得 CPU 資源,避免了低優(yōu)先級進程長時間得不到執(zhí)行的情況。這使得它在一些對響應時間要求較高,同時又需要保證公平性的實時進程中得到了廣泛應用,比如交互式應用程序、游戲等。在這些應用中,用戶希望能夠得到及時的響應,同時也不希望某個任務獨占 CPU 資源,導致其他操作變得遲緩。
三、實時調(diào)度類的數(shù)據(jù)結構詳解
3.1優(yōu)先級隊列rt_prio_array
在kernel/sched.c中,是一組鏈表,每個優(yōu)先級對應一個鏈表。還維護一個由101 bit組成的bitmap,其中實時進程優(yōu)先級為0-99,占100 bit,再加1 bit的定界符。當某個優(yōu)先級別上有進程被插入列表時,相應的比特位就被置位。 通常用sched_find_first_bit()函數(shù)查詢該bitmap,它返回當前被置位的最高優(yōu)先級的數(shù)組下標。由于使用位圖,查找一個任務來執(zhí)行所需要的時間并不依賴于活動任務的個數(shù),而是依賴于優(yōu)先級的數(shù)量??梢妼崟r調(diào)度是一個O(1)調(diào)度策略。
struct rt_prio_array {
DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* 包含1 bit的定界符 */
struct list_head queue[MAX_RT_PRIO];
};
這里用include/linux/types.h中的DECLARE_BITMAP宏來定義指定長度的位圖,用include/linux/list.h中的struct list_head來為100個優(yōu)先級定義各自的雙鏈表。在實時調(diào)度中,運行進程根據(jù)優(yōu)先級放到對應的隊列里面,對于相同的優(yōu)先級的進程后面來的進程放到同一優(yōu)先級隊列的隊尾。對于FIFO/RR調(diào)度,各自的進程需要設置相關的屬性。進程運行時,要根據(jù)task中的這些屬性判斷和設置,放棄cpu的時機(運行完或是時間片用完)。
3.2實時運行隊列rt_rq
在kernel/sched.c中,用于組織實時調(diào)度的相關信息。
struct rt_rq {
struct rt_prio_array active;
unsigned long rt_nr_running;
#if defined CONFIG_SMP || defined CONFIG_RT_GROUP_SCHED
struct {
int curr; /* 最高優(yōu)先級的實時任務 */
#ifdef CONFIG_SMP
int next; /* 下一個最高優(yōu)先級的任務 */
#endif
} highest_prio;
#endif
#ifdef CONFIG_SMP
unsigned long rt_nr_migratory;
unsigned long rt_nr_total;
int overloaded;
struct plist_head pushable_tasks;
#endif
int rt_throttled;
u64 rt_time;
u64 rt_runtime;
/* Nests inside the rq lock: */
spinlock_t rt_runtime_lock;
#ifdef CONFIG_RT_GROUP_SCHED
unsigned long rt_nr_boosted;
struct rq *rq;
struct list_head leaf_rt_rq_list;
struct task_group *tg;
struct sched_rt_entity *rt_se;
#endif
};
3.3實時調(diào)度實體 sched_rt_entity
在 Linux 內(nèi)核的實時調(diào)度機制中,sched_rt_entity結構體扮演著至關重要的角色,它就像是一個精心打造的 “任務名片”,記錄了實時進程參與調(diào)度所需的關鍵信息。該結構體定義于include/linux/sched.h頭文件中,其源碼如下:
struct sched_rt_entity {
struct list_head run_list; // 用于將“實時調(diào)度實體”加入到優(yōu)先級隊列中的
unsigned long timeout; // 用于設置調(diào)度超時時間
unsigned long watchdog_stamp; // 用于記錄jiffies的值
unsigned int time_slice; // 時間片
unsigned short on_rq;
unsigned short on_list;
struct sched_rt_entity *back; // 用于由上到下連接“實時調(diào)度實體”
#ifdef CONFIG_RT_GROUP_SCHED
struct sched_rt_entity *parent; // 指向父類“實時調(diào)度實體”
/* rq on which this entity is (to be) queued: */
struct rt_rq *rt_rq; // 表示“實時調(diào)度實體”所屬的“實時運行隊列”
/* rq "owned" by this entity/group: */
struct rt_rq *my_q; // 表示“實時調(diào)度實體”所擁有的“實時運行隊列”,用于管理“子任務”
#endif
} __randomize_layout;
run_list字段是一個雙向鏈表節(jié)點,它就像一根無形的線,將各個實時調(diào)度實體按照優(yōu)先級串聯(lián)起來,加入到優(yōu)先級隊列中,方便調(diào)度器快速定位和處理。當一個實時進程被創(chuàng)建或者狀態(tài)發(fā)生變化時,它的run_list就會被插入到相應的優(yōu)先級隊列中,等待調(diào)度器的調(diào)度。
timeout字段用于設置調(diào)度超時時間,這就像是給任務設定了一個 “鬧鐘”。當任務運行時間超過這個設定的超時時間時,調(diào)度器可能會對其進行特殊處理,比如將其從 CPU 上移除,重新調(diào)度其他任務,以確保系統(tǒng)的實時性和穩(wěn)定性。在一些對時間要求極高的實時系統(tǒng)中,如自動駕駛汽車的控制系統(tǒng),每個任務都必須在規(guī)定的時間內(nèi)完成,否則可能會導致嚴重的后果。timeout字段就可以保證這些任務不會因為長時間占用 CPU 而影響其他關鍵任務的執(zhí)行。
watchdog_stamp字段用于記錄jiffies的值,jiffies是 Linux 內(nèi)核中的一個全局變量,表示系統(tǒng)啟動以來的時鐘滴答數(shù)。通過記錄jiffies的值,watchdog_stamp可以為調(diào)度器提供時間參考,用于判斷任務的運行狀態(tài)和調(diào)度時機。比如,調(diào)度器可以根據(jù)watchdog_stamp和當前的jiffies值來計算任務的運行時間,從而決定是否需要對任務進行調(diào)度。
time_slice字段表示時間片,對于采用時間片輪轉(zhuǎn)調(diào)度策略(如SCHED_RR)的實時進程來說,這個字段尤為重要。它規(guī)定了每個進程在被調(diào)度后可以連續(xù)運行的時間長度。當進程的時間片用完后,調(diào)度器會將其從 CPU 上移除,并將其放入就緒隊列的末尾,等待下一輪調(diào)度。這就像一場接力比賽,每個選手都有固定的跑步時間,時間一到就把接力棒交給下一位選手,保證了每個選手都有公平的參與機會。在多媒體播放系統(tǒng)中,音頻和視頻的解碼任務通常采用SCHED_RR策略,通過合理設置time_slice,可以確保音頻和視頻的流暢播放,不會出現(xiàn)卡頓或延遲的情況。
在支持實時組調(diào)度(CONFIG_RT_GROUP_SCHED)的情況下,parent字段指向父類 “實時調(diào)度實體”,這就像是一個家族樹中的父子關系,通過這種關系,調(diào)度器可以更好地管理和調(diào)度整個任務組。rt_rq字段表示 “實時調(diào)度實體” 所屬的 “實時運行隊列”,而my_q字段則表示 “實時調(diào)度實體” 所擁有的 “實時運行隊列”,用于管理 “子任務”。這種層次化的結構設計,使得調(diào)度器能夠更加靈活地處理復雜的實時任務場景。
3.4實時就緒隊列 struct rt_rq
struct rt_rq結構體是 Linux 內(nèi)核實時調(diào)度的核心數(shù)據(jù)結構之一,它就像是一個高效的 “任務指揮官”,負責管理實時進程的運行隊列,在核心調(diào)度器管理活動進程中發(fā)揮著舉足輕重的作用。該結構體定義于kernel/sched/sched.h頭文件中,其源碼如下:
struct rt_rq {
struct rt_prio_array active; // 優(yōu)先級隊列
unsigned int rt_nr_running; // 在RT運行隊列中所有活動的任務數(shù)
unsigned int rr_nr_running;
#if defined CONFIG_SMP || defined CONFIG_RT_GROUP_SCHED
struct {
int curr; // 當前RT任務的最高優(yōu)先級
#ifdef CONFIG_SMP
int next; // 下一個要運行的RT任務的優(yōu)先級,如果兩個任務都有最高優(yōu)先級,則curr == next
#endif
} highest_prio;
#endif
#ifdef CONFIG_SMP
unsigned long rt_nr_migratory; // 任務沒有綁定在某個CPU上時,這個值會增減,用于任務遷移
unsigned long rt_nr_total; // 用于overload檢查
int overloaded; // RT運行隊列過載,則將任務推送到其他CPU
struct plist_head pushable_tasks; // 優(yōu)先級列表,用于推送過載任務
#endif /* CONFIG_SMP */
int rt_queued; // 表示RT運行隊列已經(jīng)加入rq隊列
int rt_throttled; // 用于限流操作
u64 rt_time; // 累加的運行時,超出了本地rt_runtime時,則進行限制
u64 rt_runtime; // 分配給本地池的運行時
/* Nests inside the rq lock: */
raw_spinlock_t rt_runtime_lock;
#ifdef CONFIG_RT_GROUP_SCHED
unsigned long rt_nr_boosted; // 用于優(yōu)先級翻轉(zhuǎn)問題解決
struct rq *rq; // 指向運行隊列
struct task_group *tg; // 指向任務組
#endif
};
active字段是一個rt_prio_array類型的優(yōu)先級隊列,它維護了 100 個優(yōu)先級的隊列(鏈表),優(yōu)先級范圍從 0 到 99,從高到低排列。同時,它還定義了位圖,用于快速查詢。這就像是一個多層的貨架,每個貨架層對應一個優(yōu)先級,實時進程根據(jù)其優(yōu)先級被放置在相應的貨架層上。調(diào)度器可以通過位圖快速找到最高優(yōu)先級的隊列,從而選擇優(yōu)先級最高的進程進行調(diào)度,大大提高了調(diào)度效率。在航空航天控制系統(tǒng)中,各種實時任務的優(yōu)先級劃分非常嚴格,通過active優(yōu)先級隊列,調(diào)度器能夠快速響應高優(yōu)先級任務,確保系統(tǒng)的安全和穩(wěn)定運行。
rt_nr_running字段表示在 “實時運行隊列” 中所有活動的任務數(shù),這個數(shù)字就像是一個實時監(jiān)控的計數(shù)器,調(diào)度器可以根據(jù)它來了解當前實時運行隊列中的任務負載情況。如果任務數(shù)過多,調(diào)度器可能會采取一些措施,如任務遷移、限流等,以保證系統(tǒng)的正常運行。
在支持對稱多處理(CONFIG_SMP)或?qū)崟r組調(diào)度(CONFIG_RT_GROUP_SCHED)的情況下,highest_prio結構體中的curr字段表示當前 RT 任務的最高優(yōu)先級,next字段表示下一個要運行的 RT 任務的優(yōu)先級。如果兩個任務都有最高優(yōu)先級,則curr和next字段值相等。這些字段就像是調(diào)度器的 “指南針”,幫助調(diào)度器在眾多任務中準確地選擇下一個要運行的任務。
rt_nr_migratory字段用于記錄任務沒有綁定在某個 CPU 上時,這個值會增減,用于任務遷移。在多處理器系統(tǒng)中,當某個 CPU 的負載過高時,調(diào)度器可以根據(jù)這個字段的值,將一些可遷移的任務遷移到其他 CPU 上,以實現(xiàn)負載均衡。rt_nr_total字段用于overload檢查,當rt_nr_total超過一定閾值時,說明系統(tǒng)可能處于過載狀態(tài),調(diào)度器會采取相應的措施,如將任務推送到其他 CPU,以緩解系統(tǒng)壓力。overloaded字段表示 RT 運行隊列過載,當該字段為真時,調(diào)度器會將任務推送到其他 CPU,以保證系統(tǒng)的正常運行。pushable_tasks字段是一個優(yōu)先級列表,用于推送過載任務,它就像是一個 “任務搬運工”,將過載的任務從一個 CPU 推送到其他 CPU 上。
rt_queued字段表示 RT 運行隊列已經(jīng)加入rq隊列,rq隊列是系統(tǒng)中所有進程的運行隊列,RT 運行隊列是其中的一部分。rt_throttled字段用于限流操作,當實時進程的運行時間超過一定限制時,調(diào)度器會對其進行限流,以保證系統(tǒng)的公平性和穩(wěn)定性。rt_time字段表示累加的運行時,當超出本地rt_runtime時,則進行限制。rt_runtime字段表示分配給本地池的運行時,它就像是一個 “資源配額”,限制了實時進程在本地的運行時間。
在支持實時組調(diào)度(CONFIG_RT_GROUP_SCHED)的情況下,rt_nr_boosted字段用于優(yōu)先級翻轉(zhuǎn)問題解決。在實時系統(tǒng)中,可能會出現(xiàn)優(yōu)先級翻轉(zhuǎn)的情況,即低優(yōu)先級任務持有高優(yōu)先級任務所需的資源,導致高優(yōu)先級任務無法執(zhí)行。通過rt_nr_boosted字段,調(diào)度器可以對任務的優(yōu)先級進行調(diào)整,解決優(yōu)先級翻轉(zhuǎn)問題。rq字段指向運行隊列,tg字段指向任務組,通過這些指針,調(diào)度器可以更好地管理和調(diào)度整個任務組。
實時調(diào)度的主要操作:實時調(diào)度的操作在kernel/sched_rt.c中實現(xiàn)。
(1)進程插入enqueue_task_rt:更新調(diào)度信息,調(diào)用enqueue_rt_entity()-->__enqueue_rt_entity(),將調(diào)度實體插入到相應優(yōu)先級隊列的末尾。如下:
static void
enqueue_task_rt(struct rq *rq, struct task_struct *p, int wakeup, bool head)
{
struct sched_rt_entity *rt_se = &p->rt;
if (wakeup)
rt_se->timeout = 0;
enqueue_rt_entity(rt_se, head); /* 實際工作 */
if (!task_current(rq, p) && p->rt.nr_cpus_allowed > 1)
enqueue_pushable_task(rq, p); /* 添加到對應的hash表中 */
}
static void enqueue_rt_entity(struct sched_rt_entity *rt_se, bool head)
{
dequeue_rt_stack(rt_se); /* 先從運行隊列中刪除 */
for_each_sched_rt_entity(rt_se)
__enqueue_rt_entity(rt_se, head); /* 然后添加到運行隊列尾部 */
}
static void __enqueue_rt_entity(struct sched_rt_entity *rt_se, bool head)
{
struct rt_rq *rt_rq = rt_rq_of_se(rt_se);
struct rt_prio_array *array = &rt_rq->active;
struct rt_rq *group_rq = group_rt_rq(rt_se);
struct list_head *queue = array->queue + rt_se_prio(rt_se);
/*
* Don't enqueue the group if its throttled, or when empty.
* The latter is a consequence of the former when a child group
* get throttled and the current group doesn't have any other
* active members.
*/
if (group_rq && (rt_rq_throttled(group_rq) || !group_rq->rt_nr_running))
return;
if (head)
list_add(&rt_se->run_list, queue);
else
list_add_tail(&rt_se->run_list, queue);
__set_bit(rt_se_prio(rt_se), array->bitmap);
inc_rt_tasks(rt_se, rt_rq); /* 運行進程數(shù)增一 */
}
該函數(shù)先獲取運行隊列中的優(yōu)先級隊列,然后調(diào)用include/linux/list.h:list_add_tail()--->__list_add(),將進程插入到鏈表的末尾。如下:
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
(2)進程選擇pick_next_task_rt:實時調(diào)度會選擇最高優(yōu)先級的實時進程來運行。調(diào)用_pick_next_task_rt()--->pick_next_rt_entity()來完成獲取下一個進程的工作。如下:
static struct task_struct *pick_next_task_rt(struct rq *rq)
{
struct task_struct *p = _pick_next_task_rt(rq); /* 實際工作 */
/* The running task is never eligible for pushing */
if (p)
dequeue_pushable_task(rq, p);
#ifdef CONFIG_SMP
/*
* We detect this state here so that we can avoid taking the RQ
* lock again later if there is no need to push
*/
rq->post_schedule = has_pushable_tasks(rq);
#endif
return p;
}
static struct task_struct *_pick_next_task_rt(struct rq *rq)
{
struct sched_rt_entity *rt_se;
struct task_struct *p;
struct rt_rq *rt_rq;
rt_rq = &rq->rt;
if (unlikely(!rt_rq->rt_nr_running))
return NULL;
if (rt_rq_throttled(rt_rq))
return NULL;
do { /* 遍歷組調(diào)度中的每個進程 */
rt_se = pick_next_rt_entity(rq, rt_rq);
BUG_ON(!rt_se);
rt_rq = group_rt_rq(rt_se);
} while (rt_rq);
p = rt_task_of(rt_se);
/* 更新執(zhí)行域 */
p->se.exec_start = rq->clock_task;
return p;
}
static struct sched_rt_entity *pick_next_rt_entity(struct rq *rq,
struct rt_rq *rt_rq)
{
struct rt_prio_array *array = &rt_rq->active;
struct sched_rt_entity *next = NULL;
struct list_head *queue;
int idx;
/* 找到第一個可用的 */
idx = sched_find_first_bit(array->bitmap);
BUG_ON(idx >= MAX_RT_PRIO);
/* 從鏈表組中找到對應的鏈表 */
queue = array->queue + idx;
next = list_entry(queue->next, struct sched_rt_entity, run_list);
/* 返回找到的運行實體 */
return next;
}
該函數(shù)調(diào)用include/asm-generic/bitops/sched.h:sched_find_first_bit()返回位圖中當前被置位的最高優(yōu)先級,以作為這組鏈表的數(shù)組下標找到其優(yōu)先級隊列。然后調(diào)用include/linux/list.h:list_entry()--->include/linux/kernel.h:container_of(),返回該優(yōu)先級隊列中的第一個進程,以作為下一個要運行的實時進程。例如當前所有實時進程中最高優(yōu)先級為45(換句話說,系統(tǒng)中沒有任何實時進程的優(yōu)先級小于45),則直接讀取rt_prio_array中的queue[45],得到優(yōu)先級為45的進程隊列指針。該隊列頭上的第一個進程就是被選中的進程。這種算法的復雜度為O(1)。
sched_find_first_bit的實現(xiàn)如下。它與CPU體系結構相關,其他體系結構會實現(xiàn)自己的sched_find_fist_bit函數(shù)。下面的實現(xiàn)以最快的方式搜索100 bit的位圖,它能保證100 bit中至少有一位被清除。
static inline int sched_find_first_bit(const unsigned long *b)
{
#if BITS_PER_LONG == 64
if (b[0])
return __ffs(b[0]);
return __ffs(b[1]) + 64;
#elif BITS_PER_LONG == 32
if (b[0])
return __ffs(b[0]);
if (b[1])
return __ffs(b[1]) + 32;
if (b[2])
return __ffs(b[2]) + 64;
return __ffs(b[3]) + 96;
#else
#error BITS_PER_LONG not defined
#endif
}
(3)進程刪除dequeue_task_rt:從優(yōu)先級隊列中刪除實時進程,并更新調(diào)度信息,然后把這個進程添加到隊尾。調(diào)用鏈為dequeue_rt_entity()--->dequeue_rt_stack()--->__dequeue_rt_entity(),如下:
static void dequeue_task_rt(struct rq *rq, struct task_struct *p, int sleep)
{
struct sched_rt_entity *rt_se = &p->rt;
/* 更新調(diào)度信息 */
update_curr_rt(rq);
/* 實際工作,將rt_se從運行隊列中刪除然后
添加到隊列尾部 */
dequeue_rt_entity(rt_se);
/* 從hash表中刪除 */
dequeue_pushable_task(rq, p);
}
static void update_curr_rt(struct rq *rq)
{
struct task_struct *curr = rq->curr;
struct sched_rt_entity *rt_se = &curr->rt;
struct rt_rq *rt_rq = rt_rq_of_se(rt_se);
u64 delta_exec;
if (!task_has_rt_policy(curr)) /* 判斷是否問實時調(diào)度進程 */
return;
/* 執(zhí)行時間 */
delta_exec = rq->clock_task - curr->se.exec_start;
if (unlikely((s64)delta_exec < 0))
delta_exec = 0;
schedstat_set(curr->se.exec_max, max(curr->se.exec_max, delta_exec));
/* 更新當前進程的總的執(zhí)行時間 */
curr->se.sum_exec_runtime += delta_exec;
account_group_exec_runtime(curr, delta_exec);
/* 更新執(zhí)行的開始時間 */
curr->se.exec_start = rq->clock_task;
cpuacct_charge(curr, delta_exec); /* 組調(diào)度相關 */
sched_rt_avg_update(rq, delta_exec);
if (!rt_bandwidth_enabled())
return;
for_each_sched_rt_entity(rt_se) {
rt_rq = rt_rq_of_se(rt_se);
if (sched_rt_runtime(rt_rq) != RUNTIME_INF) {
spin_lock(&rt_rq->rt_runtime_lock);
rt_rq->rt_time += delta_exec;
if (sched_rt_runtime_exceeded(rt_rq))
resched_task(curr);
spin_unlock(&rt_rq->rt_runtime_lock);
}
}
}
static void dequeue_rt_entity(struct sched_rt_entity *rt_se)
{
dequeue_rt_stack(rt_se); /* 從運行隊列中刪除 */
for_each_sched_rt_entity(rt_se) {
struct rt_rq *rt_rq = group_rt_rq(rt_se);
if (rt_rq && rt_rq->rt_nr_running)
__enqueue_rt_entity(rt_se, false); /* 添加到隊尾 */
}
}
static void dequeue_rt_stack(struct sched_rt_entity *rt_se)
{
struct sched_rt_entity *back = NULL;
for_each_sched_rt_entity(rt_se) { /* 遍歷整個組調(diào)度實體 */
rt_se->back = back; /* 可見rt_se的back實體為組調(diào)度中前一個調(diào)度實體 */
back = rt_se;
}
/* 將組中的所有進程從運行隊列中移除 */
for (rt_se = back; rt_se; rt_se = rt_se->back) {
if (on_rt_rq(rt_se))
__dequeue_rt_entity(rt_se);
}
}
static void __dequeue_rt_entity(struct sched_rt_entity *rt_se)
{
struct rt_rq *rt_rq = rt_rq_of_se(rt_se);
struct rt_prio_array *array = &rt_rq->active;
/* 移除進程 */
list_del_init(&rt_se->run_list);
/* 如果鏈表變?yōu)榭?,則將位圖中對應的bit位清零 */
if (list_empty(array->queue + rt_se_prio(rt_se)))
__clear_bit(rt_se_prio(rt_se), array->bitmap);
dec_rt_tasks(rt_se, rt_rq); /* 運行進程計數(shù)減一 */
}
可見更新調(diào)度信息的函數(shù)為update_curr_rt(),在dequeue_rt_entity()中將當前實時進程從運行隊列中移除,并添加到隊尾。完成工作函數(shù)為dequeue_rt_stack()--->__dequeue_rt_entity(),它調(diào)用include/linux/list.h:list_del_init()--->__list_del()刪除進程。然后如果鏈表變?yōu)榭?,則將位圖中對應優(yōu)先級的bit位清零。如下:
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
從上面的介紹可以看出,對于實時調(diào)度,Linux的實現(xiàn)比較簡單,仍然采用之前的O(1)調(diào)度策略,把所有的運行進程根據(jù)優(yōu)先級放到不用的隊列里面,采用位圖方式進行使用記錄。進隊列僅僅是刪除原來隊列里面的本進程,然后將他掛到隊列尾部;而對于“移除”操作,也僅僅是從隊列里面移除后添加到運行隊列尾部。
四、實時調(diào)度類在實際中的應用
4.1工業(yè)控制系統(tǒng):精準控制的幕后英雄
在工業(yè) 4.0 的浪潮下,工業(yè)控制系統(tǒng)正朝著智能化、自動化的方向飛速發(fā)展。從汽車制造到電子設備生產(chǎn),自動化生產(chǎn)線已經(jīng)成為現(xiàn)代工業(yè)的核心。在這些復雜的生產(chǎn)線上,各種設備協(xié)同工作,每一個動作、每一次數(shù)據(jù)傳輸都需要精確的時間控制。Linux 實時調(diào)度類在其中扮演著至關重要的角色,它就像是一位精準的指揮官,確保每一個任務都能按時執(zhí)行,從而實現(xiàn)整個生產(chǎn)線的高效、穩(wěn)定運行。
以汽車制造為例,自動化生產(chǎn)線涉及眾多復雜的工序,如車身焊接、零部件裝配、噴漆等。在車身焊接環(huán)節(jié),機械臂需要按照精確的時間順序和位置坐標進行焊接操作。如果焊接任務不能按時完成,可能會導致車身結構不穩(wěn)定,影響汽車的質(zhì)量和安全性。
Linux 實時調(diào)度類通過采用 SCHED_FIFO 或 SCHED_RR 策略,為焊接任務分配高優(yōu)先級,確保機械臂能夠及時響應控制指令,準確地完成焊接操作。同時,在生產(chǎn)線的物料運輸環(huán)節(jié),AGV(自動導引車)需要根據(jù)生產(chǎn)進度及時將零部件運輸?shù)街付ㄎ恢?。Linux 實時調(diào)度類可以根據(jù) AGV 的任務優(yōu)先級和實時路況,合理地調(diào)度 AGV 的運行,避免出現(xiàn)交通擁堵和任務延遲的情況,保證生產(chǎn)線的物料供應順暢。
據(jù)相關數(shù)據(jù)顯示,采用 Linux 實時調(diào)度類的工業(yè)控制系統(tǒng),生產(chǎn)效率能夠提升 20% 以上,產(chǎn)品次品率降低 15% 左右。這充分證明了 Linux 實時調(diào)度類在工業(yè)控制系統(tǒng)中的重要性和價值,它為工業(yè)生產(chǎn)的精準控制提供了堅實的技術保障,是工業(yè)自動化不可或缺的關鍵技術之一。
4.2多媒體處理:流暢體驗的保障
在當今的數(shù)字時代,多媒體應用已經(jīng)深入到我們生活的方方面面,從高清視頻播放到音頻實時處理,從視頻會議到游戲娛樂,我們對多媒體體驗的要求越來越高。而 Linux 實時調(diào)度類正是實現(xiàn)流暢多媒體體驗的幕后功臣,它對時間敏感性任務的精確調(diào)度,確保了多媒體處理的流暢性、穩(wěn)定性和實時性。
以高清視頻播放為例,在播放高清視頻時,視頻解碼和音頻解碼任務需要在極短的時間內(nèi)完成,以保證視頻和音頻的同步播放,避免出現(xiàn)卡頓和延遲的情況。Linux 實時調(diào)度類可以為視頻解碼和音頻解碼任務分配較高的優(yōu)先級,并采用合適的調(diào)度策略,如 SCHED_RR 策略,為每個任務分配固定的時間片,確保它們能夠及時獲取 CPU 資源,快速地完成解碼工作。同時,在視頻渲染和音頻輸出環(huán)節(jié),實時調(diào)度類也能保證任務的及時執(zhí)行,將解碼后的視頻幀和音頻數(shù)據(jù)快速地輸出到顯示設備和音頻設備上,為用戶帶來流暢、清晰的視聽體驗。
在音頻實時處理領域,如語音識別、音頻編輯等應用中,Linux 實時調(diào)度類同樣發(fā)揮著重要作用。在語音識別過程中,麥克風采集到的語音信號需要及時進行處理和分析,以準確識別用戶的語音指令。Linux 實時調(diào)度類通過對語音處理任務的優(yōu)先調(diào)度,能夠快速地對語音信號進行采樣、濾波、特征提取等操作,提高語音識別的準確率和實時性。在音頻編輯軟件中,當用戶對音頻進行剪輯、混音等操作時,實時調(diào)度類可以確保音頻處理任務的高效執(zhí)行,讓用戶能夠?qū)崟r聽到處理后的音頻效果,提升音頻編輯的效率和體驗。
五、配置與優(yōu)化指南
5.1系統(tǒng)調(diào)用設置:掌控調(diào)度的 “魔法棒”
在 Linux 系統(tǒng)中,要實現(xiàn)對線程或進程調(diào)度策略和優(yōu)先級的精確控制,pthread_setschedparam和sched_setscheduler這兩個系統(tǒng)調(diào)用就像是掌控調(diào)度的 “魔法棒”,發(fā)揮著關鍵作用。
pthread_setschedparam主要用于設置線程的調(diào)度參數(shù),其函數(shù)原型如下:
#include <pthread.h>
int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param);
thread參數(shù)表示目標線程的標識符,它就像是線程的 “身份證”,通過這個標識符,系統(tǒng)能夠準確地定位到需要設置調(diào)度參數(shù)的線程。policy參數(shù)用于指定調(diào)度策略,可取值包括SCHED_OTHER(普通分時調(diào)度策略)、SCHED_FIFO(先進先出實時調(diào)度策略)和SCHED_RR(時間片輪轉(zhuǎn)實時調(diào)度策略)等,不同的取值決定了線程在系統(tǒng)中的調(diào)度方式。param是一個指向struct sched_param結構體的指針,該結構體中包含了線程的優(yōu)先級信息,通過設置param->sched_priority的值,可以調(diào)整線程的優(yōu)先級。例如:
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
void* thread_function(void* arg) {
// 獲取線程的tid
pid_t tid = gettid();
// 定義調(diào)度策略和優(yōu)先級變量
int policy;
struct sched_param sched_param;
// 獲取當前線程的調(diào)度參數(shù)
pthread_getschedparam(pthread_self(), &policy, &sched_param);
// 打印當前的調(diào)度策略和優(yōu)先級
printf("Current thread (tid: %d) policy: %d priority: %d\n", tid, policy, sched_param.sched_priority);
// 設置線程的調(diào)度策略為FIFO
policy = SCHED_FIFO;
// 設置線程的優(yōu)先級為最大值
sched_param.sched_priority = sched_get_priority_max(policy);
// 設置線程的調(diào)度策略和優(yōu)先級
pthread_setschedparam(pthread_self(), policy, &sched_param);
// 再次獲取并打印調(diào)度參數(shù)
pthread_getschedparam(pthread_self(), &policy, &sched_param);
printf("New thread (tid: %d) policy: %d priority: %d\n", tid, policy, sched_param.sched_priority);
// 線程工作代碼...
return NULL;
}
int main() {
pthread_t thread;
// 創(chuàng)建線程
pthread_create(&thread, NULL, &thread_function, NULL);
// 等待線程結束
pthread_join(thread, NULL);
return 0;
}
在上述示例中,首先獲取當前線程的調(diào)度參數(shù)并打印,然后將線程的調(diào)度策略設置為SCHED_FIFO,優(yōu)先級設置為該策略允許的最大值,最后再次獲取并打印調(diào)度參數(shù),以驗證設置是否生效。
sched_setscheduler函數(shù)則用于設置進程的調(diào)度策略和優(yōu)先級,其函數(shù)原型為:
#include <sched.h>
int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);
pid參數(shù)表示要設置調(diào)度策略的進程的進程 ID,如果pid為 0,則表示當前進程。policy和param的含義與pthread_setschedparam中的類似。例如,將當前進程的調(diào)度策略設置為SCHED_RR,并設置優(yōu)先級為 50,可以這樣實現(xiàn):
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
struct sched_param param;
param.sched_priority = 50;
int ret = sched_setscheduler(0, SCHED_RR, ?m);
if (ret == -1) {
perror("Failed to set scheduler");
exit(EXIT_FAILURE);
}
printf("Successfully set scheduler to SCHED_RR with priority 50\n");
return 0;
}
通過這兩個系統(tǒng)調(diào)用,開發(fā)者可以根據(jù)實際需求靈活地設置線程或進程的調(diào)度策略和優(yōu)先級,從而優(yōu)化系統(tǒng)的性能和實時性。在實際應用中,需要根據(jù)任務的特點和系統(tǒng)的要求,謹慎選擇調(diào)度策略和優(yōu)先級,以確保系統(tǒng)的穩(wěn)定運行和高效執(zhí)行。
5.2性能優(yōu)化建議:提升效率的 “秘籍”
在 Linux 實時調(diào)度中,為了充分發(fā)揮系統(tǒng)的性能,提升實時任務的執(zhí)行效率,我們可以從以下幾個方面入手進行優(yōu)化。
首先,根據(jù)任務特點選擇調(diào)度策略是關鍵的一步。對于那些對時間要求極為嚴格,需要在最短時間內(nèi)完成的任務,如工業(yè)控制系統(tǒng)中的緊急控制任務、航空航天中的關鍵飛行控制指令處理等,應優(yōu)先考慮使用SCHED_FIFO策略。因為該策略能夠確保任務一旦獲得 CPU 資源,就可以一直運行,直到完成或者被更高優(yōu)先級的任務搶占,從而保證了任務執(zhí)行的及時性和連續(xù)性。
而對于那些需要公平分配 CPU 時間,且對響應時間有一定要求的任務,如多媒體播放中的音頻和視頻同步處理、交互式應用程序中的用戶輸入響應等,SCHED_RR策略則更為合適。它通過時間片輪轉(zhuǎn)的方式,讓每個任務都能在一定的時間內(nèi)獲得 CPU 資源,避免了某個任務長時間獨占 CPU,導致其他任務無法及時執(zhí)行的情況,兼顧了公平性和實時性。
合理分配優(yōu)先級也是優(yōu)化實時調(diào)度性能的重要手段。在一個復雜的實時系統(tǒng)中,通常會有多個不同類型的任務同時運行,這些任務對時間的敏感度和重要性各不相同。因此,我們需要根據(jù)任務的實際需求,為它們分配合理的優(yōu)先級。高優(yōu)先級應分配給那些對系統(tǒng)正常運行至關重要,且時間要求緊迫的任務,例如在醫(yī)療設備控制系統(tǒng)中,生命體征監(jiān)測和緊急治療控制任務就需要設置較高的優(yōu)先級,以確保能夠及時響應患者的生命體征變化,保障患者的生命安全。
而對于一些相對次要的任務,如系統(tǒng)日志記錄、數(shù)據(jù)備份等,可以分配較低的優(yōu)先級,讓它們在系統(tǒng)資源空閑時執(zhí)行,避免影響關鍵任務的執(zhí)行。同時,要注意避免出現(xiàn)優(yōu)先級反轉(zhuǎn)的情況,即低優(yōu)先級任務持有高優(yōu)先級任務所需的資源,導致高優(yōu)先級任務無法執(zhí)行??梢酝ㄟ^采用優(yōu)先級繼承、優(yōu)先級天花板等算法來解決這個問題。
優(yōu)化系統(tǒng)資源配置也不容忽視。一方面,要合理分配 CPU 資源。在多處理器系統(tǒng)中,可以根據(jù)任務的特點和 CPU 的負載情況,將任務綁定到特定的 CPU 核心上執(zhí)行,以提高 CPU 緩存的命中率,減少任務在不同 CPU 核心之間切換帶來的開銷。例如,對于一些計算密集型的實時任務,可以將它們固定分配到性能較強的 CPU 核心上,以充分發(fā)揮 CPU 的計算能力。
另一方面,要關注內(nèi)存資源的管理。實時任務通常對內(nèi)存的訪問速度和穩(wěn)定性有較高的要求,因此可以通過優(yōu)化內(nèi)存分配算法、減少內(nèi)存碎片等方式,提高內(nèi)存的使用效率和性能。此外,還可以采用內(nèi)存鎖定技術,將關鍵的實時任務所需的內(nèi)存頁面鎖定在物理內(nèi)存中,避免它們被交換到磁盤上,從而提高任務的執(zhí)行速度和實時性。在一些對響應時間要求極高的金融交易系統(tǒng)中,就可以采用內(nèi)存鎖定技術,確保交易處理任務能夠快速、穩(wěn)定地執(zhí)行。