一文玩轉(zhuǎn)ARM64 SMP多核啟動(dòng)(一)- spin-table(超級(jí)詳細(xì)~)
1.前言
環(huán)境: 處理器架構(gòu):arm64 uboot版本:uboot-2020.01 內(nèi)核源碼:linux-5.0 ubuntu版本:20.04.1 ATF版本:2.1 代碼閱讀工具:vim+ctags+cscope
一般嵌入式系統(tǒng)使用的都是對(duì)稱(chēng)多處理器(Symmetric Multi-Processor, SMP)系統(tǒng),包含了多個(gè)cpu, 這幾個(gè)cpu都是相同的處理器,如4核Contex-A53。但是在系統(tǒng) 啟動(dòng)階段他們的地位并不是相同的,其中core0是主cpu(也叫引導(dǎo)處理器),其他core是從cpu(也叫輔處理器),引導(dǎo)cpu負(fù)責(zé)執(zhí)行我們的啟動(dòng)加載程序如uboot,以及初始化內(nèi)核,系統(tǒng)初始化完成之后主core會(huì)啟動(dòng)從處理器。
一般主處理器啟動(dòng)從處理器有以下三種:
ACPI
spin-table
PSCI
第一種ACPI是高級(jí)配置與電源接口(Advanced Configuration and Power Interface)一般在x86平臺(tái)用的比較多,而后兩種spin-table(自旋表)和PSCI(電源狀態(tài)協(xié)調(diào)協(xié)議 Power State Coordination)會(huì)在arm平臺(tái)上使用,本系列主要講解后兩種。
2.cpu啟動(dòng)的一些概念
cpu啟動(dòng)的含義:cpu可以從內(nèi)存中取指、譯碼、執(zhí)行,當(dāng)然內(nèi)存可以是soc片內(nèi)的sram,也可以是ddr。
我們要知道,程序?yàn)楹慰梢栽诙鄠€(gè)cpu上并發(fā)執(zhí)行:他們有各自獨(dú)立的一套寄存器,如:程序計(jì)數(shù)器pc,棧指針寄存器sp,通用寄存器等,可以獨(dú)自 取指、譯碼、執(zhí)行,當(dāng)然內(nèi)存和外設(shè)資源是共享的,多核環(huán)境下當(dāng)訪問(wèn)臨界區(qū) 資源一般 自旋鎖來(lái)防止競(jìng)態(tài)發(fā)生。
soc啟動(dòng)流程:soc啟動(dòng)的一般會(huì)從片內(nèi)的rom, 也叫bootrom開(kāi)始執(zhí)行第一條指令,這個(gè)地址是系統(tǒng)默認(rèn)的啟動(dòng)地址,會(huì)在bootrom中由芯片廠家固化一段啟動(dòng)代碼來(lái)加載啟動(dòng)bootloader到片內(nèi)的sram,啟動(dòng)完成后的bootloader除了做一些硬件初始化之外做的最重要的事情是初始化ddr,因?yàn)閟ram的空間比較小所以需要初始化擁有大內(nèi)存 ddr,最后會(huì)從網(wǎng)絡(luò)/usb下載 或從存儲(chǔ)設(shè)備分區(qū)上加載內(nèi)核到ddr某個(gè)地址,為內(nèi)核傳遞參數(shù)之后,然后bootloader就完成了它的使命,跳轉(zhuǎn)到內(nèi)核,就進(jìn)入了操作系統(tǒng)內(nèi)核的世界。
linux內(nèi)核啟動(dòng)流程:bootloader將系統(tǒng)的控制權(quán)交給內(nèi)核之后,他首先會(huì)進(jìn)行處理器架構(gòu)相關(guān)初始化部分,如設(shè)置異常向量表,初始化mmu(之后內(nèi)核就從物理地址空間進(jìn)入了虛擬地址空間的世界,一切是那么的虛無(wú)縹緲,又是那么的恰到好處)等等,然后會(huì)清bss段,設(shè)置sp之后跳轉(zhuǎn)到C語(yǔ)言部分進(jìn)行更加復(fù)雜通用的初始化,其中會(huì)進(jìn)行內(nèi)存方面的初始化,調(diào)度器初始化,文件系統(tǒng)等內(nèi)核基礎(chǔ)組件 初始化工作,隨后會(huì)進(jìn)行關(guān)鍵的從處理器的引導(dǎo)過(guò)程,然后是各種實(shí)質(zhì)性的設(shè)備驅(qū)動(dòng)的初始化,最后 創(chuàng)建系統(tǒng)的第一個(gè)用戶進(jìn)程init后進(jìn)入用戶空間執(zhí)行用戶進(jìn)程宣誓內(nèi)核初始化完成,可以進(jìn)程正常的調(diào)度執(zhí)行。
系統(tǒng)初始化階段大多數(shù)都是主處理器做初始化工作,所有不用考慮處理器并發(fā)情況,一旦從處理器被bingup起來(lái),調(diào)度器和各自的運(yùn)行隊(duì)列準(zhǔn)備就緒,多個(gè)任務(wù)就會(huì)均衡到各個(gè)處理器,開(kāi)始了并發(fā)的世界,一切是那么的神奇。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個(gè)人覺(jué)得比較好的學(xué)習(xí)書(shū)籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。?!前100名進(jìn)群領(lǐng)取,額外贈(zèng)送一份價(jià)值699的內(nèi)核資料包(含視頻教程、電子書(shū)、實(shí)戰(zhàn)項(xiàng)目及代碼)? ??


3.支持spin-table情況
了解了關(guān)于cpu啟動(dòng)的一些基本概念,下面開(kāi)始我們的正題,講解arm64常用的兩種cpu啟動(dòng)方式。首先,我們來(lái)看一下比較簡(jiǎn)單的自旋表的方式啟動(dòng)從處理器。
從bootloader說(shuō)起(以u(píng)boot為例):首先,上電后主處理器和從處理器都會(huì)啟動(dòng),執(zhí)行uboot,從uboot的_start的匯編代碼開(kāi)始執(zhí)行,主處理器在uboot中歡快的執(zhí)行后啟動(dòng)內(nèi)核,進(jìn)入內(nèi)核執(zhí)行,而從處理器會(huì)執(zhí)行到spin_table_secondary_jump中(注意:之前執(zhí)行的代碼,設(shè)置的寄存器都是各cpu獨(dú)立的寄存器)
在spin_table_secondary_jump中:首先會(huì)執(zhí)行wfe指令,使得從處理器睡眠等待。如果被喚醒,則從處理器會(huì)判斷spin_table_cpu_release_addr這個(gè)地址是否為0,為0則繼續(xù)跳轉(zhuǎn)到wfe處繼續(xù)睡眠,否則跳轉(zhuǎn)到spin_table_cpu_release_addr指定的地址處執(zhí)行。
那么這個(gè)地址什么時(shí)候會(huì)被設(shè)置呢?答案是:主處理器在uboot中讀取設(shè)備樹(shù)的相關(guān)節(jié)點(diǎn)屬性獲得,我們來(lái)看下如何獲得。執(zhí)行路徑為:
在spin_table_update_dt函數(shù)中做了幾件非常重要的事情:
其實(shí),他做的工作主要有兩個(gè):
將即將供內(nèi)核使用的設(shè)備樹(shù)的cpu節(jié)點(diǎn)的cpu-release-addr屬性設(shè)置為spin_table_cpu_release_addr的地址(這個(gè)地址也就是cpu的釋放地址)。
將spin_table_reserve_begin到spin_table_reserve_end符號(hào)描述的地址范圍添加到設(shè)備樹(shù)的保留內(nèi)存中。
實(shí)際上保留的是spin_table_secondary_jump匯編函數(shù)的指令代碼段和spin_table_cpu_release_addr地址內(nèi)存,當(dāng)然保留是為了在內(nèi)核中不被內(nèi)存管理使用,這樣這段物理內(nèi)存的數(shù)據(jù)不會(huì)被覆蓋丟失。注意:spin_table_cpu_release_addr地址處被初始化為0(上面匯編19行)。
先來(lái)看一下一個(gè)使用自旋表作為啟動(dòng)方式的平臺(tái)設(shè)備樹(shù)cpu節(jié)點(diǎn):
可以發(fā)現(xiàn)啟動(dòng)方法為spin-table,釋放地址初始化為0x10000fff8。
那么什么時(shí)候釋放地址spin_table_cpu_release_addr 的內(nèi)容不是0呢?
那么我們得回到主處理器流程上來(lái):主處理器設(shè)置好了設(shè)備樹(shù),傳遞給內(nèi)核設(shè)備樹(shù)地址之后就要啟動(dòng)內(nèi)核,啟動(dòng)內(nèi)核之后,執(zhí)行初始化工作,執(zhí)行如下路徑:
我們來(lái)看下smp_spin_table_cpu_init函數(shù):
可以發(fā)現(xiàn),函數(shù)讀取設(shè)備樹(shù)的cpu-release-addr屬性值到cpu_release_addr[cpu]中,cpu_release_addr變量是個(gè)NR_CPUS個(gè)元素的數(shù)組,每個(gè)處理器占用一個(gè)元素,其實(shí)也就是將之前保存的spin_table_reserve_begin符號(hào)的物理地址保存到這個(gè)變量中。
現(xiàn)在還沒(méi)有看到設(shè)置釋放地址的地方,繼續(xù)往下看:
主處理器繼續(xù)執(zhí)行如下路徑:
我們來(lái)看這個(gè)函數(shù):
上面函數(shù)主要做兩點(diǎn):
102行,cpu的釋放地址處寫(xiě)入secondary_holding_pen的地址,由于獲得的內(nèi)核符號(hào)是虛擬地址所以轉(zhuǎn)化為物理地址寫(xiě)到釋放地址處。
109行,喚醒處于wfe狀態(tài)的從處理器。
我們?cè)俅位氐綇奶幚砥魉叩却牡胤剑涸趨R編函數(shù)spin_table_secondary_jump中喚醒后執(zhí)行,wfe的下幾行指令,判斷spin_table_cpu_release_addr地址處的內(nèi)容是否為0,這個(gè)時(shí)候由于主處理器往這個(gè)地址寫(xiě)入了釋放地址,所有會(huì)執(zhí)行15行指令,跳轉(zhuǎn)到secondary_holding_pen處執(zhí)行,請(qǐng)注意:這個(gè)地址是物理地址,而且從處理器還沒(méi)有開(kāi)啟mmu,所以從處理器還沒(méi)有進(jìn)入虛擬地址的世界。
獲得釋放地址后的從處理器,猶如脫韁的野馬,喚醒后直接進(jìn)入了內(nèi)核的世界去執(zhí)行指令,多么的殘暴,來(lái)到了如下的匯編函數(shù):
但是事與愿違,在這個(gè)函數(shù)中又有了一層關(guān)卡:689行到701行 判斷是否secondary_holding_pen_release被設(shè)置為了從處理器的編號(hào),如果設(shè)置的不是我的編號(hào),則我再次進(jìn)入705行執(zhí)行wfe睡眠等待,行吧,那就等待啥時(shí)候主處理器來(lái)將secondary_holding_pen_release設(shè)置為我的處理器編號(hào)吧。那么何時(shí)會(huì)設(shè)置呢?答案是最終要啟動(dòng)從處理器的時(shí)候。
我們?cè)俅位氐街魈幚砥鞯奶幚砹鞒?,上面主處理器?zhí)行到了smp_prepare_cpus之后,繼續(xù)往下執(zhí)行,代碼路徑如下:
我們來(lái)看smp_spin_table_cpu_boot函數(shù):
可以看到這里將從處理器編號(hào)寫(xiě)到了secondary_holding_pen_release中,然后喚醒從處理器,從處理器再次歡快的執(zhí)行,最后執(zhí)行到secondary_startup,來(lái)做從處理器的初始化工作(如設(shè)置mmu,異常向量表等),最終從處理器還是處于wfi狀態(tài),但是這個(gè)時(shí)候從處理器已經(jīng)具備了執(zhí)行進(jìn)程的能力,可以用來(lái)調(diào)度進(jìn)程,觸發(fā)中斷等,和主處理器有著相同的地位,后面我們會(huì)分析。耐心讀到這里的讀者,也很不容易了,為你們勇氣點(diǎn)贊。我覺(jué)得源代碼是最好的資料,閱讀源代碼才是最佳的學(xué)習(xí)理解內(nèi)核的方法,當(dāng)然不想看代碼可以直接看下面這張圖解:

spin-table方式的多核啟動(dòng)方式,顧名思義在于自旋,主處理器和從處理器上電都會(huì)啟動(dòng),主處理器執(zhí)行uboot暢通無(wú)阻,從處理器在spin_table_secondary_jump處wfe睡眠,主處理器通過(guò)修改設(shè)備樹(shù)的cpu節(jié)點(diǎn)的cpu-release-addr屬性為spin_table_cpu_release_addr,這是從處理器的釋放地址所在的地方,主處理器進(jìn)入內(nèi)核后,會(huì)通過(guò)smp_prepare_cpus函數(shù)調(diào)用spin-table 對(duì)應(yīng)的cpu操作集的cpu_prepare方法從而在smp_spin_table_cpu_prepare函數(shù)中設(shè)置從處理器的釋放地址為secondary_holding_pen這個(gè)內(nèi)核函數(shù),然后通過(guò)sev指令喚醒從處理器,從處理器繼續(xù)從secondary_holding_pen開(kāi)始執(zhí)行(從處理器來(lái)到了內(nèi)核的世界),發(fā)現(xiàn)secondary_holding_pen_release不是自己的處理編號(hào),然后通過(guò)wfe繼續(xù)睡眠,當(dāng)主處理器完成了大多數(shù)的內(nèi)核組件的初始化之后,調(diào)用smp_init來(lái)來(lái)開(kāi)始真正的啟動(dòng)從處理器,最終調(diào)用spin-table 對(duì)應(yīng)的cpu操作集的cpu_boot方法從而在smp_spin_table_cpu_boot將需要啟動(dòng)的處理器的編號(hào)寫(xiě)入secondary_holding_pen_release中,然后再次sev指令喚醒從處理器,從處理器得以繼續(xù)執(zhí)行(設(shè)置自己異常向量表,初始化mmu等),最終在idle線程中執(zhí)行wfi睡眠。其他從處理器也是同樣的方式啟動(dòng)起來(lái),同樣最后進(jìn)入各種idle進(jìn)程執(zhí)行wfi睡眠,主處理器繼續(xù)往下進(jìn)行內(nèi)核初始化,直到啟動(dòng)init進(jìn)程,后面多個(gè)處理器都被啟動(dòng)起來(lái),都可以調(diào)度進(jìn)程,多進(jìn)程還會(huì)被均衡到多核。
