硬實(shí)時(shí)是什么?
眾所周知,硬實(shí)時(shí)的概念,其核心并非追求速度的極致,而是確保系統(tǒng)能在預(yù)定的、可重復(fù)的時(shí)間范圍內(nèi)給予確定的響應(yīng)。這意味著,實(shí)時(shí)系統(tǒng)的正確性不僅在于計(jì)算邏輯的正確,更在于結(jié)果的產(chǎn)生時(shí)間是否符合預(yù)期。以汽車為例,當(dāng)發(fā)生碰撞時(shí),安全氣囊必須在極短的時(shí)間內(nèi)彈開,否則可能無(wú)法起到應(yīng)有的保護(hù)作用。
在評(píng)估實(shí)時(shí)操作系統(tǒng)(RTOS)的性能時(shí),我們通常會(huì)考慮其在最惡劣情況下的延遲。比如,當(dāng)對(duì)Linux進(jìn)行改造,以實(shí)現(xiàn)中斷或高優(yōu)先級(jí)任務(wù)在100微秒內(nèi)的確定性延遲時(shí),我們還需要比較其他RTOS如RT-Thread的性能。RT-Thread可能無(wú)需改造就能達(dá)到微秒級(jí)別的延遲。因此,在選擇RTOS時(shí),我們需要根據(jù)應(yīng)用的延遲要求來(lái)權(quán)衡。對(duì)于200微秒以內(nèi)的延遲要求,改造后的Linux可能是一個(gè)合適的選擇;但對(duì)于微秒級(jí)別的要求,Linux可能就不是最佳選擇。
另外,關(guān)于RTOS和Linux在實(shí)時(shí)性方面的差異,我們需要澄清一個(gè)誤解。并非在RTOS中隨意編寫代碼就能滿足硬實(shí)時(shí)的要求,同樣,在Linux中也并非無(wú)法實(shí)現(xiàn)實(shí)時(shí)性。RTOS由于其設(shè)計(jì)特點(diǎn)和調(diào)度機(jī)制,通常更容易實(shí)現(xiàn)硬實(shí)時(shí),但這并不意味著在Linux中就無(wú)法實(shí)現(xiàn)。Linux通過(guò)特定的配置和優(yōu)化,也可以提供一定程度的實(shí)時(shí)性,盡管可能無(wú)法與專門的RTOS相媲美。
因此,在選擇操作系統(tǒng)時(shí),我們需要根據(jù)應(yīng)用的具體需求和場(chǎng)景來(lái)權(quán)衡。對(duì)于需要高實(shí)時(shí)性的應(yīng)用,RTOS可能是更好的選擇;而對(duì)于一些對(duì)實(shí)時(shí)性要求不那么嚴(yán)格的應(yīng)用,Linux則可能是一個(gè)更經(jīng)濟(jì)、更靈活的選擇。
Linux為什么不硬實(shí)時(shí)?
我們首先看一下,Linux為什么不能提供硬實(shí)時(shí)能力。我們認(rèn)為L(zhǎng)inux主要有如下問(wèn)題(你站在硬實(shí)時(shí)的角度看它是問(wèn)題,你換個(gè)角度看,它就反而是正確的地方):
1. spinlock是一個(gè)隨處可見被內(nèi)核、驅(qū)動(dòng)使用的API
Linux內(nèi)核與驅(qū)動(dòng)開發(fā)人員對(duì)自旋鎖(spinlock)的運(yùn)用可謂是熱衷至極。每當(dāng)遇到無(wú)需睡眠且時(shí)間較短的臨界區(qū)保護(hù)場(chǎng)景時(shí),他們幾乎都會(huì)優(yōu)先考慮使用自旋鎖??梢哉f(shuō),如果不了解自旋鎖,那么即便在內(nèi)核與驅(qū)動(dòng)開發(fā)領(lǐng)域有所建樹,也稱不上是真正的英雄。
自旋鎖的魅力在于其高效性。當(dāng)兩個(gè)或多個(gè)執(zhí)行單元(如線程、中斷等)競(jìng)相獲取同一鎖時(shí),自旋鎖允許失敗的執(zhí)行單元不是立即進(jìn)行上下文切換,而是原地自旋等待。這種機(jī)制避免了因上下文切換而帶來(lái)的額外開銷,特別是在鎖持有時(shí)間較短的情況下,自旋等待的代價(jià)往往低于上下文切換的代價(jià)。
然而,自旋鎖也并非完美無(wú)缺。它有一個(gè)顯著的副作用,即當(dāng)某個(gè)執(zhí)行單元持有鎖時(shí),會(huì)禁止該CPU核上的搶占調(diào)度。這意味著即使存在更高優(yōu)先級(jí)的任務(wù)等待執(zhí)行,也必須等待當(dāng)前持有鎖的任務(wù)釋放鎖后才能獲得執(zhí)行機(jī)會(huì)。
在Linux內(nèi)核中,自旋鎖的實(shí)現(xiàn)主要側(cè)重于核間自旋。當(dāng)多個(gè)核上的執(zhí)行單元嘗試獲取同一鎖時(shí),它們會(huì)在各自的核上進(jìn)行自旋等待。而在核內(nèi),則是通過(guò)禁止搶占來(lái)實(shí)現(xiàn)臨界區(qū)的保護(hù),確保在持有鎖期間不會(huì)有其他任務(wù)打斷當(dāng)前任務(wù)的執(zhí)行。
綜上所述,自旋鎖在Linux內(nèi)核與驅(qū)動(dòng)開發(fā)中扮演著重要角色,其高效性使得它在特定場(chǎng)景下成為首選的同步機(jī)制。然而,我們也需要認(rèn)識(shí)到它帶來(lái)的副作用,并在使用時(shí)權(quán)衡其優(yōu)缺點(diǎn)。
假設(shè)T1、T2、T3和T4這四個(gè)任務(wù)都在同一個(gè)CPU核上運(yùn)行。當(dāng)T1成功獲取到一個(gè)自旋鎖(spinlock)時(shí),該CPU核上的搶占調(diào)度機(jī)制就會(huì)被臨時(shí)禁用。這樣做的目的是為了保護(hù)臨界區(qū)內(nèi)的代碼和數(shù)據(jù),避免在T1執(zhí)行關(guān)鍵任務(wù)時(shí)被其他任務(wù)打斷。
然而,如果在T1持有自旋鎖的過(guò)程中,T2作為一個(gè)高優(yōu)先級(jí)的實(shí)時(shí)任務(wù)被喚醒并準(zhǔn)備執(zhí)行,由于搶占調(diào)度被禁止,T2無(wú)法立即打斷T1的執(zhí)行。即使T2的優(yōu)先級(jí)高于T1,它也必須耐心地等待T1釋放自旋鎖。
這里的問(wèn)題在于,我們無(wú)法精確預(yù)知T1將會(huì)持有自旋鎖多久。這完全取決于T1在臨界區(qū)內(nèi)執(zhí)行的具體任務(wù)(即“做xxxx”)的復(fù)雜性和耗時(shí)情況。由于這種不確定性,T2需要等待的具體時(shí)間也變得不可預(yù)測(cè)。這種不確定性對(duì)于實(shí)時(shí)任務(wù)來(lái)說(shuō)是非常不利的,因?yàn)樗茐牧藢?shí)時(shí)系統(tǒng)所追求的決定性時(shí)延。
決定性時(shí)延是指在實(shí)時(shí)系統(tǒng)中,任務(wù)能夠在預(yù)定的、可預(yù)測(cè)的時(shí)間范圍內(nèi)完成。然而,由于T1持有自旋鎖的時(shí)間不可知,T2的執(zhí)行被延遲了多久也變得未知,這就破壞了實(shí)時(shí)系統(tǒng)的決定性時(shí)延特性。
因此,在使用自旋鎖時(shí),需要仔細(xì)考慮其對(duì)實(shí)時(shí)任務(wù)調(diào)度和時(shí)延的影響。在實(shí)時(shí)性要求非常高的系統(tǒng)中,可能需要考慮其他同步機(jī)制或調(diào)度策略,以確保實(shí)時(shí)任務(wù)能夠得到及時(shí)的響應(yīng)和執(zhí)行。
2. Linux的中斷執(zhí)行時(shí)間可能過(guò)長(zhǎng)且不可嵌套
眾所周知,早期的Linux版本有個(gè)標(biāo)記叫IRQF_DISABLED,標(biāo)記本中斷在執(zhí)行的時(shí)候,其他所有中斷都被禁止進(jìn)入;而后Linux內(nèi)核實(shí)際去掉了這個(gè)申請(qǐng)flags,其實(shí)就是都是IRQF_DISABLED了,總體可認(rèn)為L(zhǎng)inux內(nèi)核不支持中斷的嵌套。
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id);
中斷在執(zhí)行的時(shí)候,所有的中斷都進(jìn)不來(lái),這個(gè)設(shè)計(jì)本身簡(jiǎn)化了內(nèi)核,但是對(duì)于硬實(shí)時(shí)的打擊是致命的,前面的中斷不執(zhí)行完成,優(yōu)先級(jí)再高的中斷也得給我等著。
比如中斷1在執(zhí)行的過(guò)程中,來(lái)了中斷2,而中斷2對(duì)應(yīng)的事情是必須要決定性時(shí)延的,由于IRQ1的中斷服務(wù)程序也是碼農(nóng)寫的,我們無(wú)法確定這個(gè)中斷服務(wù)程序要執(zhí)行多久。這顯然讓高優(yōu)先級(jí)中斷2的進(jìn)入延遲不再具備可預(yù)期性。
3. 軟中斷(softirq)是一個(gè)比進(jìn)程上下文優(yōu)先級(jí)更高的上下文
我們?cè)O(shè)想一個(gè)場(chǎng)景,哪怕Linux解決了問(wèn)題2,就是Linux的中斷變地可嵌套,高優(yōu)先級(jí)的中斷可以打斷低優(yōu)先級(jí)的中斷,并且高優(yōu)先級(jí)的中斷2喚醒了一個(gè)用戶寫的實(shí)時(shí)線程。
IRQ2喚醒了實(shí)時(shí)任務(wù)T1,但是T1必須等待IRQ1喚起的軟中斷(也包括使用軟中斷上下文的tasklet等)被執(zhí)行完,T1才能投入執(zhí)行。IRQ1喚起的softirq的代碼是碼農(nóng)寫的,這個(gè)碼農(nóng)寫多久,鬼都不知道,這顯然破壞了實(shí)時(shí)任務(wù)T1得以調(diào)度執(zhí)行的確定性時(shí)延。
4. 內(nèi)核里面會(huì)屏蔽中斷的API如local_irq_disable、spin_lock_irqsave等
前文已經(jīng)多次指出,在驅(qū)動(dòng)程序中調(diào)用local_irq_disable()函數(shù)往往被視為一個(gè)潛在的問(wèn)題或者說(shuō)是bug。原因在于這個(gè)函數(shù)會(huì)禁用本地CPU的中斷,但它并不能解決其他CPU核上運(yùn)行的線程或中斷服務(wù)程序與當(dāng)前核上線程之間的競(jìng)態(tài)條件。盡管在只有一個(gè)CPU核的系統(tǒng)中調(diào)用此API通常是安全的,但我們?cè)诰帉慙inux內(nèi)核代碼時(shí),應(yīng)當(dāng)始終假設(shè)我們是在多核環(huán)境下工作,這是Linux內(nèi)核編程跨平臺(tái)的基本常識(shí)。
大部分有經(jīng)驗(yàn)的開發(fā)者都明白,在編寫驅(qū)動(dòng)程序時(shí)應(yīng)當(dāng)避免使用local_irq_disable()這樣的API。然而,spin_lock_irqsave()這樣的API在內(nèi)核編程中卻非常常見。它通常用于一個(gè)特定的場(chǎng)景,即當(dāng)中斷服務(wù)程序與線程之間存在潛在的競(jìng)態(tài)條件時(shí)。作為內(nèi)核程序員,我相信你已經(jīng)非常熟悉這樣的經(jīng)典用法了,這已經(jīng)成為了內(nèi)核編程中的常規(guī)操作,體現(xiàn)出了內(nèi)核編程的嚴(yán)謹(jǐn)性和技巧性。
它把T1、T2、T3、T4、IRQ1、IRQ2這6者之間的競(jìng)爭(zhēng)消滅于無(wú)形。T1如果持有了spin_lock_irqsave,本核上的T2、IRQ1顯然進(jìn)不來(lái),CPU1上面的T3、T4、IRQ2想訪問(wèn)T1訪問(wèn)的臨界資源必須spin。IRQ1如果持有了spin_lock, CPU1上面的T3、T4、IRQ2想訪問(wèn)IRQ1訪問(wèn)的臨界資源必須spin。
那么,問(wèn)題又來(lái)了,spin_lock_irqsave既屏蔽了搶占,又屏蔽了中斷,這會(huì)導(dǎo)致中斷和實(shí)時(shí)任務(wù)的確定性時(shí)延造成不可預(yù)期的破壞。因?yàn)閟pin_lock_irqsave和spin_lock_irqrestore是碼農(nóng)寫的,鬼都不知道它要多久。
當(dāng)然,歷史上,粗獷的大內(nèi)核鎖(Big Kernel Lock,BKL)也是一個(gè)問(wèn)題。由于晶晶姑娘不喜歡內(nèi)核粗獷的一面,BKL在如今的內(nèi)核里面已經(jīng)煙消云散。
在Linux的世界里,這些鎖當(dāng)然都沒(méi)有一個(gè)鎖牛逼,就是RCU,尤其是面對(duì)這個(gè)世界符合阿姆達(dá)爾定律(Amdahl's law)定律的情況下,我們既要保證臨界資源訪問(wèn)的被保護(hù),又要盡一切可能地讓多個(gè)線程同時(shí)狂奔。關(guān)于RCU的細(xì)節(jié),謝神醫(yī)已經(jīng)有多篇文章論述。
Linux的世界大概是這樣的:中斷、軟中斷、線程(包括ksoftirqd線程)。我們都清楚地知道,軟中斷大量陷入的情況下,內(nèi)核會(huì)將后續(xù)的軟中斷投入ksoftirqd內(nèi)核線程執(zhí)行,所以軟中斷還有一個(gè)可能的執(zhí)行時(shí)機(jī)是在內(nèi)核線程里面。
5. Linux用戶空間內(nèi)存的lazy分配機(jī)制與交換swap
對(duì)于喜歡在RTOS寫程序的童鞋來(lái)說(shuō),Linux的世界一時(shí)半會(huì)難以理解,但是對(duì)于寫Linux的童鞋來(lái)說(shuō),絕大多數(shù)的RTOS簡(jiǎn)直就是在裸奔。
我們都知道,在Linux里面,用戶空間的內(nèi)存都執(zhí)行l(wèi)azy的分配機(jī)制。比如你malloc一個(gè)內(nèi)存
char *p = malloc(1024*1024);
這個(gè)時(shí)候Linux忽悠你說(shuō)拿到了內(nèi)存并且p獲得了地址,但是實(shí)際的拿到卻是在你寫的時(shí)候,以page fault缺頁(yè)中斷的形式獲得的。比如你寫p[0]=1就拿到了第一頁(yè),你寫p[4096]就拿到了第2頁(yè)。這個(gè)lazy的分配機(jī)制,也同樣適用于棧、代碼段等。
你是一個(gè)實(shí)時(shí)的線程,你被喚醒得以執(zhí)行,你執(zhí)行的時(shí)候,發(fā)現(xiàn)你訪問(wèn)的臨時(shí)變量還沒(méi)有獲得內(nèi)存,你的代碼段可能還特馬在硬盤里,請(qǐng)問(wèn)你實(shí)時(shí)個(gè)什么鬼?你執(zhí)行到函數(shù)b的時(shí)候,去訪問(wèn)d[1000],結(jié)果發(fā)現(xiàn)這個(gè)棧的這頁(yè)內(nèi)存還要通過(guò)page fault來(lái)通過(guò)內(nèi)核buddy去申請(qǐng),你的確定性延遲還如何滿足?
main()
{
…
a();
}
a()
{
…
b();
}
b()
{
int d[1024];
d[1000]=100;
c();
}
當(dāng)然,已經(jīng)進(jìn)入內(nèi)存的東西,也由于內(nèi)核的swap機(jī)制,會(huì)與磁盤進(jìn)行交換。
絕大多數(shù)的RTOS都沒(méi)有這個(gè)“問(wèn)題”,這也恰恰是他們不夠“牛逼”的地方。對(duì)于手機(jī)、電腦這種富應(yīng)用的系統(tǒng)而言,你不能用資源已經(jīng)被確定性分配的思維模式來(lái)思考。
Linux preempt-rt如何解決這些問(wèn)題?
前段時(shí)間,這篇文章刷屏了:《Linux實(shí)時(shí)補(bǔ)丁即將合并進(jìn)Linux 5.3》?,許多童鞋都說(shuō)活久見,實(shí)際是活久了也特么沒(méi)見到。我進(jìn)內(nèi)核搜索,發(fā)現(xiàn)沒(méi)有一個(gè)體系架構(gòu)到目前真地使能了支持。
到今天為止,ARCH_SUPPORTS_RT誰(shuí)他么都不是真:
barry@barryUbuntu:~/develop/linux$ git grep ARCH_SUPPORTS_RT
arch/Kconfig:config ARCH_SUPPORTS_RT
kernel/Kconfig.preempt: depends on EXPERT && ARCH_SUPPORTS_RT
所以,你要真地在mainline見到PREEMPT_RT開花結(jié)果,還必須活地更久一點(diǎn)。
當(dāng)你提到preempt-rt補(bǔ)丁時(shí),強(qiáng)調(diào)了Linux的特性和它在實(shí)時(shí)性方面的考量,這是非常準(zhǔn)確的。Linux作為一個(gè)功能豐富的操作系統(tǒng),其設(shè)計(jì)初衷是支持多樣化的應(yīng)用和場(chǎng)景,包括用戶空間的各種進(jìn)程和線程。
preempt-rt補(bǔ)丁是Linux內(nèi)核的一個(gè)實(shí)時(shí)性增強(qiáng)補(bǔ)丁,它旨在提升Linux在實(shí)時(shí)任務(wù)調(diào)度方面的性能。通過(guò)改進(jìn)內(nèi)核的調(diào)度策略和中斷處理機(jī)制,preempt-rt使得Linux能夠更好地滿足實(shí)時(shí)應(yīng)用的需求。
相對(duì)于其他RTOS,Linux在處理實(shí)時(shí)任務(wù)時(shí)確實(shí)有其獨(dú)特之處。RTOS通常更強(qiáng)調(diào)高優(yōu)先級(jí)中斷的確定性時(shí)延,因?yàn)樗鼈兺ǔ⒄麄€(gè)系統(tǒng)編譯在一起,可以在中斷處理程序中直接嵌入策略。然而,Linux作為一個(gè)通用的操作系統(tǒng),其內(nèi)核與用戶空間之間有著明確的分離。用戶空間的應(yīng)用無(wú)法直接訪問(wèn)或修改內(nèi)核代碼,只能通過(guò)系統(tǒng)調(diào)用等接口與內(nèi)核進(jìn)行交互。
因此,在Linux中,實(shí)現(xiàn)實(shí)時(shí)任務(wù)的確定性調(diào)度時(shí)延就顯得尤為重要。通過(guò)preempt-rt補(bǔ)丁,Linux內(nèi)核提供了更好的實(shí)時(shí)調(diào)度能力,使得高優(yōu)先級(jí)的RT線程能夠得到及時(shí)的處理和調(diào)度。同時(shí),由于Linux內(nèi)核提供了豐富的操作接口,開發(fā)者可以在用戶空間編寫應(yīng)用,通過(guò)調(diào)用這些接口來(lái)利用內(nèi)核提供的實(shí)時(shí)功能。
總的來(lái)說(shuō),Linux不是一個(gè)簡(jiǎn)單的裸機(jī)操作系統(tǒng),它有著復(fù)雜的內(nèi)核架構(gòu)和用戶空間應(yīng)用。在實(shí)現(xiàn)實(shí)時(shí)性時(shí),需要充分考慮到這種架構(gòu)的特點(diǎn),并通過(guò)適當(dāng)?shù)难a(bǔ)丁和配置來(lái)優(yōu)化實(shí)時(shí)性能。而preempt-rt補(bǔ)丁正是為了提升Linux在實(shí)時(shí)任務(wù)調(diào)度方面的能力而設(shè)計(jì)的。