0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

SMP是什么?多核芯片(SMP)的啟動方法

sanyue7758 ? 來源:TrustZone ? 2023-07-26 09:26 ? 次閱讀

前言

看這篇文章,你必備的一些前置知識有如下

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啟動流程的示意圖如下:

image.png在這里插入圖片描述

芯片上電后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í)行流程圖:

image.png在這里插入圖片描述

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_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啟動操作:

image.png在這里插入圖片描述

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啟動流程:

image.png在這里插入圖片描述

由于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)用流程:

image.png在這里插入圖片描述

其中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í)行主流程:

image.png在這里插入圖片描述

由于其底層相關(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é)

整個圖來看看

image.png在這里插入圖片描述

最后這里補(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:

image.png在這里插入圖片描述

由于其中每個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):

image.png在這里插入圖片描述

其中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)系:

image.png在這里插入圖片描述

這里解釋了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ù)。以下為其示意圖:

image.png在這里插入圖片描述

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),其流程如下:

image.png在這里插入圖片描述

其主要工作即為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í)行實際的處理流程。

image.png在這里插入圖片描述

上圖為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)的異常處理流程:

image.png在這里插入圖片描述

即其會根據(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)的一些初始化工作:

image.png在這里插入圖片描述

在初始化完成且hotplug線程創(chuàng)建完成后,就可通過以下流程喚醒cpu hotplug線程:

image.png在這里插入圖片描述

此后hotplug線程將調(diào)用psci回調(diào)函數(shù),并最終觸發(fā)smc異常進(jìn)入bl31:

image.png在這里插入圖片描述

bl31接收到該異常后執(zhí)行std_svc_smc_handler處理函數(shù),并最終調(diào)用平臺相關(guān)的電源管理接口,完成cpu的上電工作,以下為其執(zhí)行流程:

image.png在這里插入圖片描述

平臺相關(guān)回調(diào)函數(shù)pwr_domain_on將為secondary cpu設(shè)置入口函數(shù),然后為其上電使該cpu跳轉(zhuǎn)到內(nèi)核入口secondary_entry處開始執(zhí)行。以下為其內(nèi)核啟動流程:

image.png在這里插入圖片描述

到這里其實就結(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)確安全的啟動位置了。)

image.png在這里插入圖片描述

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更為廣泛。

審核編輯:湯梓紅

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 處理器
    +關(guān)注

    關(guān)注

    68

    文章

    19100

    瀏覽量

    228814
  • 芯片
    +關(guān)注

    關(guān)注

    452

    文章

    50206

    瀏覽量

    420894
  • cpu
    cpu
    +關(guān)注

    關(guān)注

    68

    文章

    10804

    瀏覽量

    210829
  • SMP
    SMP
    +關(guān)注

    關(guān)注

    0

    文章

    71

    瀏覽量

    19614

原文標(biāo)題:參考文章

文章出處:【微信號:處芯積律,微信公眾號:處芯積律】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    對稱多處理 (SMP) 的應(yīng)用優(yōu)勢

      何謂 SMP?我們?yōu)槭裁葱枰?   對稱多處理 (SMP) 廣泛應(yīng)用于 PC 領(lǐng)域,能夠顯著提升臺式計算機(jī)的性能。SMP 能使單芯片上多個相同的處理子系統(tǒng)運行相同的指令集,而
    發(fā)表于 09-03 11:07 ?2743次閱讀
    對稱多處理 (<b class='flag-5'>SMP</b>) 的應(yīng)用優(yōu)勢

    【深圳SMP03,SMP03】

    `SMP03,30G信號源,二手SMP03 SMP03,30G信號源,二手SMP03型號:R & S SMP02,
    發(fā)表于 02-01 16:32

    AliOS Things SMP系統(tǒng)及其在esp32上實現(xiàn)示例

    對應(yīng)CPU的SMP功能:2.1 核啟動加載目前的啟動順序是,系統(tǒng)默認(rèn)開始啟動0核,在0核的主任務(wù)入口內(nèi)啟動其他核的加載,使其都進(jìn)入任務(wù)調(diào)度,
    發(fā)表于 05-15 12:45

    SMP04|SMP22|信號源|SMP04 現(xiàn)金回收

    SMP04|SMP22|信號源|SMP04 現(xiàn)金回收歐陽R:***QQ:734645067回收工廠或個人、庫存閑置、二手儀器及附件。長期 銷售、維修、回收 高頻 二手儀器。溫馨提示:如果您
    發(fā)表于 12-31 17:51

    多核處理器分類之SMP與NUMA簡析

    多核處理器,在每個多核處理器芯片內(nèi)部的L2和L3可以采用共享緩存或SMP方式,而在多個芯片間采用DSM結(jié)構(gòu)互連。原作者:老秦談芯
    發(fā)表于 06-07 16:46

    RT-Thread SMP和AMP初體驗簡介

    ,所有CPU共享系統(tǒng)內(nèi)存和外設(shè)資源。嵌入式領(lǐng)域除 SMP 外還有一種支持多核處理器芯片的操作系統(tǒng)體系結(jié)構(gòu),即 AMP。AMP(Asymmetric Multi-processing) : 非對稱
    發(fā)表于 02-03 14:33

    RT-Thread框架下的SMP支持

    支持 SMP,在對稱多核上可以通過使能 RT_USING_SMP 來開啟。系統(tǒng)上電后,各 CPU 的啟動流程如下圖所示:每個次級 CPU 自身硬件部分的初始化不能由 CPU0 完成,因
    發(fā)表于 02-13 15:02

    如何啟用SMP

    我知道如何啟用SMP。開啟SMP后, [color=\\\"#FF0000\\\"]在用戶空間使用SMP應(yīng)該怎么做,比如怎么寫代碼,怎么指定CPU核心,怎么開啟負(fù)載均衡
    發(fā)表于 05-16 06:00

    SMP-04采樣保持四放大器和SMP-08 SMP-18采樣

    SMP-04采樣保持四放大器和SMP-08 SMP-18采樣保持八放大器的應(yīng)用:
    發(fā)表于 06-03 14:54 ?34次下載
    <b class='flag-5'>SMP</b>-04采樣保持四放大器和<b class='flag-5'>SMP</b>-08 <b class='flag-5'>SMP</b>-18采樣

    什么是SMP(對稱多處理)

    什么是SMP(對稱多處理)
    發(fā)表于 12-17 14:04 ?2291次閱讀

    SMP技術(shù)

    SMP技術(shù) SMP英文全稱為Symmetrical Multi-Processing,意指“對稱多處理”技術(shù),是指在一個計算機(jī)上匯集了一組處理器—即多CPU,各CPU之
    發(fā)表于 12-17 14:08 ?4514次閱讀

    Linux在SMP系統(tǒng)上的移植研究

    基于自主開發(fā)以雙核嵌入式CPU EM8301為處理核心的嵌入式應(yīng)用的目的,針對雙核CPU芯片的系統(tǒng)結(jié)構(gòu)和Linux內(nèi)核的特性,通過研究嵌入式Linux操作系統(tǒng)在SMP系統(tǒng)上的移植,探討SMP架構(gòu)
    發(fā)表于 11-14 16:09 ?11次下載
    Linux在<b class='flag-5'>SMP</b>系統(tǒng)上的移植研究

    ARM64 SMP多核啟動(上)—spin-table

    一般嵌入式系統(tǒng)使用的都是對稱多處理器(Symmetric Multi-Processor, SMP)系統(tǒng),包含了多個cpu, 這幾個cpu都是相同的處理器,如4核Contex-A53。
    發(fā)表于 06-09 14:28 ?949次閱讀
    ARM64 <b class='flag-5'>SMP</b><b class='flag-5'>多核</b><b class='flag-5'>啟動</b>(上)—spin-table

    SMP是什么 啟動方式介紹

    ,一個服務(wù)器系統(tǒng)可以同時運行多個處理器,并共享內(nèi)存和其他的主機(jī)資源。 CMP 英文為Chip multiprocessors,指的是單芯片多處理器,也指多核心。其思想是將大規(guī)模并行處理器中的SMP集成到同一
    的頭像 發(fā)表于 12-05 15:23 ?1705次閱讀

    SMP多核啟動cpu操作函數(shù)

    其中spin-table啟動方式的回調(diào)如下: const struct cpu_operations smp_spin_table_ops = {.name= "spin-table
    的頭像 發(fā)表于 12-05 16:04 ?666次閱讀
    <b class='flag-5'>SMP</b><b class='flag-5'>多核</b><b class='flag-5'>啟動</b>cpu操作函數(shù)