1.開場白
環境:
uboot版本:uboot-2020.01
內核源碼:linux-5.0
ubuntu版本:20.04.1
ATF版本:2.1
代碼閱讀工具:vim+ctags+cscope
一般嵌入式系統使用的都是對稱多處理器(Symmetric Multi-Processor, SMP)系統,包含了多個cpu, 這幾個cpu都是相同的處理器,如4核Contex-A53。但是在系統 啟動階段他們的地位并不是相同的,其中core0是主cpu(也叫引導處理器),其他core是從cpu(也叫輔處理器),引導cpu負責執行我們的啟動加載程序如uboot,以及初始化內核,系統初始化完成之后主core會啟動從處理器。
一般主處理器啟動從處理器有以下三種:
**(2).spin-table **
(3).PSCI
第一種ACPI是高級配置與電源接口(Advanced Configuration and Power Interface)一般在x86平臺用的比較多,而后兩種spin-table(自旋表)和PSCI(電源狀態協調協議 Power State Coordination)會在arm平臺上使用,本系列 主要講解后兩種 。主要內容分為上下兩篇如下:
上篇:
1.開場白
2.cpu啟動的一些基本概念
3.支持spin-table情況
下篇:
4.支持psci情況
5.從處理器啟動進入內核世界之后做了些什么
6.最后說兩句
2.cpu啟動的一些概念
1)cpu啟動的含義:cpu可以從內存中取指、譯碼、執行,當然內存可以是soc片內的sram,也可以是ddr。
2)我們要知道,程序為何可以在多個cpu上并發執行:他們有各自獨立的一套寄存器,如:程序計數器pc,棧指針寄存器sp,通用寄存器等,可以獨自 取指、譯碼、執行,當然內存和外設資源是共享的,多核環境下當訪問臨界區 資源一般 自旋鎖來防止競態發生。
3)soc啟動流程:soc啟動的一般會從片內的rom, 也叫bootrom開始執行第一條指令,這個地址是系統默認的啟動地址,會在bootrom中由芯片廠家固化一段啟動代碼來加載啟動bootloader到片內的sram,啟動完成后的bootloader除了做一些硬件初始化之外做的最重要的事情是初始化ddr,因為sram的空間比較小所以需要初始化擁有大內存 ddr,最后會從網絡/usb下載 或從存儲設備分區上加載內核到ddr某個地址,為內核傳遞參數之后,然后bootloader就完成了它的使命,跳轉到內核,就進入了操作系統內核的世界。
4)linux內核啟動流程:bootloader將系統的控制權交給內核之后,他首先會進行處理器架構相關初始化部分,如設置異常向量表,初始化mmu(之后內核就從物理地址空間進入了虛擬地址空間的世界,一切是那么的虛無縹緲,又是那么的恰到好處)等等,然后會清bss段,設置sp之后跳轉到C語言部分進行更加復雜通用的初始化,其中會進行內存方面的初始化,調度器初始化,文件系統等內核基礎組件 初始化工作,隨后會進行關鍵的從處理器的引導過程,然后是各種實質性的設備驅動的初始化,最后 創建系統的第一個用戶進程init后進入用戶空間執行用戶進程宣誓內核初始化完成,可以進程正常的調度執行。
5)系統初始化階段大多數都是主處理器做初始化工作,所有不用考慮處理器并發情況,一旦從處理器被bingup起來,調度器和各自的運行隊列準備就緒,多個任務就會均衡到各個處理器,開始了并發的世界,一切是那么的神奇。
3.支持spin-table情況
了解了關于cpu啟動的一些基本概念,下面開始我們的正題,講解arm64常用的兩種cpu啟動方式。首先,我們來看一下比較簡單的自旋表的方式啟動從處理器。
從bootloader說起(以uboot為例):首先,上電后主處理器和從處理器都會啟動,執行uboot,從uboot的_start的匯編代碼開始執行,主處理器在uboot中歡快的執行后啟動內核,進入內核執行,而從處理器會執行到spin_table_secondary_jump中(注意:之前執行的代碼,設置的寄存器都是各cpu獨立的寄存器)
arch/arm/cpu/armv8/start.S:
19 .globl _start
20 _start:
...
151 #if defined(CONFIG_ARMV8_SPIN_TABLE) && !defined(CONFIG_SPL_BUILD)
152 branch_if_master x0, x1, master_cpu //判斷是否為主cpu(core0),是跳轉到master_cpu,否則往下走
153 b spin_table_secondary_jump //跳轉執行
...
arch/arm/cpu/armv8/spin_table_v8.S:
9 ENTRY(spin_table_secondary_jump)
10 .globl spin_table_reserve_begin
11 spin_table_reserve_begin:
12 0: wfe
13 ldr x0, spin_table_cpu_release_addr
14 cbz x0, 0b
15 br x0
16 .globl spin_table_cpu_release_addr
17 .align 3
18 spin_table_cpu_release_addr:
19 .quad 0
20 .globl spin_table_reserve_end
21 spin_table_reserve_end:
22 ENDPROC(spin_table_secondary_jump)
在spin_table_secondary_jump中:首先會 執行wfe指令,使得從處理器睡眠等待 。如果被喚醒,則從處理器會判斷spin_table_cpu_release_addr這個地址是否為0,為0則繼續跳轉到wfe處繼續睡眠,否則跳轉到spin_table_cpu_release_addr指定的地址處執行。
那么這個地址什么時候會被設置呢?答案是:主處理器在uboot中讀取設備樹的相關節點屬性獲得,我們來看下如何獲得。 執行路徑為:
do_bootm_linux
- >boot_prep_linux
- >image_setup_linux
- >image_setup_libfdt
- >arch_fixup_fdt
- >spin_table_update_dt
在spin_table_update_dt函數中做了幾件非常重要的事情:
arch/arm/cpu/armv8/spin_table.c:
11 int spin_table_update_dt(void *fdt)
12 {
13 int cpus_offset, offset;
14 const char *prop;
15 int ret;
16 unsigned long rsv_addr = (unsigned long)&spin_table_reserve_begin;
17 unsigned long rsv_size = &spin_table_reserve_end -
18 &spin_table_reserve_begin;
19 //獲取設備樹的cpus節點的偏移
20 cpus_offset = fdt_path_offset(fdt, "/cpus");
21 if (cpus_offset < 0)
22 return -ENODEV;
23 //尋找每一個device_type屬性為cpu的節點
24 for (offset = fdt_first_subnode(fdt, cpus_offset);
25 | offset >= 0;
26 | offset = fdt_next_subnode(fdt, offset)) {
27 prop = fdt_getprop(fdt, offset, "device_type", NULL);
28 if (!prop || strcmp(prop, "cpu"))
29 continue;
30
31 /*
32 |* In the first loop, we check if every CPU node specifies
33 |* spin-table. Otherwise, just return successfully to not
34 |* disturb other methods, like psci.
35 |*///獲得enable-method屬性,比較屬性值是否為 "spin-table"(即是使用自旋表啟動方式)
36 prop = fdt_getprop(fdt, offset, "enable-method", NULL);
37 if (!prop || strcmp(prop, "spin-table"))
38 return 0;
39 }
40
41 for (offset = fdt_first_subnode(fdt, cpus_offset);
42 | offset >= 0;
43 | offset = fdt_next_subnode(fdt, offset)) {
//找到cpu節點
44 prop = fdt_getprop(fdt, offset, "device_type", NULL);
45 if (!prop || strcmp(prop, "cpu"))
46 continue;
47 //重點:設置cpu-release-addr屬性值為spin_table_cpu_release_addr的地址!
48 ret = fdt_setprop_u64(fdt, offset, "cpu-release-addr",
49 (unsigned long)&spin_table_cpu_release_addr);
50 if (ret)
51 return -ENOSPC;
52 }
53 //設置設備樹的保留內存 :添加一個內存區域為16和17行描述的地址范圍(這是物理地址)
54 ret = fdt_add_mem_rsv(fdt, rsv_addr, rsv_size);
55 if (ret)
56 return -ENOSPC;
57
58 printf(" Reserved memory region for spin-table: addr=%lx size=%lx\\n",
59 | rsv_addr, rsv_size);
60
61 return 0;
62 }