Android 筆記-Linux Kernel SMP (Symmetric Multi-Processors) 開機流程解析 Part(2) Linux Kernel SMP zImage到start_kernel流程.
hlchou@mail2000.com.tw
by loda.
Android/Linux Source Code Tags
App BizOrz
BizOrz.COM
BizOrz Blog
Mmmmmm,必須承認,我把這篇文章寫的有點囉嗦,以前在Linux Kernel上的工作,沒有留下太多的筆記,抽象的概念,容易隨著下一個產品或是技術的開發,成為過往記憶的一部分,這次重新整理,希望以後回來看時,可以很快Pick-up所有的細節,所以在一些枝微末節上,會比較嘮叨.
也因此,如果你原本就對ARM與Linux Kernel原始碼有一定的基礎,可能讀起本文來會比較輕鬆些. 若是有些部分,筆者探究的太過細節,還請各位見諒.
Linux Kernel對於SMP的支援有三種組合,
1,不支援SMP的Linux Kernel
2,支援SMP的Linux Kernel (關閉SMP對於單核心UniProcessor的支援,SMP_ON_UP=n)
3,支援SMP/UP的Linux Kernel (開啟SMP對於單核心UniProcessor的支援,SMP_ON_UP=y),並在啟動時,如果偵測到為UniProcessor時,會自我修正不能在non-SMP運作的指令.
目前SMP_ON_UP選項只在ARM處理器上有.
其實,整個Linux Kernel中包括PageSet,Process ID Map與相關的資料結構,都會參考目前系統中的處理器個數,來做出對應的配置,也就是說Linux Kernel對於支援多核心的架構,已經是相當的內化(骨子裡...就是會考慮到多核心的情況.),並蘊含在許多核心模組的設計上. 也因此,在整理本文的過程中,收獲最大的也是筆者自己對於SMP架構與Linux Kernel模組的藍圖. 並希望對閱讀本文的人也能有所助益.
本文主要從zImage開始到start_kernel完畢(rest_init除外),並以Tegra平台為主要參考,由於並非所有函式都在筆者平台上被參考到,在說明中也會略過,只選擇在這平台上比較重要的部份.
由於筆者時間受限,本系列文章會分次刊登,還請見諒.
Linux Kernel Image
依據開發的需求,Linux Kernel Image可以編譯為 zImage (Compressed kernel image),Image (Uncompressed kernel image),xipImage(XIP(eXecution In Place) kernel image),uImage(U-Boot wrapped zImage)與 bootpImage( Combined zImage and initial RAM disk).
若對Linux Kernel編譯過程有興趣,可在編譯時加上 KBUILD_VERBOSE=1,讓quiet參數為空白,可把編譯過程吐到Console中,便於觀察.
不只是ARMv32,還支援Thumb2的 Linux Kernel Image
如果所選擇的處理器是ARMv7 (也就是Cortex的架構),可以透過勾選Experimental程式碼的選項,就可把Linux Kernel以Thumb2的方式進行編譯.
有關ARMv32與Thumb2效能的比較可以參考這篇在ARM工作的Richard Phelan所寫的文章Improving ARM Code Density and Performance (http://www.cs.uiuc.edu/class/fa05/cs433ug/PROCESSORS/Thumb2.pdf), 以C Code實作同樣的功能來說,編譯為Thumb2最高可以達到98%的ARM指令及效能,程式碼本身所需的記憶體空間只佔原本ARM程式碼的74%. 對記憶體受限的嵌入式裝置,以Thumb2 16/32 bits混合的程式碼可以得到較佳的 Performance/Code Size的C/P值.
在選擇Linux Kernel選單時,只要進行如下勾選即可,
General setup --->Prompt for development and/or incomplete code/drivers
與
Kernel Features --->Compile the kernel in Thumb-2 mode
目前筆者並未驗證過這部份的代碼,僅作為有興趣的開發者參考資訊.
Linux Kernel編譯時所產生的Relocatable Object File.
當一個編譯系統比較龐大時,如果是一次要Link大量的.o或.a檔時,要解決這些Symbol Resolve會需要的記憶體與運算成本,也會對應的提高,Linux Kernel有使用GCC relocatable output的機制,讓個別模組可以先進行 Symbol Resolve,節省最後Kernel Image產生的運算資源. 簡要說明如下
1, 編譯過程中,會透過arm-eabi-ld (GCC Linker) 搭配 “-r” 產生”relocatable output”,例如:
arm-eabi-ld -EL -r -o drivers/tty/vt/built-in.o drivers/tty/vt/vt_ioctl.o drivers/tty/vt/vc_screen.o drivers/tty/vt/selection.o drivers/tty/vt/keyboard.o drivers/tty/vt/consolemap.o drivers/tty/vt/consolemap_deftbl.o drivers/tty/vt/vt.o drivers/tty/vt/defkeymap.o
會把 drivers/tty/vt下的.o檔案,產生出一個在內部已經做過Symbol Resolved動作的集合Object檔案 built-in.o,透過objdump我們先檢視在目錄下的vt.o檔案中呼叫外部函式vt_ioctl,
arm-eabi-objdump -t vt.o|grep "vt_ioctl"
00000000 *UND* 00000000 vt_ioctl
由於該函式的實作不在vt.c中,因此在編譯後,.o檔案中的Symbol會被標示為 “Undefined”,再來檢視實作該函式的vt_ioctl.c產生的Object檔案,如下所示
arm-eabi-objdump -t vt_ioctl.o|grep "vt_ioctl"
vt_ioctl.o: file format elf32-littlearm
00000000 l df *ABS* 00000000 vt_ioctl.c
00000558 g F .text 00001ccc vt_ioctl
可以看到該函式在vt_ioctl.c編譯後,是在text節區中,且屬性為 global,可供外部的.o檔案連結.
最後我們檢視drivers/tty/vt目錄下產生的built-in.o,
arm-eabi-objdump -t built-in.o|grep "vt_ioctl"
00000000 l df *ABS* 00000000 vt_ioctl.c
00000558 g F .text 00001ccc vt_ioctl
可以看到,最後產生的集合檔案built-in.o,包含了vt.o與vt_ioctl.o,且在其中這些.o之間的Symbol交互參考的問題,都已經在編譯階段被解決.
想像一下,如果一次有5000個Object檔案或是.a檔案(.a檔案,等於是Object檔案的Archive,可以分辨.o檔案的集合性,但其中所包含的.o並沒有彼此先進行Symbol Resolved,因此,所花的時間成本跟.o是一樣的.),要去做Symbol Resolved,這要建立的對應表格複雜度,跟我先把這5000檔案所在的20個目錄,針對這20個目錄先把其中包含的Object檔案做內部的Symbol Resolved,減少要解決的Symbol個數與要建立的查表範圍,就可以顯著的加速最後要連結成Image的運算時間與記憶體成本.
參考平台Tegra2的記憶體配置
筆者以Linux Kernel 2.6.39並選擇ARM Tegra2的平台為例 (NVIDIA Tegra (ARCH_TEGRA)),關於這處理器的基本資訊為
1,兩個Cortex A9處理器
2,一個Audio/Video ARM7處理器
3,實體記憶體SDRAM定址在 0x00000000 ( AP20_BASE_PA_SDRAM)
4,OnChip 256KB SRAM定址在0x40000000 (AP20_BASE_PA_SRAM)
5,NOR Flash的定址在0xD0000000 ( AP20_BASE_PA_NOR_FLASH)
有關NVidia Tegra2的資訊可以參考http://developer.nvidia.com/tegra/taxonomy/term/36/0
有關ALT_UP對SMP到UP程式碼的修正
由於Linux Kernel SMP的實作,在ARM的架構下會有SMP與單核心共用函式實作程式碼的差異,在檔案 arch/arm/include/asm/assembler.h中,有實現ALT_SMP與ALT_UP兩個巨集,例如在程式碼中使用ALT_UP,該指令就會被加入Section .alt.smp.init 如下所示.
#define ALT_UP(instr...) \
.pushsection ".alt.smp.init", "a" ;\
.long 9998b ;\
9997: instr ;\
.if . - 9997b != 4 ;\
.error "ALT_UP() content must assemble to exactly 4 bytes";\
.endif ;\
.popsection
藉此我們可以在同樣的函式中,根據單核心與SMP實作的差異,透過ALT_SMP與ALT_UP來把兩種版本的程式碼置入,以開啟SMP與SMP_ON_UP的實作來說,屬於SMP的實作,會被編譯在原本執行函式的內容中,而屬於單核心版本的實作,則會被編譯到Section .alt.smp.init下,參考如下程式碼的例子
在檔案arch/arm/mm/proc-v7.S中,
ALT_SMP(orr r0, r0, #TTB_FLAGS_SMP)
ALT_UP(orr r0, r0, #TTB_FLAGS_UP)
…..
cpu_resume_l1_flags:
ALT_SMP(.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_FLAGS_SMP)
ALT_UP(.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_FLAGS_UP)
或檔案arch/arm/mm/tlb-v7.S中,
ALT_SMP(mcr p15, 0, r0, c8, c3, 1) @ TLB invalidate U MVA (shareable)
ALT_UP(mcr p15, 0, r0, c8, c7, 1) @ TLB invalidate U MVA
我們可以看到依據SMP與單核心版本的差異,實作上會在同一處程式碼中同時實現兩種版本的程式碼,並透過ALT_UP把單核心的版本在編譯階段放到Section .alt.smp.init中,並且會在每4bytes程式碼位址後,記錄對應4bytes單核心版本指令集,以便修正時參考,如下例子
0xc001858c <__smpalt_begin>:
…..
0xc0018634: c0029fd8 .word 0xc0029fd8 //4 bytes 要取代的目標記憶體位址
0xc0018638: ee080f37 mcr 15, 0, r0, cr8, cr7, {1} //4bytes UniProcessor版本指令
0xc001863c: c0029fec .word 0xc0029fec
0xc0018640: ee07cfd5 mcr 15, 0, ip, cr7, cr5, {6}
0xc0018644: c002a00c .word 0xc002a00c
0xc0018648: ee080f37 mcr 15, 0, r0, cr8, cr7, {1}
…..
在最後的Link階段,會把Section .alt.smp.init放在Symbol __smpalt_begin與__smpalt_end之中,因此在程式碼執行階段,就可以透過這兩個Symbol取得 Section .alt.smp.init中所包含單核心程式碼的內容與記憶體範圍.
在Linux Kernel啟動後會呼叫函式__fixup_smp,如果判斷目前是在單核心平台上,就會把在__smpalt_begin到__smpalt_end記憶體範圍的單核心程式碼依據其對應的記憶體位址,進行修正動作.
運作概念如下圖所示
從zImage開始,啟動Linux Kernel
接下來,以Linux Kernel zImage為例,簡要說明執行流程,也借此對產生的Linux Kernel Image有一個概念,有關SMP的部份,會在流程走到時,著重說明
編譯完成後,在根目錄下的vmlinux會透過如下的命令產生出來,其中有關記憶體位置與節區的配置參考檔案為 arch/arm/kernel/vmlinux.lds
arm-eabi-ld -EL -p --no-undefined -X --build-id -o vmlinux -T arch/arm/kernel/vmlinux.lds arch/arm/kernel/head.o arch/arm/kernel/init_task.o init/built-in.o --start-group usr/built-in.o arch/arm/kernel/built-in.o arch/arm/mm/built-in.o arch/arm/common/built-in.o arch/arm/mach-tegra/built-in.o kernel/built-in.o mm/built-in.o fs/built-in.o ipc/built-in.o security/built-in.o crypto/built-in.o block/built-in.o arch/arm/lib/lib.a lib/lib.a arch/arm/lib/built-in.o lib/built-in.o drivers/built-in.o sound/built-in.o firmware/built-in.o net/built-in.o --end-group .tmp_kallsyms2.o
(關於 .tmp_vmlinux1 與 .tmp_vmlinux2的產生,在此先略過)
之後,執行如下命令把ELF格式的vmlinux轉為 Binary 格式的Image
arm-eabi-objcopy -O binary -R .comment -S vmlinux arch/arm/boot/Image
並執行如下命令把 Linux Kernel Binary Image轉為壓縮檔案
cat arch/arm/boot/compressed/../Image | gzip -f -9 > arch/arm/boot/compressed/piggy.gzip
參考arch/arm/boot/compressed/piggy.gzip.S原始碼
.section .piggydata,#alloc
.globl input_data
input_data:
.incbin "arch/arm/boot/compressed/piggy.gzip"
.globl input_data_end
input_data_end:
可以知道在編譯arch/arm/boot/compressed/piggy.gzip.S產生arch/arm/boot/compressed/piggy.gzip.o時,就會把壓縮後的Linux Kernel Image " arch/arm/boot/compressed/piggy.gzip",一併產生在piggy.gzip.o中的Symbol input_data與input_data_end之間.
然後,把壓縮檔跟解壓縮的部份,連結產生 compressed目錄下的vmlinux (記憶體起點為0x00000000,也就是對應到Tegra2外部記憶體的起點),執行的Link指令如下所示
arm-eabi-ld -EL --defsym _image_size=1602596 --defsym zreladdr=0x00008000 -p --no-undefined -X -T arch/arm/boot/compressed/vmlinux.lds arch/arm/boot/compressed/head.o arch/arm/boot/compressed/piggy.gzip.o arch/arm/boot/compressed/misc.o arch/arm/boot/compressed/decompress.o arch/arm/boot/compressed/lib1funcs.o -o arch/arm/boot/compressed/vmlinux
然後,執行如下命令把帶有壓縮後的vmlinux與解壓縮程式的ELF格式vmlinux轉為 Binary 格式的zImage
arm-eabi-objcopy -O binary -R .comment -S arch/arm/boot/compressed/vmlinux arch/arm/boot/zImage
如此,就完成Linux Kernel Image的產生.
其中有關zImage執行的實體記憶體位址可以透過CONFIG_ZBOOT_ROM_TEXT與CONFIG_ZBOOT_ROM_BSS設定.
而Linux Kernel解壓縮的位址會在 CONFIG_ZBOOT_ROM_TEXT + 16kbytes的位址,以這例子來說就是 0x00008000. 這是在最後產生arch/arm/boot/compressd/vmlinux時,透過 “--defsym zreladdr=0x00008000” 產生zreladdr Syombol傳遞給zImage.
可以參考 boot/compressed/Makefile中
LDFLAGS_vmlinux += --defsym zreladdr=$(ZRELADDR)
而 ZRELADDR是在arch/arm/boot/Makefile 中設定的
ZRELADDR := $(zreladdr-y)
其中,zreladdr-y會是在每個Machine對應的目錄下的Makefile.boot被定義,例如Tegra2是在檔案 arch/arm/mach-tegra/Makefile.boot中,以如下方式定義zreladdr-y
zreladdr-$(CONFIG_ARCH_TEGRA_2x_SOC) := 0x00008000
同時,對解壓縮的Kernel Image執行的虛擬與實體記憶體對應,必須滿足如下條件
ZRELADDR == virt_to_phys(PAGE_OFFSET + TEXT_OFFSET)
因為如此,解壓縮的Linux Kernel在虛擬記憶體中的位址就必須是0xc0008000 對應到實體記憶體中的位址會是 0x00008000. 如果Kernel Space的虛擬記憶體空間有調整的話(例如從 0xc0000000調整為0x80000000,就會變成 0x80008000 ↔ 0x00008000).
Linux Kernel Image在虛擬記憶體的運作位置可以透過 xx訂定,一般而言都是給User-Space 3GB的範圍,Kernel Space為1GB的範圍.
CONFIG_PAGE_OFFSET=0xC0000000
要進一步探討Linux Kernel啟動流程,我們可以透arch/arm/boot/compressed/vmlinux.lds了解zImage的啟動流程,
1,節區.text產生的Binary Symbol有
a,<lext.1135> ("static const unsigned short lext" in lib/zlib_inflate/inftrees.c)
b,<lbase.1134> ("static const unsigned short lbase" in lib/zlib_inflate/inftrees.c)
c,<dext.1137> ("static const unsigned short dext" in lib/zlib_inflate/inftrees.c)
d,<dbase.1136> ("static const unsigned short dbase" in lib/zlib_inflate/inftrees.c)
e,<lenfix.1621> ("static const code lenfix" in lib/zlib_inflate/inffixed.h)
f,<distfix.1622> ("static const code distfix" in lib/zlib_inflate/inffixed.h)
....etc
2,會把壓縮後的Linux Kernel piggy.gzip 放在.text節區中Symbol <input_d