本文主要討論在高實時要求、高效能計算、DPDK等領域,Linux如何讓某一個線程排他性獨佔CPU;獨佔CPU涉及的線程、中斷隔離原理;以及如何在排他性獨佔的狀況下,甚至讓系統的timer tick也不打斷獨佔任務,從而實現最低的延遲抖動。網絡
閱讀本文大約須要20分鐘。負載均衡
本文目錄:less
在一個SMP或者NUMA系統中,CPU的數量大於1。在工程中,咱們有時候有一種需求,就是讓某個可以獨佔CPU,這個CPU什麼都不作,就只作指定的任務,從而得到低延遲、高實時的好處。性能
好比在DPDK中,經過設置線程
GRUB_CMDLINE_LINUX_DEFAULT=「isolcpus=0-3,5,7」
隔離CPU0,3,5,7,讓DPDK的任務在運行的時候,其餘任務不會和DPDK的任務進行上下文切換,從而保證網絡性能最佳[1]。在Realtime應用場景中,經過isolcpus=2隔離CPU2,而後把實時應用經過taskset綁定到隔離的核:3d
taskset-c 2 pn_dev
從而保證低延遲要求[2]。code
這個地方,咱們能夠看出,它們統一都使用了isolcpus這樣一個啓動參數。blog
實踐是檢驗真理的惟一標準,下面咱們來啓動一個8核的ARM64系統,運行Ubuntu,並指定isolcpus=2這個啓動參數:進程
系統啓動後,咱們運行下面簡單的程序(啓動8個進程運行while死循環):文檔
咱們是8核的,如今又是運行8個進程,因此理論上來說,負載均衡後,8個進程應該均分地運行在8個核上面,可是咱們來看看實際的htop結果:
咱們發現3(也就是CPU2)上面的CPU佔用率是0.0%。這實證了CPU2已經被隔離,用戶空間的進程不能在它上面跑。
固然,這個時候,咱們能夠經過taskset,強行把其中的一個a.out,綁定到CPU2上面去:
從上面命令的結果看出,663本來的affinity list只有0,1,3-7是沒有2的,而咱們強行把它設置爲了2,以後再看htop,CPU2上面佔用100%:
經過上面的實驗,咱們明顯能夠看出isolcpus=2使得CPU2上沒法再運行用戶空間的進程了(除非手動設置affinity)。
中斷
可是,能在CPU2上面運行的,不是隻有用戶態的任務,還能夠有內核線程、中斷等,那麼isolcpus=可否隔離內核線程和中斷呢?
對於中斷,咱們特別容易查看,就是實際去驗證每一個IRQ的smp_affinity就行了:
從上圖明顯能夠看出,對於4四、47號這種外設的中斷,Linux內核把smp_affinity設置爲了FB(11111011),明顯避開了CPU2,因此,實際外設中斷也不會在CPU2發生,除非咱們強行給中斷綁核,好比讓44號中斷綁定到CPU2:
echo 2 >/proc/irq/44/smp_affinity_list
以後,咱們發現44號中斷在CPU2能夠發生:
可是,系統的timer中斷、IPI,因爲是Linux系統的運行基石,實際仍是要在CPU2上面運行的。這裏面最可能給任務帶來延遲抖動的,天然是timer tick。
下面咱們重點探討下tick的問題,因爲Linux通常狀況下,已經配置IDLE狀態的NO_HZ tickless,因此CPU2上面什麼都不跑的時候,實際timer中斷幾乎不發生。
下面,咱們仍是在isolcpus=2的狀況下,運行前面那個8個進程的a.out,默認狀況下沒有任務會佔用CPU2。經過前後運行幾回cat /proc/interrupts | head 2,咱們會看到其餘core的timer中斷頻繁發生,而CPU2幾乎不變,這顯然是IDLE時候的NO_HZ在發揮省電的做用:
可是,一旦咱們聽任務到CPU2,哪怕只是放1個,就會發現CPU2上面的timer中斷開始增長:
這說明一點,哪怕隔離的CPU上面只有一個線程去跑,timer tick就會開始跑,固然,這個timer tick也會頻繁打斷這一個線程,從而形成大量的上下文切換。你確定會以爲Linux怎麼這麼傻,既然只有一我的,那也沒有時間片分片的必要,不須要在2個或者多個任務進行時間片劃分地調度,爲啥還要跑tick?其實緣由是咱們的內核默認只是使能了IDLE的NO_HZ:
咱們來從新編譯一個內核,使能NO_HZ_FULL:
當咱們使能了NO_HZ_FULL後,Linux支持在CPU上僅有1個任務的時候,是能夠NO_HZ的。可是有2個就傻眼了,因此這個「FULL」也不是真地FULL[3]。這固然也能夠理解,由於有2個就涉及到時間片調度的問題。何時應該使能NO_HZ_FULL,內核文檔Documentation/timers/no_hz.rst有明確地「指示」,只有在實時和HPC等的場景,才須要,不然默認的NO_HZ_IDLE是你最好的選擇:
咱們從新編譯了內核,選中了NO_HZ_FULL,下面啓動Linux,注意啓動的時候參數添加nohz_full=2,讓CPU2支持NO_HZ_FULL:
從新運行CPU2只有一個任務的場景,看看它的timer中斷髮生狀況:
發現CPU2上面的tick穩定在188上面,這樣相信你會更加開心,由於你獨佔地更加完全了!
下面,咱們再放一個task進去CPU2,有2個任務的狀況下,CPU2上面的timer tick開始增長:
不過,這或許不是個問題,由於咱們說好了「獨佔」,1個任務獨佔的時候,timer tick不來打擾,應該已是很是理想的狀況了!
內核態線程
內核態的線程其實和用戶態差很少,當它們沒有綁定到隔離的CPU的時候,是不會跑到隔離CPU運行的。下面用筆者在內核裏面添加的dma_map_benchmark來作實驗[4],開啓16個內核線程來進行DMA map和unmap(注意咱們只有8個核):
./dma_map_benchmark -s 120 -t 16
咱們看到CPU2上面的CPU佔用也是0:
內核裏面的dma_map_benchmark線程在狂佔CPU0-1, 3-7,可是就是不去佔CPU2:
可是,內核線程若是用kthread_bind_mask()相似API把線程綁定到了隔離的CPU,則狀況就不同了,這就相似用taskset把用戶態的任務綁定到CPU同樣。
對於實時性要求高、高性能計算等場景,若是要讓某個任務獨佔CPU,最理想的選擇是:
採用isolcpus隔離CPU
將指定任務綁定到隔離CPU
當心意外地把中斷、內核線程綁定到了隔離CPU,排查到這些「意外」分子
使能NO_HZ_FULL,則效果更佳,由於連timer tick中斷也不打擾你了。