前言
看這篇文章,你必備的一些前置知識有如下
1、ATF啟動流程 2、PSCI電源管理的概念 3、設(shè)備樹
如果沒有,想要我發(fā)布什么方面的內(nèi)容?記得點贊、分享、評論三連。哈哈哈
昨天有個朋友在問多核啟動,于是今天就整理一篇多核啟動的文章,內(nèi)容大多數(shù)參考與網(wǎng)上前輩們的優(yōu)秀博客,感激。
1、SMP是什么?
SMP 英文為Symmetric Multi-Processing ,是對稱多處理結(jié)構(gòu)的簡稱,是指在一個計算機(jī)上匯集了一組處理器(多CPU),各CPU之間共享內(nèi)存子系統(tǒng)以及總線結(jié)構(gòu),一個服務(wù)器系統(tǒng)可以同時運行多個處理器,并共享內(nèi)存和其他的主機(jī)資源。
CMP 英文為Chip multiprocessors,指的是單芯片多處理器,也指多核心。其思想是將大規(guī)模并行處理器中的SMP集成到同一芯片內(nèi),各個處理器并行執(zhí)行不同的進(jìn)程。
(1)CPU數(shù):獨立的中央處理單元,體現(xiàn)在主板上就是有多少個CPU槽位
(2)CPU核心數(shù)(CPU cores):在每一個CPU上,都可能有多核(core),每個核中都有獨立的ALU,F(xiàn)PU,Cache等組件,可以理解為CPU的物理核數(shù)。(我們常說4核8線程中的核),指物理上存在的物體。
(3)CPU線程數(shù)(processor邏輯核):一種邏輯上的概念,并非真實存在的物體,只是為了更好地描述CPU的運作能力。簡單地說,就是模擬出的CPU核心數(shù)。
不過在這里我們這里指的是多個單核CPU組合到一起,每個核都有自己的一套寄存器。
一個系統(tǒng)存在多個CPU,成本會更高和管理也更困難。多核算是輕量級的SMP,物理上多核CPU還是封裝成一個CPU,但是在CPU內(nèi)部具有多個CPU的核心部件,可以同時運行多個線程/進(jìn)程。但是需要CPU核心之間要共享資源,比如緩存。
對程序員來說,它們之間的區(qū)別很小,大多數(shù)情況可以不做區(qū)分。我們在嵌入式開發(fā)中,大部分都是用的多核CPU。
這里我們就把這個SMP啟動轉(zhuǎn)換成多核CPU啟動。
2、啟動方式
程序為何可以在多個cpu上并發(fā)執(zhí)行:他們有各自獨立的一套寄存器,如:程序計數(shù)器pc,棧指針寄存器sp,通用寄存器等,可以獨自 取指、譯碼、執(zhí)行,當(dāng)然內(nèi)存和外設(shè)資源是共享的,多核環(huán)境下當(dāng)訪問臨界區(qū) 資源一般 自旋鎖來防止競態(tài)發(fā)生。
soc啟動的一般會從片內(nèi)的rom, 也叫bootrom開始執(zhí)行第一條指令,這個地址是系統(tǒng)默認(rèn)的啟動地址,會在bootrom中由芯片廠家固化一段啟動代碼來加載啟動bootloader到片內(nèi)的sram,啟動完成后的bootloader除了做一些硬件初始化之外做的最重要的事情是初始化ddr,因為sram的空間比較小所以需要初始化擁有大內(nèi)存 ddr,最后會從網(wǎng)絡(luò)/usb下載 或從存儲設(shè)備分區(qū)上加載內(nèi)核到ddr某個地址,為內(nèi)核傳遞參數(shù)之后,然后bootloader就完成了它的使命,跳轉(zhuǎn)到內(nèi)核,就進(jìn)入了操作系統(tǒng)內(nèi)核的世界。
bootloader將系統(tǒng)的控制權(quán)交給內(nèi)核之后,他首先會進(jìn)行處理器架構(gòu)相關(guān)初始化部分,如設(shè)置異常向量表,初始化mmu(之后內(nèi)核就從物理地址空間進(jìn)入了虛擬地址空間的世界,一切是那么的虛無縹緲,又是那么的恰到好處)等等,然后會清bss段,設(shè)置sp之后跳轉(zhuǎn)到C語言部分進(jìn)行更加復(fù)雜通用的初始化,其中會進(jìn)行內(nèi)存方面的初始化,調(diào)度器初始化,文件系統(tǒng)等內(nèi)核基礎(chǔ)組件 初始化工作,隨后會進(jìn)行關(guān)鍵的從處理器的引導(dǎo)過程,然后是各種實質(zhì)性的設(shè)備驅(qū)動的初始化,最后 創(chuàng)建系統(tǒng)的第一個用戶進(jìn)程init后進(jìn)入用戶空間執(zhí)行用戶進(jìn)程宣誓內(nèi)核初始化完成,可以進(jìn)程正常的調(diào)度執(zhí)行。
系統(tǒng)初始化階段大多數(shù)都是主處理器做初始化工作,所有不用考慮處理器并發(fā)情況,一旦從處理器被bingup起來,調(diào)度器和各自的運行隊列準(zhǔn)備就緒,多個任務(wù)就會均衡到各個處理器,開始了并發(fā)的世界,一切是那么的神奇。
soc在啟動階段除了一些特殊情況外(如為了加快啟動速度,在bl2階段通過并行加載方式同時加載bl31、bl32和bl33鏡像),一般都沒有并行化需求。因此只需要一個cpu執(zhí)行啟動流程即可,這個cpu被稱為primary cpu,而其它的cpu則統(tǒng)一被稱為secondary cpu。為了防止secondary cpu在啟動階段的執(zhí)行,它們在啟動時必須要被設(shè)置為一個特定的狀態(tài)。(有時候為了增加啟動速度,必須對時間敏感的設(shè)備,就可能啟動的時候整個從核并行跑一些任務(wù))
當(dāng)primary cpu完成操作系統(tǒng)初始化,調(diào)度系統(tǒng)開始工作后,就可以通過一定的機(jī)制啟動secondary cpu。顯然secondary cpu不再需要執(zhí)行啟動流程代碼,而只需直接跳轉(zhuǎn)到內(nèi)核中執(zhí)行即可。
主流程啟動初始化一般來說都是主核在干的,當(dāng)系統(tǒng)完成了初始化后就開始啟動從核。 這就像在啟動的大門,只有主核讓你過了,其他的先在門外等著。當(dāng)cpu0啟動到kernel后,就會去門口,把它們的門禁卡給它們,卡上就寫的它們的目的地班級是哪里。如果沒有這個門禁卡的cpu,說明地址為0,就繼續(xù)在原地等著。
故其啟動的關(guān)鍵是如何將內(nèi)核入口地址告知secondary cpu,以使其能跳轉(zhuǎn)到正確的執(zhí)行位置。
aarch64架構(gòu)實現(xiàn)了兩種不同的啟動方式,spin-table和psci。
其中spin-table方式非常簡單,但其只能被用于secondary cpu啟動,功能比較單一。
隨著aarch64架構(gòu)電源管理需求的增加(如cpu熱插拔、cpu idle等),arm設(shè)計了一套標(biāo)準(zhǔn)的電源管理接口協(xié)議psci。該協(xié)議可以支持所有cpu相關(guān)的電源管理接口,而且由于電源相關(guān)操作是系統(tǒng)的關(guān)鍵功能,為了防止其被攻擊,該協(xié)議將底層相關(guān)的實現(xiàn)都放到了secure空間,從而可提高系統(tǒng)的安全性。
2.1 spin-table
spin-table啟動流程的示意圖如下:
在這里插入圖片描述
芯片上電后primary cpu開始執(zhí)行啟動流程,而secondary cpu則將自身設(shè)置為WFE睡眠狀態(tài),并且為內(nèi)核準(zhǔn)備了一塊內(nèi)存,用于填寫secondary cpu的入口地址。
uboot負(fù)責(zé)將這塊內(nèi)存的地址寫入devicetree中,當(dāng)內(nèi)核初始化完成,需要啟動secondary cpu時,就將其內(nèi)核入口地址寫到那塊內(nèi)存中,然后喚醒cpu。
secondary cpu被喚醒后,檢查該內(nèi)存的內(nèi)容,確認(rèn)內(nèi)核已經(jīng)向其寫入了啟動地址,就跳轉(zhuǎn)到該地址執(zhí)行啟動流程。
2.1.1 secondary cpu初始化狀態(tài)設(shè)置
uboot啟動時,secondary cpu會通過以下流程進(jìn)入wfe狀態(tài)(arch/arm/cpu/armv8/start.S):
#ifdefined(CONFIG_ARMV8_SPIN_TABLE)&&!defined(CONFIG_SPL_BUILD) branch_if_masterx0,x1,master_cpu(1) bspin_table_secondary_jump(2) … master_cpu:(3) bl_main
(1)若當(dāng)前cpu為primary cpu,則跳轉(zhuǎn)到step 3,繼續(xù)執(zhí)行啟動流程。其中cpu id是通過mpidr區(qū)分的,而啟動流程中哪個cpu作為primary cpu可以任意指定。當(dāng)指定完成后,此處就可以根據(jù)其身份確定相應(yīng)的執(zhí)行流程
(2)若當(dāng)前cpu為slave cpu,則執(zhí)行spin流程。它是由spin_table_secondary_jump函數(shù)實現(xiàn)的(arch/arm/cpu/armv8/start.S)。以下為其代碼實現(xiàn):
ENTRY(spin_table_secondary_jump) .globlspin_table_reserve_begin spin_table_reserve_begin: 0:wfe(1) ldrx0,spin_table_cpu_release_addr(2) cbzx0,0b(3) brx0(4) .globlspin_table_cpu_release_addr(5) .align3 spin_table_cpu_release_addr: .quad0 .globlspin_table_reserve_end spin_table_reserve_end: ENDPROC(spin_table_secondary_jump)
(1)secondary cpu當(dāng)前沒有事情要做,因此執(zhí)行wfe指令進(jìn)入睡眠模式,以降低功耗
(2)spin_table_cpu_release_addr將由uboot傳遞給內(nèi)核,根據(jù)step 5的定義可知,其長度為8個字節(jié),在64位系統(tǒng)中正好可以保存一個指針。而它的內(nèi)容在啟動時會被初始化為0,當(dāng)內(nèi)核初始化完成后,在啟動secondary cpu之前,會在uboot中將其入口地址寫到該位置,并喚醒它
(3)當(dāng)secondary cpu從wfe狀態(tài)喚醒后,會校驗內(nèi)核是否在spin_table_cpu_release_addr處填寫了它的啟動入口。若未填寫,則其會繼續(xù)進(jìn)入wfe狀態(tài)
(4)若內(nèi)核填入了啟動地址,則其直接跳轉(zhuǎn)到該地址開始執(zhí)行內(nèi)核初始化流程
2.1.2 spin_table_cpu_release_addr的傳遞
由于在armv8架構(gòu)下,uboot只能通過devicetree向內(nèi)核傳遞參數(shù)信息,因此當(dāng)其開啟了CONFIG_ARMV8_SPIN_TABLE配置選項后,就需要在適當(dāng)?shù)臅r候?qū)⒃撝祵懭雂evicetree中。
我們知道uboot一般通過bootm命令啟動操作系統(tǒng)(aarch64支持的booti命令,其底層實現(xiàn)與bootm相同),因此在bootm中會執(zhí)行一系列啟動前的準(zhǔn)備工作,其中就包括將spin-table地寫入devicetree的工作。以下其執(zhí)行流程圖:
在這里插入圖片描述
spin_table_update_dt的代碼實現(xiàn)如下:
intspin_table_update_dt(void*fdt) { … unsignedlongrsv_addr=(unsignedlong)&spin_table_reserve_begin; unsignedlongrsv_size=&spin_table_reserve_end- &spin_table_reserve_begin;(1) cpus_offset=fdt_path_offset(fdt,"/cpus");(2) if(cpus_offset0) ????????return?-ENODEV; ????for?(offset?=?fdt_first_subnode(fdt,?cpus_offset);???????????????????? ?????????offset?>=0; offset=fdt_next_subnode(fdt,offset)){ prop=fdt_getprop(fdt,offset,"device_type",NULL); if(!prop||strcmp(prop,"cpu")) continue; prop=fdt_getprop(fdt,offset,"enable-method",NULL);(3) if(!prop||strcmp(prop,"spin-table")) return0; } for(offset=fdt_first_subnode(fdt,cpus_offset); offset>=0; offset=fdt_next_subnode(fdt,offset)){ prop=fdt_getprop(fdt,offset,"device_type",NULL); if(!prop||strcmp(prop,"cpu")) continue; ret=fdt_setprop_u64(fdt,offset,"cpu-release-addr", (unsignedlong)&spin_table_cpu_release_addr);(4) if(ret) return-ENOSPC; } ret=fdt_add_mem_rsv(fdt,rsv_addr,rsv_size);(5) … }
(1)獲取其起始地址和長度
(2)從devicetree中獲取cpus節(jié)點
(3)遍歷該節(jié)點的所有cpu子節(jié)點,并校驗其enable-method是否為spin-table。若不是所有cpu的都該類型,則不設(shè)置
(4)若所有cpu的enable-method都為spin-table,則將該參數(shù)設(shè)置到cpu-release-addr屬性中
(5)由于這段地址有特殊用途,內(nèi)核的內(nèi)存管理系統(tǒng)不能將其分配給其它模塊。因此,需要將其添加到保留內(nèi)存中
2.1.3 啟動secondary cpu
內(nèi)核在啟動secondary cpu之前當(dāng)然需要為其準(zhǔn)備好執(zhí)行環(huán)境,因為內(nèi)核中cpu最終都將由調(diào)度器管理,故此時調(diào)度子系統(tǒng)應(yīng)該要初始化完成。
同時cpu啟動完成轉(zhuǎn)交給調(diào)度器之前,并沒有實際的業(yè)務(wù)進(jìn)程,而我們知道內(nèi)核中cpu在空閑時會執(zhí)行idle進(jìn)程。因此,在其啟動之前需要為每個cpu初始化一個idle進(jìn)程。
另外,由于將一個cpu通過熱插拔方式移除后,再次啟動該cpu的流程,與secondary cpu的啟動流程是相同的,因此內(nèi)核復(fù)用了cpu hotplug框架用于啟動secondary cpu。
而內(nèi)核為每個cpu都分配了一個獨立的hotplug線程,用于執(zhí)行本cpu相關(guān)的熱插拔流程。為此,內(nèi)核通過以下流程執(zhí)行secondary cpu啟動操作:
在這里插入圖片描述
idle進(jìn)程初始化
以下代碼為每個非boot cpu分配一個idle進(jìn)程
void__initidle_threads_init(void) { … boot_cpu=smp_processor_id(); for_each_possible_cpu(cpu){(1) if(cpu!=boot_cpu) idle_init(cpu);(2) } }
(1)遍歷系統(tǒng)中所有的possible cpu
(2)若該cpu為secondary cpu,則為其初始化一個idle進(jìn)程
hotplug線程初始化
以下代碼為每個cpu初始化一個hotplug線程
void__initcpuhp_threads_init(void) { BUG_ON(smpboot_register_percpu_thread(&cpuhp_threads)); kthread_unpark(this_cpu_read(cpuhp_state.thread)); }
其中線程的描述結(jié)構(gòu)體定義如下:
staticstructsmp_hotplug_threadcpuhp_threads={ .store=&cpuhp_state.thread,(1) .create=&cpuhp_create,(2) .thread_should_run=cpuhp_should_run,(3) .thread_fn=cpuhp_thread_fun,(4) .thread_comm="cpuhp/%u",(5) .selfparking=true,(6) }
(1)用于保存cpu上的task struct指針
(2)線程創(chuàng)建時調(diào)用的回調(diào)
(3)該回調(diào)用于獲取線程是否需要退出標(biāo)志
(4)cpu hotplug主函數(shù),執(zhí)行實際的hotplug操作
(5)該線程的線程名
(6)用于設(shè)置線程創(chuàng)建完成后,是否將其設(shè)置為park狀態(tài)
hotplug回調(diào)線程喚醒
內(nèi)核使用以下流程喚醒特定cpu的hotplug線程,用于執(zhí)行實際的cpu啟動流程:
在這里插入圖片描述
由于cpu啟動時需要與一系列模塊交互以執(zhí)行相應(yīng)的準(zhǔn)備工作,為此內(nèi)核為其定義了一組hotplug狀態(tài),用于表示cpu在啟動或關(guān)閉時分別需要執(zhí)行的流程。以下為個階段狀態(tài)定義示例(由于該數(shù)組較長,故只截了一小段):
staticstructcpuhp_stepcpuhp_hp_states[]={ [CPUHP_OFFLINE]={ .name="offline", .startup.single=NULL, .teardown.single=NULL, }, … [CPUHP_BRINGUP_CPU]={ .name="cpu:bringup", .startup.single=bringup_cpu, .teardown.single=finish_cpu, .cant_stop=true, } … [CPUHP_ONLINE]={ .name="online", .startup.single=NULL, .teardown.single=NULL, }, }
以上每個階段都可包含startup.single和teardown.single兩個回調(diào)函數(shù),分別表示cpu啟動和關(guān)閉時需要執(zhí)行的流程。其中在cpu啟動時,將會從CPUHP_OFFLINE狀態(tài)開始,依次執(zhí)行各個階段的startup.single回調(diào)函數(shù)。其中CPUHP_BRINGUP_CPU及之前的階段都在secondary cpu啟動之前執(zhí)行。
而CPUHP_BRINGUP_CPU階段的回調(diào)函數(shù)bringup_cpu,會實際觸發(fā)secondary cpu的啟動流程。它將通過cpu_ops接口調(diào)用spin-table函數(shù),啟動secondary cpu,并等待其啟動完成。
當(dāng)secondary cpu啟動完成后,將喚醒hotplug線程,其將繼續(xù)執(zhí)行CPUHP_BRINGUP_CPU之后階段相關(guān)的回調(diào)函數(shù)。
cpu操作函數(shù)
cpu_ops函數(shù)由bringup_cpu調(diào)用,以觸發(fā)secondary cpu啟動。它是根據(jù)設(shè)備樹中解析出的enable-method屬性確定的。
int__initinit_cpu_ops(intcpu) { constchar*enable_method=cpu_read_enable_method(cpu);(1) … cpu_ops[cpu]=cpu_get_ops(enable_method);(2) … }
(1)獲取該cpu enable-method屬性的值
(2)根據(jù)其enable-method獲取其對應(yīng)的cpu_ops回調(diào)
其中spin-table啟動方式的回調(diào)如下:
conststructcpu_operationssmp_spin_table_ops={ .name="spin-table", .cpu_init=smp_spin_table_cpu_init, .cpu_prepare=smp_spin_table_cpu_prepare, .cpu_boot=smp_spin_table_cpu_boot, }
觸發(fā)secondary cpu啟動
以上流程都準(zhǔn)備完成后,觸發(fā)secondary cpu啟動就非常簡單了。只需調(diào)用其cpu_ops回調(diào)函數(shù),向其對應(yīng)的spin_table_cpu_release_addr位置寫入secondary cpu入口地址即可。以下為其調(diào)用流程:
在這里插入圖片描述
其中smp_spin_table_cpu_boot的實現(xiàn)如下:
staticintsmp_spin_table_cpu_boot(unsignedintcpu) { write_pen_release(cpu_logical_map(cpu));(1) sev();(2) return0; }
(1)向給定地址寫入內(nèi)核entry
(2)通過sev指令喚醒secondary cpu啟動
此后,該線程將等待cpu啟動完成,并在完成后將其設(shè)置為online狀態(tài)
secondary cpu執(zhí)行流程
aarch64架構(gòu)secondary cpu的內(nèi)核入口函數(shù)為secondary_entry(arch/arm64/kernel/head.S),以下為其執(zhí)行主流程:
在這里插入圖片描述
由于其底層相關(guān)初始化流程與primary cpu類似,因此此處不再介紹。我們這里主要看一下它是如何通過secondary_start_kernel啟動idle線程的:
asmlinkagenotracevoidsecondary_start_kernel(void) { structmm_struct*mm=&init_mm; … current->active_mm=mm;(1) cpu_uninstall_idmap();(2) … ops=get_cpu_ops(cpu); if(ops->cpu_postboot) ops->cpu_postboot();(3) … set_cpu_online(cpu,true);(4) complete(&cpu_running);(5) … cpu_startup_entry(CPUHP_AP_ONLINE_IDLE);(6) }
(1)由于內(nèi)核線程并沒有用于地址空間,因此其active_mm通常指向上一個用戶進(jìn)程的地址空間。而cpu初始化時,由于之前并沒有運行過用戶進(jìn)程,因此將其初始化為init_mm
(2)idmap地址映射僅僅是用于mmu使能時地址空間的平滑切換,在mmu使能完成后已經(jīng)沒有作用。更進(jìn)一步,由于idmap頁表所使用的ttbr0_elx頁表基地址寄存器,正常情況下是用于用戶空間頁表的,在調(diào)度器接管該cpu之前也必須要將其歸還給用戶空間
(3)執(zhí)行cpu_postboot回調(diào)
(4)由secondary cpu已經(jīng)啟動成功,故將其設(shè)置為online狀態(tài)
(5)喚醒cpu hotplug線程
(6)讓cpu執(zhí)行idle線程,其代碼實現(xiàn)如下:
voidcpu_startup_entry(enumcpuhp_statestate) { arch_cpu_idle_prepare(); cpuhp_online_idle(state); while(1) do_idle(); }
至此,cpu已經(jīng)啟動完成,并開始執(zhí)行idle線程了。最后當(dāng)然是要通知調(diào)度器,將該cpu的管理權(quán)限移交給調(diào)度器了。它是通過cpu hotplug的以下回調(diào)實現(xiàn)的:
staticstructcpuhp_stepcpuhp_hp_states[]={ … [CPUHP_AP_SCHED_STARTING]={ .name="sched:starting", .startup.single=sched_cpu_starting, .teardown.single=sched_cpu_dying, } … }
以下為該函數(shù)的實現(xiàn):
intsched_cpu_starting(unsignedintcpu) { … sched_rq_cpu_starting(cpu);(1) sched_tick_start(cpu);(2) … }
(1)用于初始化負(fù)載均衡相關(guān)參數(shù),此后該cpu就可以在其后的負(fù)載均衡流程中拉取進(jìn)程
(2)tick時鐘是內(nèi)核調(diào)度器的脈搏,啟動了該時鐘之后,cpu就會在時鐘中斷中執(zhí)行調(diào)度操作,從而讓cpu參與到系統(tǒng)的調(diào)度流程中
到這里我們就知道了spin-table這個流程。不得不說前輩對這個邏輯理解很清楚,這個內(nèi)容的參考鏈接在文末,歡迎大家點擊原文鏈接點贊。
小結(jié)
整個圖來看看
在這里插入圖片描述
最后這里補(bǔ)充一下一個使用自旋表作為啟動方式的平臺設(shè)備樹cpu節(jié)點:
arch/arm64/boot/dts/xxx.dtsi: cpu@0{ device_type="cpu"; compatible="arm,armv8"; reg=<0x0?0x000>; enable-method="spin-table"; cpu-release-addr=<0x1?0x0000fff8>; };
spin-table方式的多核啟動方式,顧名思義在于自旋,主處理器和從處理器上電都會啟動,主處理器執(zhí)行uboot暢通無阻,從處理器在spin_table_secondary_jump處wfe睡眠,主處理器通過修改設(shè)備樹的cpu節(jié)點的cpu-release-addr屬性為spin_table_cpu_release_addr,這是從處理器的釋放地址所在的地方。
主處理器進(jìn)入內(nèi)核后,會通過smp_prepare_cpus函數(shù)調(diào)用spin-table 對應(yīng)的cpu操作集的cpu_prepare方法從而在smp_spin_table_cpu_prepare函數(shù)中設(shè)置從處理器的釋放地址為secondary_holding_pen這個內(nèi)核函數(shù),然后通過sev指令喚醒從處理器,從處理器繼續(xù)從secondary_holding_pen開始執(zhí)行(從處理器來到了內(nèi)核的世界),發(fā)現(xiàn)secondary_holding_pen_release不是自己的處理編號,然后通過wfe繼續(xù)睡眠。
當(dāng)主處理器完成了大多數(shù)的內(nèi)核組件的初始化之后,調(diào)用smp_init來來開始真正的啟動從處理器,最終調(diào)用spin-table 對應(yīng)的cpu操作集的cpu_boot方法從而在smp_spin_table_cpu_boot將需要啟動的處理器的編號寫入secondary_holding_pen_release中,然后再次sev指令喚醒從處理器,從處理器得以繼續(xù)執(zhí)行(設(shè)置自己異常向量表,初始化mmu等)。
最終在idle線程中執(zhí)行wfi睡眠。其他從處理器也是同樣的方式啟動起來,同樣最后進(jìn)入各種idle進(jìn)程執(zhí)行wfi睡眠,主處理器繼續(xù)往下進(jìn)行內(nèi)核初始化,直到啟動init進(jìn)程,后面多個處理器都被啟動起來,都可以調(diào)度進(jìn)程,多進(jìn)程還會被均衡到多核。
下面介紹另外一種啟動 PSCI。
2.2 psci
psci是arm提供的一套電源管理接口,當(dāng)前一共包含0.1、0.2和1.0三個版本。它可被用于以下場景:(1)cpu的idle管理
(2)cpu hotplug以及secondary cpu啟動
(3)系統(tǒng)shutdown和reset
首先,我們先來看下設(shè)備樹cpu節(jié)點對psci的支持:
arch/arm64/boot/dts/xxx.dtsi: cpu0:cpu@0{ device_type="cpu"; compatible="arm,armv8"; reg=<0x0>; enable-method="psci"; }; psci{ compatible="arm,psci"; method="smc"; cpu_suspend=<0xC4000001>; cpu_off=<0x84000002>; cpu_on=<0xC4000003>; };
psci節(jié)點的詳細(xì)說明可以參考內(nèi)核文檔:Documentation/devicetree/bindings/arm/psci.txt
從這個我們可以獲得什么信息呢?
可以看到現(xiàn)在enable-method 屬性已經(jīng)是psci,說明使用的多核啟動方式是psci,
下面還有psci節(jié)點,用于psci驅(qū)動使用,method用于說明調(diào)用psci功能使用什么指令,可選有兩個smc和hvc。
其實smc, hvc和svc都是從低運行級別向高運行級別請求服務(wù)的指令,我們最常用的就是svc指令了,這是實現(xiàn)系統(tǒng)調(diào)用的指令。
高級別的運行級別會根據(jù)傳遞過來的參數(shù)來決定提供什么樣的服務(wù)。
smc是用于陷入el3(安全),
hvc用于陷入el2(虛擬化, 虛擬化場景中一般通過hvc指令陷入el2來請求喚醒vcpu), svc用于陷入el1(系統(tǒng))。
這里只講解smc陷入el3啟動多核的情況。
2.2.1 psci 基礎(chǔ)概念知識
下面我們將按照電源管理拓?fù)浣Y(jié)構(gòu)(power domain)、電源狀態(tài)(power state)以及armv8安全擴(kuò)展幾個方面介紹psci的一些基礎(chǔ)知識
2.2.1.1power domain
我們前面已經(jīng)介紹過cpu的拓?fù)浣Y(jié)構(gòu),如aarch64架構(gòu)下每塊soc可能會包含多個cluster,而每個cluster又包含多個core,它們共同組成了層次化的拓?fù)浣Y(jié)構(gòu)。如以下為一塊包含2個cluster,每個cluster包含四個core的soc:
在這里插入圖片描述
由于其中每個core以及每個cluster的電源都可以獨立地執(zhí)行開關(guān)操作,因此若core0 – core3的電源都關(guān)閉了,則cluster 0的電源也可以被關(guān)閉以降低功耗。
若core0 – core3中的任一個core需要上電,則顯然cluster 0需要先上電。為了更好地進(jìn)行層次化電源管理,psci在電源管理流程中將以上這些組件都抽象為power domain。如以下為上例的power domain層次結(jié)構(gòu):
在這里插入圖片描述
其中system level用于管理整個系統(tǒng)的電源,cluster level用于管理某個特定cluster的電源,而core level用于管理一個單獨core的電源。
2.2.1.2power state
由于aarch64架構(gòu)有多種不用的電源狀態(tài),不同電源狀態(tài)的功耗和喚醒延遲不同。
如standby狀態(tài)會關(guān)閉power domain的clock,但并不關(guān)閉電源。因此它雖然消除了門電路翻轉(zhuǎn)引起的動態(tài)功耗,但依然存在漏電流等引起的靜態(tài)功耗。故其功耗相對較大,但相應(yīng)地喚醒延遲就比較低。
而對于power down狀態(tài),會斷開對應(yīng)power domain的電源,因此其不僅消除了動態(tài)功耗,還消除了靜態(tài)功耗,相應(yīng)地其喚醒延遲就比較高了。
psci一共為power domain定義了四種power state:
?(1)run:電源和時鐘都打開,該domain正常工作
?(2)standby:關(guān)閉時鐘,但電源處于打開狀態(tài)。其寄存器狀態(tài)得到保存,打開時鐘后就可繼續(xù)運行。功耗相對較大,但喚醒延遲較低。arm執(zhí)行wfi或wfe指令會進(jìn)入該狀態(tài)。
?(3)retention:它將core的狀態(tài),包括調(diào)試設(shè)置都保存在低功耗結(jié)構(gòu)中,并使其部分關(guān)閉。其狀態(tài)在從低功耗變?yōu)檫\行時能自動恢復(fù)。從操作系統(tǒng)角度看,除了進(jìn)入方法、延遲等有區(qū)別外,其它都與standby相同。它的功耗和喚醒延遲都介于standby和power down之間。
?(4)power down:關(guān)閉時鐘和電源。power domain掉電后,所有狀態(tài)都丟失,上電以后軟件必須重新恢復(fù)其狀態(tài)。它的功耗最低,但喚醒延遲也相應(yīng)地最高。
(這里我很好奇怎么和linux的s3、s4對應(yīng)的。當(dāng)時測試s3的時候,對應(yīng)的是suspend。這里的對于cpu的有off、on、suspend三種,我覺得這里應(yīng)該就是對于的standby,因為有wfi或wfe這些指令。那s4就是CPU off了?可以看一下這個有點認(rèn)識,突然想到psci里面的狀態(tài)是對于的cpu為對象,但是linux的電源管理應(yīng)該是對整個設(shè)備。)
可以看一下這個文章:s1s2s3S4S5的含義待機(jī)、休眠、睡眠的區(qū)別
顯然,power state的睡眠程度從run到power down逐步加深。而高層級power domain的power state不應(yīng)低于低層級power domain。
如以上例子中core 0 – core 2都為power down狀態(tài),而core 3為standby狀態(tài),則cluster 0不能為retention或power down狀態(tài)。同樣若cluster 0為standby狀態(tài),而cluster 1為run狀態(tài),則整個系統(tǒng)必須為run狀態(tài)。
為了達(dá)到上述約束,不同power domain之間的power state具有以下關(guān)系:
在這里插入圖片描述
這里解釋了psci那個源碼文檔里電源樹的概念。
psci實現(xiàn)了父leve與子level之間的電源關(guān)系協(xié)調(diào),如cluster 0中最后一個core被設(shè)置為power down狀態(tài)后,psci就會將該cluster也設(shè)置為power donw狀態(tài)。若其某一個core被設(shè)置為run狀態(tài),則psci會先將其對應(yīng)cluster的狀態(tài)設(shè)置為run,然后再設(shè)置對應(yīng)core的電源狀態(tài),這也是psci名字的由來(power state coordinate interface)
2.2.1.3armv8的安全擴(kuò)展
為了增強(qiáng)arm架構(gòu)的安全性,aarch64一共實現(xiàn)了secure和non-secure兩種安全狀態(tài)。通過一系列硬件擴(kuò)展,在cpu執(zhí)行狀態(tài)、總線、內(nèi)存、外設(shè)、中斷、tlb、cache等方面都實現(xiàn)了兩種狀態(tài)之間的隔離。
在這種機(jī)制下,secure空間的程序可以訪問所有secure和non-secure的資源,而non-secure空間的程序只能訪問non-secure資源,卻不能訪問secure資源。從而可以將一些安全關(guān)鍵的資源放到secure空間,以增強(qiáng)其安全性。
為此aarch64實現(xiàn)了4個異常等級,其中EL3工作在secure空間,而EL0 – EL2既可以工作于secure空間,又可以工作于non-secure空間。不同異常等級及不同secure狀態(tài)的模式下可運行不同類型軟件。
如secure EL1和El0用于運行trust os內(nèi)核及其用戶態(tài)程序,non-secure EL1和El0用于運行普通操作系統(tǒng)內(nèi)核(如linux)及其用戶態(tài)程序,EL2用于運行虛擬機(jī)的hypervisor。而EL3運行secure monitor程序(通常為bl31),其功能為執(zhí)行secure和non secure狀態(tài)切換、消息轉(zhuǎn)發(fā)以及提供類似psci等secure空間服務(wù)。以下為其示意圖:
在這里插入圖片描述
psci是工作于non secure EL1(linux內(nèi)核)和EL3(bl31)之間的一組電源管理接口,其目的是讓linux實現(xiàn)具體的電源管理策略,而由bl31管理底層硬件相關(guān)的操作。從而將cpu電源控制這種影響系統(tǒng)安全的控制權(quán)限放到安全等級更高的層級中,從而提升系統(tǒng)的整體安全性。
那么psci如何從EL1調(diào)用EL3的服務(wù)呢?其實它和系統(tǒng)調(diào)用是類似的,只是系統(tǒng)調(diào)用是用戶態(tài)程序陷入操作系統(tǒng)內(nèi)核,而psci是從操作系統(tǒng)內(nèi)核陷入secure monitor。armv8提供了一條smc異常指令,內(nèi)核只需要提供合適的參數(shù)后,觸發(fā)該指令即可通過異常的方式進(jìn)入secure monitor。(SMC調(diào)用,這個在ATF專欄有介紹)
2.2.2 psci軟件架構(gòu)
由于psci是由linux內(nèi)核調(diào)用bl31中的安全服務(wù),實現(xiàn)cpu電源管理功能的。因此其軟件架構(gòu)包含三個部分:(1)內(nèi)核與bl31之間的調(diào)用接口規(guī)范
(2)內(nèi)核中的架構(gòu)
(3)bl31中的架構(gòu)
2.2.3psci接口規(guī)范
psci規(guī)定了linux內(nèi)核調(diào)用bl31中電源管理相關(guān)服務(wù)的接口規(guī)范,它包含實現(xiàn)以下功能所需的接口:(1)cpu idle管理
(2)向系統(tǒng)動態(tài)添加或從系統(tǒng)動態(tài)移除cpu,通常稱為hotplug
(3)secondary cpu啟動
(4)系統(tǒng)的shutdown和reset
psci接口規(guī)定了命令對應(yīng)的function_id、接口的輸入?yún)?shù)以及返回值。其中輸入?yún)?shù)可通過x0 – x7寄存器傳遞,而返回值通過x0 – x4寄存器傳遞。
如secondary cpu啟動或cpu hotplug時可調(diào)用cpu_on接口,為一個cpu執(zhí)行上電操作。該接口的格式如下:(1)function_id:0xc400 0003
(2)輸入?yún)?shù):使用mpidr值表示的target cpu id
cpu啟動入口的物理地址
context id,該值用于表示本次調(diào)用上下文相關(guān)的信息
(3)返回值:可以為success、invalid_parameter、invalid_address、already_on、on_pending或internal_failure
有了以下這些接口的詳細(xì)定義,內(nèi)核和bl31就只需按照該接口的規(guī)定,獨立開發(fā)psci相關(guān)功能。從而避免了它們之間的耦合,簡化了開發(fā)復(fù)雜度。
2.2.4內(nèi)核中的psci架構(gòu)
內(nèi)核psci軟件架構(gòu)包含psci驅(qū)動和每個cpu的cpu_ops回調(diào)函數(shù)實現(xiàn)兩部分。
其中psci驅(qū)動實現(xiàn)了驅(qū)動初始化和psci相關(guān)接口實現(xiàn)功能,而cpu_ops回調(diào)函數(shù)最終也會調(diào)用psci驅(qū)動的接口。
2.2.4.1psci驅(qū)動
首先我們看一下devicetree中的配置:
psci{ compatible="arm,psci-0.2";(1) method="smc";(2) }
(1)用于指定psci版本
(2)根據(jù)該psci由bl31處理還是hypervisor處理,可以指定其對應(yīng)的陷入方式。若由bl31處理為smc,若由hypervisor處理則為hvc
驅(qū)動流程主要是與bl31通信,以確認(rèn)其是否支持給定的psci版本,以及相關(guān)psci操作函數(shù)的實現(xiàn),其流程如下:
在這里插入圖片描述
其主要工作即為psci設(shè)置相關(guān)的回調(diào)函數(shù),該函數(shù)定義如下:
staticvoid__initpsci_0_2_set_functions(void) { … psci_ops=(structpsci_operations){ .get_version=psci_0_2_get_version, .cpu_suspend=psci_0_2_cpu_suspend, .cpu_off=psci_0_2_cpu_off, .cpu_on=psci_0_2_cpu_on, .migrate=psci_0_2_migrate, .affinity_info=psci_affinity_info, .migrate_info_type=psci_migrate_info_type, };(1) register_restart_handler(&psci_sys_reset_nb);(2) pm_power_off=psci_sys_poweroff;(3) }
(1)為psci_ops設(shè)置相應(yīng)的回調(diào)函數(shù)
(2)為psci模塊設(shè)置系統(tǒng)重啟時的通知函數(shù)
(3)將系統(tǒng)的power_off函數(shù)指向相應(yīng)的psci接口
2.2.4.2cpu_ops接口
驅(qū)動初始化完成后,cpu的cpu_ops就可以調(diào)用這些回調(diào)實現(xiàn)psci功能的調(diào)用。如下所示,當(dāng)devicetree中cpu的enable-method設(shè)置為psci時,該cpu的cpu_ops將指向cpu_psci_ops。
cpu0:cpu@0{ ... enable-method="psci"; … }
其中cpu_psci_ops的定義如下:
conststructcpu_operationscpu_psci_ops={ .name="psci", .cpu_init=cpu_psci_cpu_init, .cpu_prepare=cpu_psci_cpu_prepare, .cpu_boot=cpu_psci_cpu_boot, #ifdefCONFIG_HOTPLUG_CPU .cpu_can_disable=cpu_psci_cpu_can_disable, .cpu_disable=cpu_psci_cpu_disable, .cpu_die=cpu_psci_cpu_die, .cpu_kill=cpu_psci_cpu_kill, #endif }
如啟動cpu的接口為cpu_psci_cpu_boot,它會通過以下流程最終調(diào)用psci驅(qū)動中的psci_ops函數(shù):
staticintcpu_psci_cpu_boot(unsignedintcpu) { phys_addr_tpa_secondary_entry=__pa_symbol(function_nocfi(secondary_entry)); interr=psci_ops.cpu_on(cpu_logical_map(cpu),pa_secondary_entry); if(err) pr_err("failedtobootCPU%d(%d) ",cpu,err); returnerr; }
2.2.5bl31中的psci架構(gòu)
bl31為內(nèi)核提供了一系列運行時服務(wù),psci作為其標(biāo)準(zhǔn)運行時服務(wù)的一部分,通過宏DECLARE_RT_SVC注冊到系統(tǒng)中。其相應(yīng)的定義如下:
DECLARE_RT_SVC( std_svc, OEN_STD_START, OEN_STD_END, SMC_TYPE_FAST, std_svc_setup, std_svc_smc_handler )
其中std_svc_setup會在bl31啟動流程中被調(diào)用,以用于初始化該服務(wù)相關(guān)的配置。而std_svc_smc_handler為其smc異常處理函數(shù),當(dāng)內(nèi)核通過psci接口調(diào)用相關(guān)服務(wù)時,最終將由該函數(shù)執(zhí)行實際的處理流程。
在這里插入圖片描述
上圖為psci初始化相關(guān)的流程,它主要包含內(nèi)容:(1)前面我們已經(jīng)介紹過power domain相關(guān)的背景,即psci需要協(xié)調(diào)不同層級的power domain狀態(tài),因此其必須要了解系統(tǒng)的power domain配置情況。以上流程中紅色虛線框的部分主要就是用于初始化系統(tǒng)的power domain拓?fù)浼捌錉顟B(tài)
(2)由于psci在執(zhí)行電源相關(guān)接口時,最終需要操作實際的硬件。而它們是與架構(gòu)相關(guān)的,因此其操作函數(shù)最終需要注冊到平臺相關(guān)的回調(diào)中。plat_setup_psci_ops即用于注冊特定平臺的psci_ops回調(diào),其格式如下:
typedefstructplat_psci_ops{ void(*cpu_standby)(plat_local_state_tcpu_state); int(*pwr_domain_on)(u_register_tmpidr); void(*pwr_domain_off)(constpsci_power_state_t*target_state); void(*pwr_domain_suspend_pwrdown_early)( constpsci_power_state_t*target_state); void(*pwr_domain_suspend)(constpsci_power_state_t*target_state); void(*pwr_domain_on_finish)(constpsci_power_state_t*target_state); void(*pwr_domain_on_finish_late)( constpsci_power_state_t*target_state); void(*pwr_domain_suspend_finish)( constpsci_power_state_t*target_state); void__dead2(*pwr_domain_pwr_down_wfi)( constpsci_power_state_t*target_state); void__dead2(*system_off)(void); void__dead2(*system_reset)(void); int(*validate_power_state)(unsignedintpower_state, psci_power_state_t*req_state); int(*validate_ns_entrypoint)(uintptr_tns_entrypoint); void(*get_sys_suspend_power_state)( psci_power_state_t*req_state); int(*get_pwr_lvl_state_idx)(plat_local_state_tpwr_domain_state, intpwrlvl); int(*translate_power_state_by_mpidr)(u_register_tmpidr, unsignedintpower_state, psci_power_state_t*output_state); int(*get_node_hw_state)(u_register_tmpidr,unsignedintpower_level); int(*mem_protect_chk)(uintptr_tbase,u_register_tlength); int(*read_mem_protect)(int*val); int(*write_mem_protect)(intval); int(*system_reset2)(intis_vendor, intreset_type,u_register_tcookie); }
最后我們再看一下psci操作相應(yīng)的異常處理流程:
在這里插入圖片描述
即其會根據(jù)function id的值,分別執(zhí)行相應(yīng)的電源管理服務(wù),如啟動cpu時會調(diào)用psci_cpu_on函數(shù),重啟系統(tǒng)時會調(diào)用psci_system_rest函數(shù)等。
2.2.6 secondary cpu啟動
由于psci方式啟動secondary cpu的流程,除了其所執(zhí)行的cpu_ops不同之外,其它流程與spin-table方式是相同的,因此我們這里只給出執(zhí)行流程圖,詳細(xì)分析可以參考上篇博文。其中以下流程執(zhí)行secondary cpu啟動相關(guān)的一些初始化工作:
在這里插入圖片描述
在初始化完成且hotplug線程創(chuàng)建完成后,就可通過以下流程喚醒cpu hotplug線程:
在這里插入圖片描述
此后hotplug線程將調(diào)用psci回調(diào)函數(shù),并最終觸發(fā)smc異常進(jìn)入bl31:
在這里插入圖片描述
bl31接收到該異常后執(zhí)行std_svc_smc_handler處理函數(shù),并最終調(diào)用平臺相關(guān)的電源管理接口,完成cpu的上電工作,以下為其執(zhí)行流程:
在這里插入圖片描述
平臺相關(guān)回調(diào)函數(shù)pwr_domain_on將為secondary cpu設(shè)置入口函數(shù),然后為其上電使該cpu跳轉(zhuǎn)到內(nèi)核入口secondary_entry處開始執(zhí)行。以下為其內(nèi)核啟動流程:
在這里插入圖片描述
到這里其實就結(jié)束了,不得不說這個前輩的文章是真的寫的邏輯清晰,收獲頗多。
小結(jié)
最后結(jié)合代碼再走一遍
1、std_svc_setup(主要關(guān)注設(shè)置psci操作集)--有服務(wù)
std_svc_setup//services/std_svc/std_svc_setup.c ->psci_setup//lib/psci/psci_setup.c ->plat_setup_psci_ops//設(shè)置平臺的psci操作調(diào)用平臺的plat_setup_psci_ops函數(shù)去設(shè)置psci操作eg:qemu平臺 ->*psci_ops=&plat_qemu_psci_pm_ops; 208staticconstplat_psci_ops_tplat_qemu_psci_pm_ops={ 209.cpu_standby=qemu_cpu_standby, 210.pwr_domain_on=qemu_pwr_domain_on, 211.pwr_domain_off=qemu_pwr_domain_off, 212.pwr_domain_suspend=qemu_pwr_domain_suspend, 213.pwr_domain_on_finish=qemu_pwr_domain_on_finish, 214.pwr_domain_suspend_finish=qemu_pwr_domain_suspend_finish, 215.system_off=qemu_system_off, 216.system_reset=qemu_system_reset, 217.validate_power_state=qemu_validate_power_state, 218.validate_ns_entrypoint=qemu_validate_ns_entrypoint 219};
在遍歷每一個注冊的運行時服務(wù)的時候,會導(dǎo)致std_svc_setup調(diào)用,其中會做psci操作集的設(shè)置,操作集中我們可以看到對核電源的管理的接口如:核上電,下電,掛起等,我們主要關(guān)注上電 .pwr_domain_on = qemu_pwr_domain_on,這個接口當(dāng)我們主處理器boot從處理器的時候會用到。
2、運行時服務(wù)觸發(fā)和處理--來請求
smc指令觸發(fā)進(jìn)入el3異常向量表:
runtime_exceptions//el3的異常向量表 ->sync_exception_aarch64 ->handle_sync_exception ->smc_handler64 ->|*PopulatetheparametersfortheSMChandler. |*Wealreadyhavex0-x4inplace.x5willpointtoacookie(notused |*now).x6willpointtothecontextstructure(SP_EL3)andx7will |*containflagsweneedtopasstothehandlerHencesavex5-x7. |* |*Note:x4onlyneedstobepreservedforAArch32callersbutwedoit |*forAArch64callersaswellforconvenience |*/ stpx4,x5,[sp,#CTX_GPREGS_OFFSET+CTX_GPREG_X4]//保存x4-x7到棧 stpx6,x7,[sp,#CTX_GPREGS_OFFSET+CTX_GPREG_X6] /*Saverestofthegpregsandsp_el0*/ save_x18_to_x29_sp_el0 movx5,xzr//x5清零 movx6,sp//sp保存在x6 /*Gettheuniqueowningentitynumber*///獲得唯一的入口編號 ubfxx16,x0,#FUNCID_OEN_SHIFT,#FUNCID_OEN_WIDTH ubfxx15,x0,#FUNCID_TYPE_SHIFT,#FUNCID_TYPE_WIDTH orrx16,x16,x15,lsl#FUNCID_OEN_WIDTH adrx11,(__RT_SVC_DESCS_START__+RT_SVC_DESC_HANDLE) /*Loaddescriptorindexfromarrayofindices*/ adrx14,rt_svc_descs_indices//獲得服務(wù)描述標(biāo)識數(shù)組 ldrbw15,[x14,x16]//根據(jù)唯一的入口編號找到處理函數(shù)的地址 /* |*RestorethesavedCruntimestackvaluewhichwillbecomethenew |*SP_EL0i.e.EL3runtimestack.Itwassavedinthe'cpu_context' |*structurepriortothelastERETfromEL3. |*/ ldrx12,[x6,#CTX_EL3STATE_OFFSET+CTX_RUNTIME_SP] /* |*Anyindexgreaterthan127isinvalid.Checkbit7for |*avalidindex |*/ tbnzw15,7,smc_unknown /*SwitchtoSP_EL0*/ msrspsel,#0 /* |*Getthedescriptorusingtheindex |*x11=(base+off),x15=index |* |*handler=(base+off)+(index<
3、找到對應(yīng)handler--請求匹配處理函數(shù)
上面其實主要的是找到服務(wù)例程,然后跳轉(zhuǎn)執(zhí)行 下面是跳轉(zhuǎn)的處理函數(shù):
std_svc_smc_handler//services/std_svc/std_svc_setup.c ->ret=psci_smc_handler(smc_fid,x1,x2,x3,x4, |cookie,handle,flags) ... 480}else{ 481/*64-bitPSCIfunction*/ 482 483switch(smc_fid){ 484casePSCI_CPU_SUSPEND_AARCH64: 485ret=(u_register_t) 486psci_cpu_suspend((unsignedint)x1,x2,x3); 487break; 488 489casePSCI_CPU_ON_AARCH64: 490ret=(u_register_t)psci_cpu_on(x1,x2,x3); 491break; 492 ... }
4、處理函數(shù)干活
處理函數(shù)根據(jù)funid來決定服務(wù),可以看到PSCI_CPU_ON_AARCH64為0xc4000003,這正是設(shè)備樹中填寫的cpu_on屬性的id,會委托psci_cpu_on來執(zhí)行核上電任務(wù)。下面分析是重點:!?。?/p>
->psci_cpu_on()//lib/psci/psci_main.c ->psci_validate_entry_point()//驗證入口地址有效性并保存入口點到一個結(jié)構(gòu)ep中 ->psci_cpu_on_start(target_cpu,&ep)//ep入口地址 ->psci_plat_pm_ops->pwr_domain_on(target_cpu) ->qemu_pwr_domain_on//實現(xiàn)核上電(平臺實現(xiàn)) /*Storethere-entryinformationforthenon-secureworld.*/ ->cm_init_context_by_index()//重點:會通過cpu的編號找到cpu上下文(cpu_context_t),存在cpu寄存器的值,異常返回的時候?qū)憣懙綄?yīng)的寄存器中,然后eret,舊返回到了el1?。?! ->cm_setup_context()//設(shè)置cpu上下文 ->write_ctx_reg(state,CTX_SCR_EL3,scr_el3);//lib/el3_runtime/aarch64/context_mgmt.c write_ctx_reg(state, CTX_ELR_EL3, ep->pc);//注:異常返回時執(zhí)行此地址于是完成了cpu的啟動!?。?write_ctx_reg(state,CTX_SPSR_EL3,ep->spsr);
psci_cpu_on主要完成開核的工作,然后會設(shè)置一些異常返回后寄存器的值(eg:從el1 -> el3 -> el1),重點關(guān)注 ep->pc寫到cpu_context結(jié)構(gòu)的CTX_ELR_EL3偏移處(從處理器啟動后會從這個地址取指執(zhí)行)。
實際上,所有的從處理器啟動后都會從bl31_warm_entrypoint開始執(zhí)行,在plat_setup_psci_ops中會設(shè)置(每個平臺都有自己的啟動地址寄存器,通過寫這個寄存器來獲得上電后執(zhí)行的指令地址)。
大致說一下:主處理器通過smc進(jìn)入el3請求開核服務(wù),atf中會響應(yīng)這種請求,通過平臺的開核操作來啟動從處理器并且設(shè)置從處理的一些寄存器eg:scr_el3、spsr_el3、elr_el3,然后主處理器,恢復(fù)現(xiàn)場,eret再次回到el1,
而處理器開核之后會從bl31_warm_entrypoint開始執(zhí)行,最后通過el3_exit返回到el1的elr_el3設(shè)置的地址。
分析到這atf的分析到此為止,atf中主要是響應(yīng)內(nèi)核的snc的請求,然后做開核處理,也就是實際的開核動作,但是從處理器最后還是要回到內(nèi)核中執(zhí)行,下面分析內(nèi)核的處理:注意流程如下:
5、開核返回-EL1 啟動從處理器
init/main.c start_kernel ->boot_cpu_init//引導(dǎo)cpu初始化設(shè)置引導(dǎo)cpu的位掩碼onlineactivepresentpossible都為true ->setup_arch//arch/arm64/kernel/setup.c ->if(acpi_disabled)//不支持acpi psci_dt_init();//drivers/firmware/psci.c(psci主要文件)psci初始化解析設(shè)備樹尋找psci匹配的節(jié)點 else psci_acpi_init();//acpi中允許使用psci情況 ->rest_init ->kernel_init ->kernel_init_freeable ->smp_prepare_cpus//準(zhǔn)備cpu對于每個可能的cpu1.cpu_ops[cpu]->cpu_prepare(cpu)2.set_cpu_present(cpu,true)cpu處于present狀態(tài) ->do_pre_smp_initcalls//多核啟動之前的調(diào)用initcall回調(diào) ->smp_init//smp初始化kernel/smp.c會啟動其他從處理器
我們主要關(guān)注兩個函數(shù):psci_dt_init和smp_init psci_dt_init是解析設(shè)備樹,設(shè)置操作函數(shù),smp_init用于啟動從處理器。
->psci_dt_init()//drivers/firmware/psci.c: ->init_fn() ->psci_0_1_init()//設(shè)備樹中compatible="arm,psci"為例 ->get_set_conduit_method()//根據(jù)設(shè)備樹method屬性設(shè)置invoke_psci_fn=__invoke_psci_fn_smc;(method="smc") ->invoke_psci_fn=__invoke_psci_fn_smc ->if(!of_property_read_u32(np,"cpu_on",&id)){ 651psci_function_id[PSCI_FN_CPU_ON]=id; 652psci_ops.cpu_on=psci_cpu_on;//設(shè)置psci操作的開核接口 653} ->psci_cpu_on() ->invoke_psci_fn() ->__invoke_psci_fn_smc() ->arm_smccc_smc(function_id,arg0,arg1,arg2,0,0,0,0,&res)//這個時候x0=function_idx1=arg0,x2=arg1,x3arg2,... ->__arm_smccc_smc() ->SMCCCsmc//arch/arm64/kernel/smccc-call.S ->20.macroSMCCCinstr 21.cfi_startproc 22instr#0//即是smc#0陷入到el3 23ldrx4,[sp] 24stpx0,x1,[x4,#ARM_SMCCC_RES_X0_OFFS] 25stpx2,x3,[x4,#ARM_SMCCC_RES_X2_OFFS] 26ldrx4,[sp,#8] 27cbzx4,1f/*noquirkstructure*/ 28ldrx9,[x4,#ARM_SMCCC_QUIRK_ID_OFFS] 29cmpx9,#ARM_SMCCC_QUIRK_QCOM_A6 30b.ne1f 31strx6,[x4,ARM_SMCCC_QUIRK_STATE_OFFS] 321:ret 33.cfi_endproc 34.endm
最終通過22行陷入了el3中。(這是因為安全所以還需要到ATF中啟動)smp_init函數(shù)做從處理器啟動:
start_kernel ->arch_call_rest_init ->rest_init ->kernel_init, ->kernel_init_freeable ->smp_prepare_cpus//arch/arm64/kernel/smp.c ->smp_init//kernel/smp.c(這是從處理器啟動的函數(shù)) ->cpu_up ->do_cpu_up ->_cpu_up ->cpuhp_up_callbacks ->cpuhp_invoke_callback ->cpuhp_hp_states[CPUHP_BRINGUP_CPU] ->bringup_cpu ->__cpu_up//arch/arm64/kernel/smp.c ->boot_secondary ->cpu_ops[cpu]->cpu_boot(cpu) ->cpu_psci_ops.cpu_boot ->cpu_psci_cpu_boot//arch/arm64/kernel/psci.c 46staticintcpu_psci_cpu_boot(unsignedintcpu) 47{ 48interr=psci_ops.cpu_on(cpu_logical_map(cpu),__pa_symbol(secondary_entry)); 49if(err) 50pr_err("failedtobootCPU%d(%d) ",cpu,err); 51 52returnerr; 53}
啟動從處理的時候最終調(diào)用到psci的cpu操作集的cpu_psci_cpu_boot函數(shù),會調(diào)用上面的psci_cpu_on,最終調(diào)用smc,傳遞第一個參數(shù)為cpu的id標(biāo)識啟動哪個cpu,第二個參數(shù)為從處理器啟動后進(jìn)入內(nèi)核執(zhí)行的地址secondary_entry(這是個物理地址)。
所以綜上,最后smc調(diào)用時傳遞的參數(shù)為arm_smccc_smc(0xC4000003, cpuid, secondary_entry, arg2, 0, 0, 0, 0, &res)。這樣陷入el3之后,就可以啟動對應(yīng)的從處理器,最終從處理器回到內(nèi)核(el3->el1),執(zhí)行secondary_entry處指令,從處理器啟動完成。
可以發(fā)現(xiàn)psci的方式啟動從處理器的方式相當(dāng)復(fù)雜,這里面涉及到了el1到安全的el3的跳轉(zhuǎn),而且涉及到大量的函數(shù)回調(diào),很容易繞暈。
(其實為了安全,所以啟動從核開核這個操作必須在EL3,開了以后,就可以會EL1,因為已經(jīng)在EL3給你了準(zhǔn)確安全的啟動位置了。)
在這里插入圖片描述
6、從處理器啟動EL1做了什么?
其實這里就和spin-table比較相似了
無論是spin-table還是psci,從處理器啟動進(jìn)入內(nèi)核之后都會執(zhí)行secondary_startup:
719secondary_startup: 720/* 721|*CommonentrypointforsecondaryCPUs. 722|*/ 723bl__cpu_secondary_check52bitva 724bl__cpu_setup//initialiseprocessor 725adrpx1,swapper_pg_dir//設(shè)置內(nèi)核主頁表 726bl__enable_mmu//使能mmu 727ldrx8,=__secondary_switched 728brx8 729ENDPROC(secondary_startup) || / 731__secondary_switched: --732adr_lx5,vectors//設(shè)置從處理器的異常向量表 --733msrvbar_el1,x5 --734isb//指令同步屏障保證屏障前面的指令執(zhí)行完 735 --736adr_lx0,secondary_data//獲得主處理器傳遞過來的從處理器數(shù)據(jù) --737ldrx1,[x0,#CPU_BOOT_STACK]//getsecondary_data.stack獲得棧地址 738movsp,x1//設(shè)置到從處理器的sp --739ldrx2,[x0,#CPU_BOOT_TASK]//獲得從處理器的tskidle進(jìn)程的tsk結(jié)構(gòu), --740msrsp_el0,x2//保存在sp_el0arm64使用sp_el0保存當(dāng)前進(jìn)程的tsk結(jié)構(gòu) 741movx29,#0//fp清0 742movx30,#0//lr清0 --743bsecondary_start_kernel//跳轉(zhuǎn)到c程序繼續(xù)執(zhí)行從處理器初始化 744ENDPROC(__secondary_switched)
__cpu_up中設(shè)置了secondary_data結(jié)構(gòu)中的一些成員:
arch/arm64/kernel/smp.c: 112int__cpu_up(unsignedintcpu,structtask_struct*idle) 113{ 114intret; 115longstatus; 116 117/* 118|*Weneedtotellthesecondarycorewheretofinditsstackandthe 119|*pagetables. 120|*/ 121secondary_data.task=idle;//執(zhí)行的進(jìn)程描述符 122secondary_data.stack=task_stack_page(idle)+THREAD_SIZE;//棧地址THREAD_SIZE=16k 123update_cpu_boot_status(CPU_MMU_OFF); 124__flush_dcache_area(&secondary_data,sizeof(secondary_data)); 125 126/* 127|*NowbringtheCPUintoourworld. 128|*/ 129ret=boot_secondary(cpu,idle);
跳轉(zhuǎn)到secondary_start_kernel這個C函數(shù)繼續(xù)執(zhí)行初始化:
183/* 184*ThisisthesecondaryCPUbootentry.We'reusingthisCPUs 185*idlethreadstack,butasetoftemporarypagetables. 186*/ 187asmlinkagenotracevoidsecondary_start_kernel(void) 188{ 189u64mpidr=read_cpuid_mpidr()&MPIDR_HWID_BITMASK; 190structmm_struct*mm=&init_mm; 191unsignedintcpu; 192 193cpu=task_cpu(current); 194set_my_cpu_offset(per_cpu_offset(cpu)); 195 196/* 197|*Allkernelthreadssharethesamemmcontext;graba 198|*referenceandswitchtoit. 199|*/ 200mmgrab(mm);//init_mm的引用計數(shù)加1 201current->active_mm=mm;//設(shè)置idle借用的mm結(jié)構(gòu) 202 203/* 204|*TTBR0isonlyusedfortheidentitymappingatthisstage.Makeit 205|*pointtozeropagetoavoidspeculativelyfetchingnewentries. 206|*/ 207cpu_uninstall_idmap(); 208 209preempt_disable();//禁止內(nèi)核搶占 210trace_hardirqs_off(); 211 212/* 213|*Ifthesystemhasestablishedthecapabilities,makesure 214|*thisCPUticksallofthose.Ifitdoesn't,theCPUwill 215|*failtocomeonline. 216|*/ 217check_local_cpu_capabilities(); 218 219if(cpu_ops[cpu]->cpu_postboot) 220cpu_ops[cpu]->cpu_postboot(); 221 222/* 223|*LogtheCPUinfobeforeitismarkedonlineandmightgetread. 224|*/ 225cpuinfo_store_cpu();//存儲cpu信息 226 227/* 228|*EnableGICandtimers. 229|*/ 230notify_cpu_starting(cpu);//使能gic和timer 231 232store_cpu_topology(cpu);//保存cpu拓?fù)?233numa_add_cpu(cpu);///numa添加cpu 234 235/* 236|*OK,nowit'ssafetoletthebootCPUcontinue.Waitfor 237|*theCPUmigrationcodetonoticethattheCPUisonline 238|*beforewecontinue. 239|*/ 240pr_info("CPU%u:Bootedsecondaryprocessor0x%010lx[0x%08x] ", 241|cpu,(unsignedlong)mpidr, 242|read_cpuid_id());//打印內(nèi)核log 243update_cpu_boot_status(CPU_BOOT_SUCCESS); 244set_cpu_online(cpu,true);//設(shè)置cpu狀態(tài)為online 245complete(&cpu_running);//喚醒主處理器的完成等待函數(shù),繼續(xù)啟動下一個從處理器 246 247local_daif_restore(DAIF_PROCCTX);//從處理器繼續(xù)往下執(zhí)行 248 249/* 250|*OK,it'sofftotheidlethreadforus 251|*/ 252cpu_startup_entry(CPUHP_AP_ONLINE_IDLE);//idle進(jìn)程進(jìn)入idle狀態(tài) 253}
實際上,可以看的當(dāng)從處理器啟動到內(nèi)核的時候,他們也需要設(shè)置異常向量表,設(shè)置mmu等,然后執(zhí)行各自的idle進(jìn)程(這些都是一些處理器強(qiáng)相關(guān)的初始化代碼,一些通用的初始化都已經(jīng)被主處理器初始化完),當(dāng)cpu負(fù)載均衡的時候會放置一些進(jìn)程到這些從處理器,然后進(jìn)程就可以再這些從處理器上歡快的運行。
寫到這里,關(guān)于arm64平臺的多核啟動已經(jīng)介紹完成,可以發(fā)現(xiàn)里面還是會涉及到很多細(xì)節(jié),源碼散落在uboot,atf,kernel等源碼目錄中,多核啟動并不是很神秘,都是需要告訴從處理器從那個地方開始取值執(zhí)行,然后從處理器進(jìn)入內(nèi)核后需要自身做一些必要的初始化,就進(jìn)入idle狀態(tài)等待有任務(wù)來調(diào)度.
arm64平臺使用psci更為廣泛。
審核編輯:湯梓紅
-
處理器
+關(guān)注
關(guān)注
68文章
19100瀏覽量
228814 -
芯片
+關(guān)注
關(guān)注
452文章
50206瀏覽量
420894 -
cpu
+關(guān)注
關(guān)注
68文章
10804瀏覽量
210829 -
SMP
+關(guān)注
關(guān)注
0文章
71瀏覽量
19614
原文標(biāo)題:參考文章
文章出處:【微信號:處芯積律,微信公眾號:處芯積律】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論