前言
看這篇文章,你必備的一些前置知識有如下
1、ATF啟動流程 2、PSCI電源管理的概念 3、設備樹
如果沒有,想要我發布什么方面的內容?記得點贊、分享、評論三連。哈哈哈
昨天有個朋友在問多核啟動,于是今天就整理一篇多核啟動的文章,內容大多數參考與網上前輩們的優秀博客,感激。
1、SMP是什么?
SMP 英文為Symmetric Multi-Processing ,是對稱多處理結構的簡稱,是指在一個計算機上匯集了一組處理器(多CPU),各CPU之間共享內存子系統以及總線結構,一個服務器系統可以同時運行多個處理器,并共享內存和其他的主機資源。
CMP 英文為Chip multiprocessors,指的是單芯片多處理器,也指多核心。其思想是將大規模并行處理器中的SMP集成到同一芯片內,各個處理器并行執行不同的進程。
(1)CPU數:獨立的中央處理單元,體現在主板上就是有多少個CPU槽位
(2)CPU核心數(CPU cores):在每一個CPU上,都可能有多核(core),每個核中都有獨立的ALU,FPU,Cache等組件,可以理解為CPU的物理核數。(我們常說4核8線程中的核),指物理上存在的物體。
(3)CPU線程數(processor邏輯核):一種邏輯上的概念,并非真實存在的物體,只是為了更好地描述CPU的運作能力。簡單地說,就是模擬出的CPU核心數。
不過在這里我們這里指的是多個單核CPU組合到一起,每個核都有自己的一套寄存器。
一個系統存在多個CPU,成本會更高和管理也更困難。多核算是輕量級的SMP,物理上多核CPU還是封裝成一個CPU,但是在CPU內部具有多個CPU的核心部件,可以同時運行多個線程/進程。但是需要CPU核心之間要共享資源,比如緩存。
對程序員來說,它們之間的區別很小,大多數情況可以不做區分。我們在嵌入式開發中,大部分都是用的多核CPU。
這里我們就把這個SMP啟動轉換成多核CPU啟動。
2、啟動方式
程序為何可以在多個cpu上并發執行:他們有各自獨立的一套寄存器,如:程序計數器pc,棧指針寄存器sp,通用寄存器等,可以獨自 取指、譯碼、執行,當然內存和外設資源是共享的,多核環境下當訪問臨界區 資源一般 自旋鎖來防止競態發生。
soc啟動的一般會從片內的rom, 也叫bootrom開始執行第一條指令,這個地址是系統默認的啟動地址,會在bootrom中由芯片廠家固化一段啟動代碼來加載啟動bootloader到片內的sram,啟動完成后的bootloader除了做一些硬件初始化之外做的最重要的事情是初始化ddr,因為sram的空間比較小所以需要初始化擁有大內存 ddr,最后會從網絡/usb下載 或從存儲設備分區上加載內核到ddr某個地址,為內核傳遞參數之后,然后bootloader就完成了它的使命,跳轉到內核,就進入了操作系統內核的世界。
bootloader將系統的控制權交給內核之后,他首先會進行處理器架構相關初始化部分,如設置異常向量表,初始化mmu(之后內核就從物理地址空間進入了虛擬地址空間的世界,一切是那么的虛無縹緲,又是那么的恰到好處)等等,然后會清bss段,設置sp之后跳轉到C語言部分進行更加復雜通用的初始化,其中會進行內存方面的初始化,調度器初始化,文件系統等內核基礎組件 初始化工作,隨后會進行關鍵的從處理器的引導過程,然后是各種實質性的設備驅動的初始化,最后 創建系統的第一個用戶進程init后進入用戶空間執行用戶進程宣誓內核初始化完成,可以進程正常的調度執行。
系統初始化階段大多數都是主處理器做初始化工作,所有不用考慮處理器并發情況,一旦從處理器被bingup起來,調度器和各自的運行隊列準備就緒,多個任務就會均衡到各個處理器,開始了并發的世界,一切是那么的神奇。
soc在啟動階段除了一些特殊情況外(如為了加快啟動速度,在bl2階段通過并行加載方式同時加載bl31、bl32和bl33鏡像),一般都沒有并行化需求。因此只需要一個cpu執行啟動流程即可,這個cpu被稱為primary cpu,而其它的cpu則統一被稱為secondary cpu。為了防止secondary cpu在啟動階段的執行,它們在啟動時必須要被設置為一個特定的狀態。(有時候為了增加啟動速度,必須對時間敏感的設備,就可能啟動的時候整個從核并行跑一些任務)
當primary cpu完成操作系統初始化,調度系統開始工作后,就可以通過一定的機制啟動secondary cpu。顯然secondary cpu不再需要執行啟動流程代碼,而只需直接跳轉到內核中執行即可。
主流程啟動初始化一般來說都是主核在干的,當系統完成了初始化后就開始啟動從核。 這就像在啟動的大門,只有主核讓你過了,其他的先在門外等著。當cpu0啟動到kernel后,就會去門口,把它們的門禁卡給它們,卡上就寫的它們的目的地班級是哪里。如果沒有這個門禁卡的cpu,說明地址為0,就繼續在原地等著。
故其啟動的關鍵是如何將內核入口地址告知secondary cpu,以使其能跳轉到正確的執行位置。
aarch64架構實現了兩種不同的啟動方式,spin-table和psci。
其中spin-table方式非常簡單,但其只能被用于secondary cpu啟動,功能比較單一。
隨著aarch64架構電源管理需求的增加(如cpu熱插拔、cpu idle等),arm設計了一套標準的電源管理接口協議psci。該協議可以支持所有cpu相關的電源管理接口,而且由于電源相關操作是系統的關鍵功能,為了防止其被攻擊,該協議將底層相關的實現都放到了secure空間,從而可提高系統的安全性。
2.1 spin-table
spin-table啟動流程的示意圖如下:
在這里插入圖片描述
芯片上電后primary cpu開始執行啟動流程,而secondary cpu則將自身設置為WFE睡眠狀態,并且為內核準備了一塊內存,用于填寫secondary cpu的入口地址。
uboot負責將這塊內存的地址寫入devicetree中,當內核初始化完成,需要啟動secondary cpu時,就將其內核入口地址寫到那塊內存中,然后喚醒cpu。
secondary cpu被喚醒后,檢查該內存的內容,確認內核已經向其寫入了啟動地址,就跳轉到該地址執行啟動流程。
2.1.1 secondary cpu初始化狀態設置
uboot啟動時,secondary cpu會通過以下流程進入wfe狀態(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)若當前cpu為primary cpu,則跳轉到step 3,繼續執行啟動流程。其中cpu id是通過mpidr區分的,而啟動流程中哪個cpu作為primary cpu可以任意指定。當指定完成后,此處就可以根據其身份確定相應的執行流程
(2)若當前cpu為slave cpu,則執行spin流程。它是由spin_table_secondary_jump函數實現的(arch/arm/cpu/armv8/start.S)。以下為其代碼實現:
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當前沒有事情要做,因此執行wfe指令進入睡眠模式,以降低功耗
(2)spin_table_cpu_release_addr將由uboot傳遞給內核,根據step 5的定義可知,其長度為8個字節,在64位系統中正好可以保存一個指針。而它的內容在啟動時會被初始化為0,當內核初始化完成后,在啟動secondary cpu之前,會在uboot中將其入口地址寫到該位置,并喚醒它
(3)當secondary cpu從wfe狀態喚醒后,會校驗內核是否在spin_table_cpu_release_addr處填寫了它的啟動入口。若未填寫,則其會繼續進入wfe狀態
(4)若內核填入了啟動地址,則其直接跳轉到該地址開始執行內核初始化流程
2.1.2 spin_table_cpu_release_addr的傳遞
由于在armv8架構下,uboot只能通過devicetree向內核傳遞參數信息,因此當其開啟了CONFIG_ARMV8_SPIN_TABLE配置選項后,就需要在適當的時候將該值寫入devicetree中。
我們知道uboot一般通過bootm命令啟動操作系統(aarch64支持的booti命令,其底層實現與bootm相同),因此在bootm中會執行一系列啟動前的準備工作,其中就包括將spin-table地寫入devicetree的工作。以下其執行流程圖:
在這里插入圖片描述
spin_table_update_dt的代碼實現如下:
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節點
(3)遍歷該節點的所有cpu子節點,并校驗其enable-method是否為spin-table。若不是所有cpu的都該類型,則不設置
(4)若所有cpu的enable-method都為spin-table,則將該參數設置到cpu-release-addr屬性中
(5)由于這段地址有特殊用途,內核的內存管理系統不能將其分配給其它模塊。因此,需要將其添加到保留內存中
2.1.3 啟動secondary cpu
內核在啟動secondary cpu之前當然需要為其準備好執行環境,因為內核中cpu最終都將由調度器管理,故此時調度子系統應該要初始化完成。
同時cpu啟動完成轉交給調度器之前,并沒有實際的業務進程,而我們知道內核中cpu在空閑時會執行idle進程。因此,在其啟動之前需要為每個cpu初始化一個idle進程。
另外,由于將一個cpu通過熱插拔方式移除后,再次啟動該cpu的流程,與secondary cpu的啟動流程是相同的,因此內核復用了cpu hotplug框架用于啟動secondary cpu。
而內核為每個cpu都分配了一個獨立的hotplug線程,用于執行本cpu相關的熱插拔流程。為此,內核通過以下流程執行secondary cpu啟動操作:
在這里插入圖片描述
idle進程初始化
以下代碼為每個非boot cpu分配一個idle進程
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)遍歷系統中所有的possible cpu
(2)若該cpu為secondary cpu,則為其初始化一個idle進程
hotplug線程初始化
以下代碼為每個cpu初始化一個hotplug線程
void__initcpuhp_threads_init(void) { BUG_ON(smpboot_register_percpu_thread(&cpuhp_threads)); kthread_unpark(this_cpu_read(cpuhp_state.thread)); }
其中線程的描述結構體定義如下:
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)線程創建時調用的回調
(3)該回調用于獲取線程是否需要退出標志
(4)cpu hotplug主函數,執行實際的hotplug操作
(5)該線程的線程名
(6)用于設置線程創建完成后,是否將其設置為park狀態
hotplug回調線程喚醒
內核使用以下流程喚醒特定cpu的hotplug線程,用于執行實際的cpu啟動流程:
在這里插入圖片描述
由于cpu啟動時需要與一系列模塊交互以執行相應的準備工作,為此內核為其定義了一組hotplug狀態,用于表示cpu在啟動或關閉時分別需要執行的流程。以下為個階段狀態定義示例(由于該數組較長,故只截了一小段):
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兩個回調函數,分別表示cpu啟動和關閉時需要執行的流程。其中在cpu啟動時,將會從CPUHP_OFFLINE狀態開始,依次執行各個階段的startup.single回調函數。其中CPUHP_BRINGUP_CPU及之前的階段都在secondary cpu啟動之前執行。
而CPUHP_BRINGUP_CPU階段的回調函數bringup_cpu,會實際觸發secondary cpu的啟動流程。它將通過cpu_ops接口調用spin-table函數,啟動secondary cpu,并等待其啟動完成。
當secondary cpu啟動完成后,將喚醒hotplug線程,其將繼續執行CPUHP_BRINGUP_CPU之后階段相關的回調函數。
cpu操作函數
cpu_ops函數由bringup_cpu調用,以觸發secondary cpu啟動。它是根據設備樹中解析出的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)根據其enable-method獲取其對應的cpu_ops回調
其中spin-table啟動方式的回調如下:
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, }
觸發secondary cpu啟動
以上流程都準備完成后,觸發secondary cpu啟動就非常簡單了。只需調用其cpu_ops回調函數,向其對應的spin_table_cpu_release_addr位置寫入secondary cpu入口地址即可。以下為其調用流程:
在這里插入圖片描述
其中smp_spin_table_cpu_boot的實現如下:
staticintsmp_spin_table_cpu_boot(unsignedintcpu) { write_pen_release(cpu_logical_map(cpu));(1) sev();(2) return0; }
(1)向給定地址寫入內核entry
(2)通過sev指令喚醒secondary cpu啟動
此后,該線程將等待cpu啟動完成,并在完成后將其設置為online狀態
secondary cpu執行流程
aarch64架構secondary cpu的內核入口函數為secondary_entry(arch/arm64/kernel/head.S),以下為其執行主流程:
在這里插入圖片描述
由于其底層相關初始化流程與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)由于內核線程并沒有用于地址空間,因此其active_mm通常指向上一個用戶進程的地址空間。而cpu初始化時,由于之前并沒有運行過用戶進程,因此將其初始化為init_mm
(2)idmap地址映射僅僅是用于mmu使能時地址空間的平滑切換,在mmu使能完成后已經沒有作用。更進一步,由于idmap頁表所使用的ttbr0_elx頁表基地址寄存器,正常情況下是用于用戶空間頁表的,在調度器接管該cpu之前也必須要將其歸還給用戶空間
(3)執行cpu_postboot回調
(4)由secondary cpu已經啟動成功,故將其設置為online狀態
(5)喚醒cpu hotplug線程
(6)讓cpu執行idle線程,其代碼實現如下:
voidcpu_startup_entry(enumcpuhp_statestate) { arch_cpu_idle_prepare(); cpuhp_online_idle(state); while(1) do_idle(); }
至此,cpu已經啟動完成,并開始執行idle線程了。最后當然是要通知調度器,將該cpu的管理權限移交給調度器了。它是通過cpu hotplug的以下回調實現的:
staticstructcpuhp_stepcpuhp_hp_states[]={ … [CPUHP_AP_SCHED_STARTING]={ .name="sched:starting", .startup.single=sched_cpu_starting, .teardown.single=sched_cpu_dying, } … }
以下為該函數的實現:
intsched_cpu_starting(unsignedintcpu) { … sched_rq_cpu_starting(cpu);(1) sched_tick_start(cpu);(2) … }
(1)用于初始化負載均衡相關參數,此后該cpu就可以在其后的負載均衡流程中拉取進程
(2)tick時鐘是內核調度器的脈搏,啟動了該時鐘之后,cpu就會在時鐘中斷中執行調度操作,從而讓cpu參與到系統的調度流程中
到這里我們就知道了spin-table這個流程。不得不說前輩對這個邏輯理解很清楚,這個內容的參考鏈接在文末,歡迎大家點擊原文鏈接點贊。
小結
整個圖來看看
在這里插入圖片描述
最后這里補充一下一個使用自旋表作為啟動方式的平臺設備樹cpu節點:
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方式的多核啟動方式,顧名思義在于自旋,主處理器和從處理器上電都會啟動,主處理器執行uboot暢通無阻,從處理器在spin_table_secondary_jump處wfe睡眠,主處理器通過修改設備樹的cpu節點的cpu-release-addr屬性為spin_table_cpu_release_addr,這是從處理器的釋放地址所在的地方。
主處理器進入內核后,會通過smp_prepare_cpus函數調用spin-table 對應的cpu操作集的cpu_prepare方法從而在smp_spin_table_cpu_prepare函數中設置從處理器的釋放地址為secondary_holding_pen這個內核函數,然后通過sev指令喚醒從處理器,從處理器繼續從secondary_holding_pen開始執行(從處理器來到了內核的世界),發現secondary_holding_pen_release不是自己的處理編號,然后通過wfe繼續睡眠。
當主處理器完成了大多數的內核組件的初始化之后,調用smp_init來來開始真正的啟動從處理器,最終調用spin-table 對應的cpu操作集的cpu_boot方法從而在smp_spin_table_cpu_boot將需要啟動的處理器的編號寫入secondary_holding_pen_release中,然后再次sev指令喚醒從處理器,從處理器得以繼續執行(設置自己異常向量表,初始化mmu等)。
最終在idle線程中執行wfi睡眠。其他從處理器也是同樣的方式啟動起來,同樣最后進入各種idle進程執行wfi睡眠,主處理器繼續往下進行內核初始化,直到啟動init進程,后面多個處理器都被啟動起來,都可以調度進程,多進程還會被均衡到多核。
下面介紹另外一種啟動 PSCI。
2.2 psci
psci是arm提供的一套電源管理接口,當前一共包含0.1、0.2和1.0三個版本。它可被用于以下場景:(1)cpu的idle管理
(2)cpu hotplug以及secondary cpu啟動
(3)系統shutdown和reset
首先,我們先來看下設備樹cpu節點對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節點的詳細說明可以參考內核文檔:Documentation/devicetree/bindings/arm/psci.txt
從這個我們可以獲得什么信息呢?
可以看到現在enable-method 屬性已經是psci,說明使用的多核啟動方式是psci,
下面還有psci節點,用于psci驅動使用,method用于說明調用psci功能使用什么指令,可選有兩個smc和hvc。
其實smc, hvc和svc都是從低運行級別向高運行級別請求服務的指令,我們最常用的就是svc指令了,這是實現系統調用的指令。
高級別的運行級別會根據傳遞過來的參數來決定提供什么樣的服務。
smc是用于陷入el3(安全),
hvc用于陷入el2(虛擬化, 虛擬化場景中一般通過hvc指令陷入el2來請求喚醒vcpu), svc用于陷入el1(系統)。
這里只講解smc陷入el3啟動多核的情況。
2.2.1 psci 基礎概念知識
下面我們將按照電源管理拓撲結構(power domain)、電源狀態(power state)以及armv8安全擴展幾個方面介紹psci的一些基礎知識
2.2.1.1power domain
我們前面已經介紹過cpu的拓撲結構,如aarch64架構下每塊soc可能會包含多個cluster,而每個cluster又包含多個core,它們共同組成了層次化的拓撲結構。如以下為一塊包含2個cluster,每個cluster包含四個core的soc:
在這里插入圖片描述
由于其中每個core以及每個cluster的電源都可以獨立地執行開關操作,因此若core0 – core3的電源都關閉了,則cluster 0的電源也可以被關閉以降低功耗。
若core0 – core3中的任一個core需要上電,則顯然cluster 0需要先上電。為了更好地進行層次化電源管理,psci在電源管理流程中將以上這些組件都抽象為power domain。如以下為上例的power domain層次結構:
在這里插入圖片描述
其中system level用于管理整個系統的電源,cluster level用于管理某個特定cluster的電源,而core level用于管理一個單獨core的電源。
2.2.1.2power state
由于aarch64架構有多種不用的電源狀態,不同電源狀態的功耗和喚醒延遲不同。
如standby狀態會關閉power domain的clock,但并不關閉電源。因此它雖然消除了門電路翻轉引起的動態功耗,但依然存在漏電流等引起的靜態功耗。故其功耗相對較大,但相應地喚醒延遲就比較低。
而對于power down狀態,會斷開對應power domain的電源,因此其不僅消除了動態功耗,還消除了靜態功耗,相應地其喚醒延遲就比較高了。
psci一共為power domain定義了四種power state:
?(1)run:電源和時鐘都打開,該domain正常工作
?(2)standby:關閉時鐘,但電源處于打開狀態。其寄存器狀態得到保存,打開時鐘后就可繼續運行。功耗相對較大,但喚醒延遲較低。arm執行wfi或wfe指令會進入該狀態。
?(3)retention:它將core的狀態,包括調試設置都保存在低功耗結構中,并使其部分關閉。其狀態在從低功耗變為運行時能自動恢復。從操作系統角度看,除了進入方法、延遲等有區別外,其它都與standby相同。它的功耗和喚醒延遲都介于standby和power down之間。
?(4)power down:關閉時鐘和電源。power domain掉電后,所有狀態都丟失,上電以后軟件必須重新恢復其狀態。它的功耗最低,但喚醒延遲也相應地最高。
(這里我很好奇怎么和linux的s3、s4對應的。當時測試s3的時候,對應的是suspend。這里的對于cpu的有off、on、suspend三種,我覺得這里應該就是對于的standby,因為有wfi或wfe這些指令。那s4就是CPU off了?可以看一下這個有點認識,突然想到psci里面的狀態是對于的cpu為對象,但是linux的電源管理應該是對整個設備。)
可以看一下這個文章:s1s2s3S4S5的含義待機、休眠、睡眠的區別
顯然,power state的睡眠程度從run到power down逐步加深。而高層級power domain的power state不應低于低層級power domain。
如以上例子中core 0 – core 2都為power down狀態,而core 3為standby狀態,則cluster 0不能為retention或power down狀態。同樣若cluster 0為standby狀態,而cluster 1為run狀態,則整個系統必須為run狀態。
為了達到上述約束,不同power domain之間的power state具有以下關系:
在這里插入圖片描述
這里解釋了psci那個源碼文檔里電源樹的概念。
psci實現了父leve與子level之間的電源關系協調,如cluster 0中最后一個core被設置為power down狀態后,psci就會將該cluster也設置為power donw狀態。若其某一個core被設置為run狀態,則psci會先將其對應cluster的狀態設置為run,然后再設置對應core的電源狀態,這也是psci名字的由來(power state coordinate interface)
2.2.1.3armv8的安全擴展
為了增強arm架構的安全性,aarch64一共實現了secure和non-secure兩種安全狀態。通過一系列硬件擴展,在cpu執行狀態、總線、內存、外設、中斷、tlb、cache等方面都實現了兩種狀態之間的隔離。
在這種機制下,secure空間的程序可以訪問所有secure和non-secure的資源,而non-secure空間的程序只能訪問non-secure資源,卻不能訪問secure資源。從而可以將一些安全關鍵的資源放到secure空間,以增強其安全性。
為此aarch64實現了4個異常等級,其中EL3工作在secure空間,而EL0 – EL2既可以工作于secure空間,又可以工作于non-secure空間。不同異常等級及不同secure狀態的模式下可運行不同類型軟件。
如secure EL1和El0用于運行trust os內核及其用戶態程序,non-secure EL1和El0用于運行普通操作系統內核(如linux)及其用戶態程序,EL2用于運行虛擬機的hypervisor。而EL3運行secure monitor程序(通常為bl31),其功能為執行secure和non secure狀態切換、消息轉發以及提供類似psci等secure空間服務。以下為其示意圖:
在這里插入圖片描述
psci是工作于non secure EL1(linux內核)和EL3(bl31)之間的一組電源管理接口,其目的是讓linux實現具體的電源管理策略,而由bl31管理底層硬件相關的操作。從而將cpu電源控制這種影響系統安全的控制權限放到安全等級更高的層級中,從而提升系統的整體安全性。
那么psci如何從EL1調用EL3的服務呢?其實它和系統調用是類似的,只是系統調用是用戶態程序陷入操作系統內核,而psci是從操作系統內核陷入secure monitor。armv8提供了一條smc異常指令,內核只需要提供合適的參數后,觸發該指令即可通過異常的方式進入secure monitor。(SMC調用,這個在ATF專欄有介紹)
2.2.2 psci軟件架構
由于psci是由linux內核調用bl31中的安全服務,實現cpu電源管理功能的。因此其軟件架構包含三個部分:(1)內核與bl31之間的調用接口規范
(2)內核中的架構
(3)bl31中的架構
2.2.3psci接口規范
psci規定了linux內核調用bl31中電源管理相關服務的接口規范,它包含實現以下功能所需的接口:(1)cpu idle管理
(2)向系統動態添加或從系統動態移除cpu,通常稱為hotplug
(3)secondary cpu啟動
(4)系統的shutdown和reset
psci接口規定了命令對應的function_id、接口的輸入參數以及返回值。其中輸入參數可通過x0 – x7寄存器傳遞,而返回值通過x0 – x4寄存器傳遞。
如secondary cpu啟動或cpu hotplug時可調用cpu_on接口,為一個cpu執行上電操作。該接口的格式如下:(1)function_id:0xc400 0003
(2)輸入參數:使用mpidr值表示的target cpu id
cpu啟動入口的物理地址
context id,該值用于表示本次調用上下文相關的信息
(3)返回值:可以為success、invalid_parameter、invalid_address、already_on、on_pending或internal_failure
有了以下這些接口的詳細定義,內核和bl31就只需按照該接口的規定,獨立開發psci相關功能。從而避免了它們之間的耦合,簡化了開發復雜度。
2.2.4內核中的psci架構
內核psci軟件架構包含psci驅動和每個cpu的cpu_ops回調函數實現兩部分。
其中psci驅動實現了驅動初始化和psci相關接口實現功能,而cpu_ops回調函數最終也會調用psci驅動的接口。
2.2.4.1psci驅動
首先我們看一下devicetree中的配置:
psci{ compatible="arm,psci-0.2";(1) method="smc";(2) }
(1)用于指定psci版本
(2)根據該psci由bl31處理還是hypervisor處理,可以指定其對應的陷入方式。若由bl31處理為smc,若由hypervisor處理則為hvc
驅動流程主要是與bl31通信,以確認其是否支持給定的psci版本,以及相關psci操作函數的實現,其流程如下:
在這里插入圖片描述
其主要工作即為psci設置相關的回調函數,該函數定義如下:
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設置相應的回調函數
(2)為psci模塊設置系統重啟時的通知函數
(3)將系統的power_off函數指向相應的psci接口
2.2.4.2cpu_ops接口
驅動初始化完成后,cpu的cpu_ops就可以調用這些回調實現psci功能的調用。如下所示,當devicetree中cpu的enable-method設置為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,它會通過以下流程最終調用psci驅動中的psci_ops函數:
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架構
bl31為內核提供了一系列運行時服務,psci作為其標準運行時服務的一部分,通過宏DECLARE_RT_SVC注冊到系統中。其相應的定義如下:
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啟動流程中被調用,以用于初始化該服務相關的配置。而std_svc_smc_handler為其smc異常處理函數,當內核通過psci接口調用相關服務時,最終將由該函數執行實際的處理流程。
在這里插入圖片描述
上圖為psci初始化相關的流程,它主要包含內容:(1)前面我們已經介紹過power domain相關的背景,即psci需要協調不同層級的power domain狀態,因此其必須要了解系統的power domain配置情況。以上流程中紅色虛線框的部分主要就是用于初始化系統的power domain拓撲及其狀態
(2)由于psci在執行電源相關接口時,最終需要操作實際的硬件。而它們是與架構相關的,因此其操作函數最終需要注冊到平臺相關的回調中。plat_setup_psci_ops即用于注冊特定平臺的psci_ops回調,其格式如下:
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操作相應的異常處理流程:
在這里插入圖片描述
即其會根據function id的值,分別執行相應的電源管理服務,如啟動cpu時會調用psci_cpu_on函數,重啟系統時會調用psci_system_rest函數等。
2.2.6 secondary cpu啟動
由于psci方式啟動secondary cpu的流程,除了其所執行的cpu_ops不同之外,其它流程與spin-table方式是相同的,因此我們這里只給出執行流程圖,詳細分析可以參考上篇博文。其中以下流程執行secondary cpu啟動相關的一些初始化工作:
在這里插入圖片描述
在初始化完成且hotplug線程創建完成后,就可通過以下流程喚醒cpu hotplug線程:
在這里插入圖片描述
此后hotplug線程將調用psci回調函數,并最終觸發smc異常進入bl31:
在這里插入圖片描述
bl31接收到該異常后執行std_svc_smc_handler處理函數,并最終調用平臺相關的電源管理接口,完成cpu的上電工作,以下為其執行流程:
在這里插入圖片描述
平臺相關回調函數pwr_domain_on將為secondary cpu設置入口函數,然后為其上電使該cpu跳轉到內核入口secondary_entry處開始執行。以下為其內核啟動流程:
在這里插入圖片描述
到這里其實就結束了,不得不說這個前輩的文章是真的寫的邏輯清晰,收獲頗多。
小結
最后結合代碼再走一遍
1、std_svc_setup(主要關注設置psci操作集)--有服務
std_svc_setup//services/std_svc/std_svc_setup.c ->psci_setup//lib/psci/psci_setup.c ->plat_setup_psci_ops//設置平臺的psci操作調用平臺的plat_setup_psci_ops函數去設置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};
在遍歷每一個注冊的運行時服務的時候,會導致std_svc_setup調用,其中會做psci操作集的設置,操作集中我們可以看到對核電源的管理的接口如:核上電,下電,掛起等,我們主要關注上電 .pwr_domain_on = qemu_pwr_domain_on,這個接口當我們主處理器boot從處理器的時候會用到。
2、運行時服務觸發和處理--來請求
smc指令觸發進入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//獲得服務描述標識數組 ldrbw15,[x14,x16]//根據唯一的入口編號找到處理函數的地址 /* |*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、找到對應handler--請求匹配處理函數
上面其實主要的是找到服務例程,然后跳轉執行 下面是跳轉的處理函數:
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、處理函數干活
處理函數根據funid來決定服務,可以看到PSCI_CPU_ON_AARCH64為0xc4000003,這正是設備樹中填寫的cpu_on屬性的id,會委托psci_cpu_on來執行核上電任務。下面分析是重點:!!!
->psci_cpu_on()//lib/psci/psci_main.c ->psci_validate_entry_point()//驗證入口地址有效性并保存入口點到一個結構ep中 ->psci_cpu_on_start(target_cpu,&ep)//ep入口地址 ->psci_plat_pm_ops->pwr_domain_on(target_cpu) ->qemu_pwr_domain_on//實現核上電(平臺實現) /*Storethere-entryinformationforthenon-secureworld.*/ ->cm_init_context_by_index()//重點:會通過cpu的編號找到cpu上下文(cpu_context_t),存在cpu寄存器的值,異常返回的時候寫寫到對應的寄存器中,然后eret,舊返回到了el1!!! ->cm_setup_context()//設置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);//注:異常返回時執行此地址于是完成了cpu的啟動!!! write_ctx_reg(state,CTX_SPSR_EL3,ep->spsr);
psci_cpu_on主要完成開核的工作,然后會設置一些異常返回后寄存器的值(eg:從el1 -> el3 -> el1),重點關注 ep->pc寫到cpu_context結構的CTX_ELR_EL3偏移處(從處理器啟動后會從這個地址取指執行)。
實際上,所有的從處理器啟動后都會從bl31_warm_entrypoint開始執行,在plat_setup_psci_ops中會設置(每個平臺都有自己的啟動地址寄存器,通過寫這個寄存器來獲得上電后執行的指令地址)。
大致說一下:主處理器通過smc進入el3請求開核服務,atf中會響應這種請求,通過平臺的開核操作來啟動從處理器并且設置從處理的一些寄存器eg:scr_el3、spsr_el3、elr_el3,然后主處理器,恢復現場,eret再次回到el1,
而處理器開核之后會從bl31_warm_entrypoint開始執行,最后通過el3_exit返回到el1的elr_el3設置的地址。
分析到這atf的分析到此為止,atf中主要是響應內核的snc的請求,然后做開核處理,也就是實際的開核動作,但是從處理器最后還是要回到內核中執行,下面分析內核的處理:注意流程如下:
5、開核返回-EL1 啟動從處理器
init/main.c start_kernel ->boot_cpu_init//引導cpu初始化設置引導cpu的位掩碼onlineactivepresentpossible都為true ->setup_arch//arch/arm64/kernel/setup.c ->if(acpi_disabled)//不支持acpi psci_dt_init();//drivers/firmware/psci.c(psci主要文件)psci初始化解析設備樹尋找psci匹配的節點 else psci_acpi_init();//acpi中允許使用psci情況 ->rest_init ->kernel_init ->kernel_init_freeable ->smp_prepare_cpus//準備cpu對于每個可能的cpu1.cpu_ops[cpu]->cpu_prepare(cpu)2.set_cpu_present(cpu,true)cpu處于present狀態 ->do_pre_smp_initcalls//多核啟動之前的調用initcall回調 ->smp_init//smp初始化kernel/smp.c會啟動其他從處理器
我們主要關注兩個函數:psci_dt_init和smp_init psci_dt_init是解析設備樹,設置操作函數,smp_init用于啟動從處理器。
->psci_dt_init()//drivers/firmware/psci.c: ->init_fn() ->psci_0_1_init()//設備樹中compatible="arm,psci"為例 ->get_set_conduit_method()//根據設備樹method屬性設置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;//設置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函數做從處理器啟動:
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(這是從處理器啟動的函數) ->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}
啟動從處理的時候最終調用到psci的cpu操作集的cpu_psci_cpu_boot函數,會調用上面的psci_cpu_on,最終調用smc,傳遞第一個參數為cpu的id標識啟動哪個cpu,第二個參數為從處理器啟動后進入內核執行的地址secondary_entry(這是個物理地址)。
所以綜上,最后smc調用時傳遞的參數為arm_smccc_smc(0xC4000003, cpuid, secondary_entry, arg2, 0, 0, 0, 0, &res)。這樣陷入el3之后,就可以啟動對應的從處理器,最終從處理器回到內核(el3->el1),執行secondary_entry處指令,從處理器啟動完成。
可以發現psci的方式啟動從處理器的方式相當復雜,這里面涉及到了el1到安全的el3的跳轉,而且涉及到大量的函數回調,很容易繞暈。
(其實為了安全,所以啟動從核開核這個操作必須在EL3,開了以后,就可以會EL1,因為已經在EL3給你了準確安全的啟動位置了。)
在這里插入圖片描述
6、從處理器啟動EL1做了什么?
其實這里就和spin-table比較相似了
無論是spin-table還是psci,從處理器啟動進入內核之后都會執行secondary_startup:
719secondary_startup: 720/* 721|*CommonentrypointforsecondaryCPUs. 722|*/ 723bl__cpu_secondary_check52bitva 724bl__cpu_setup//initialiseprocessor 725adrpx1,swapper_pg_dir//設置內核主頁表 726bl__enable_mmu//使能mmu 727ldrx8,=__secondary_switched 728brx8 729ENDPROC(secondary_startup) || / 731__secondary_switched: --732adr_lx5,vectors//設置從處理器的異常向量表 --733msrvbar_el1,x5 --734isb//指令同步屏障保證屏障前面的指令執行完 735 --736adr_lx0,secondary_data//獲得主處理器傳遞過來的從處理器數據 --737ldrx1,[x0,#CPU_BOOT_STACK]//getsecondary_data.stack獲得棧地址 738movsp,x1//設置到從處理器的sp --739ldrx2,[x0,#CPU_BOOT_TASK]//獲得從處理器的tskidle進程的tsk結構, --740msrsp_el0,x2//保存在sp_el0arm64使用sp_el0保存當前進程的tsk結構 741movx29,#0//fp清0 742movx30,#0//lr清0 --743bsecondary_start_kernel//跳轉到c程序繼續執行從處理器初始化 744ENDPROC(__secondary_switched)
__cpu_up中設置了secondary_data結構中的一些成員:
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;//執行的進程描述符 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);
跳轉到secondary_start_kernel這個C函數繼續執行初始化:
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的引用計數加1 201current->active_mm=mm;//設置idle借用的mm結構 202 203/* 204|*TTBR0isonlyusedfortheidentitymappingatthisstage.Makeit 205|*pointtozeropagetoavoidspeculativelyfetchingnewentries. 206|*/ 207cpu_uninstall_idmap(); 208 209preempt_disable();//禁止內核搶占 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拓撲 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());//打印內核log 243update_cpu_boot_status(CPU_BOOT_SUCCESS); 244set_cpu_online(cpu,true);//設置cpu狀態為online 245complete(&cpu_running);//喚醒主處理器的完成等待函數,繼續啟動下一個從處理器 246 247local_daif_restore(DAIF_PROCCTX);//從處理器繼續往下執行 248 249/* 250|*OK,it'sofftotheidlethreadforus 251|*/ 252cpu_startup_entry(CPUHP_AP_ONLINE_IDLE);//idle進程進入idle狀態 253}
實際上,可以看的當從處理器啟動到內核的時候,他們也需要設置異常向量表,設置mmu等,然后執行各自的idle進程(這些都是一些處理器強相關的初始化代碼,一些通用的初始化都已經被主處理器初始化完),當cpu負載均衡的時候會放置一些進程到這些從處理器,然后進程就可以再這些從處理器上歡快的運行。
寫到這里,關于arm64平臺的多核啟動已經介紹完成,可以發現里面還是會涉及到很多細節,源碼散落在uboot,atf,kernel等源碼目錄中,多核啟動并不是很神秘,都是需要告訴從處理器從那個地方開始取值執行,然后從處理器進入內核后需要自身做一些必要的初始化,就進入idle狀態等待有任務來調度.
arm64平臺使用psci更為廣泛。
審核編輯:湯梓紅
-
處理器
+關注
關注
68文章
19407瀏覽量
231181 -
芯片
+關注
關注
456文章
51170瀏覽量
427238 -
cpu
+關注
關注
68文章
10902瀏覽量
213002 -
SMP
+關注
關注
0文章
76瀏覽量
19746
原文標題:參考文章
文章出處:【微信號:處芯積律,微信公眾號:處芯積律】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論