Android 筆記-Linux Kernel SMP
hlchou@mail2000.com.tw
by loda.
Mmmmmm,必須承認,我把這篇文章寫的有點囉嗦,以前在Linux Kernel上的工作,沒有留下太多的筆記,抽象的概念,容易隨著下一個產品或是技術的開發,成為過往記憶的一部分,這次重新整理,希望以後回來看時,可以很快Pick-up所有的細節,所以在一些枝微末節上,會比較嘮叨.
也因此,如果你原本就對ARM與Linux Kernel原始碼有一定的基礎,可能讀起本文來會比較輕鬆些. 若是有些部分,筆者探究的太過細節,還請各位見諒.
Linux Kernel對於SMP的支援有三種組合,
1,不支援SMP的Linux Kernel
2,支援SMP的Linux Kernel
3,支援SMP/UP的Linux Kernel
目前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的
如果所選擇的處理器是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%.
在選擇Linux Kernel選單時,只要進行如下勾選即可,
General setup
與
Kernel Features
目前筆者並未驗證過這部份的代碼,僅作為有興趣的開發者參考資訊.
Linux Kernel編譯時所產生的Relocatable Object File.
當一個編譯系統比較龐大時,如果是一次要Link大量的.o或.a檔時,要解決這些Symbol Resolve會需要的記憶體與運算成本,也會對應的提高,Linux Kernel有使用GCC
1, 編譯過程中,會透過arm-eabi-ld (GCC Linker) 搭配 “-r” 產生”relocatable output”,例如:
arm-eabi-ld -EL
會把 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
由於該函式的實作不在vt.c中,因此在編譯後,.o檔案中的Symbol會被標示為 “Undefined”,再來檢視實作該函式的vt_ioctl.c產生的Object檔案,如下所示
arm-eabi-objdump -t vt_ioctl.o|grep "vt_ioctl"
vt_ioctl.o:
00000000 l
00000558 g
可以看到該函式在vt_ioctl.c編譯後,是在text節區中,且屬性為 global,可供外部的.o檔案連結.
最後我們檢視drivers/tty/vt目錄下產生的built-in.o,
arm-eabi-objdump -t built-in.o|grep "vt_ioctl"
00000000 l
00000558 g
可以看到,最後產生的集合檔案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定址在 0×00000000
4,OnChip 256KB SRAM定址在0×40000000 (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
9997:
.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
ALT_UP(orr
…..
cpu_resume_l1_flags:
ALT_SMP(.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_FLAGS_SMP)
ALT_UP(.long
或檔案arch/arm/mm/tlb-v7.S中,
ALT_SMP(mcr
ALT_UP(mcr
我們可以看到依據SMP與單核心版本的差異,實作上會在同一處程式碼中同時實現兩種版本的程式碼,並透過ALT_UP把單核心的版本在編譯階段放到Section .alt.smp.init中,並且會在每4bytes程式碼位址後,記錄對應4bytes單核心版本指令集,以便修正時參考,如下例子
0xc001858c <__smpalt_begin>:
…..
0xc0018634: c0029fd8
0xc0018638: ee080f37
0xc001863c: c0029fec
0xc0018640: ee07cfd5
0xc0018644: c002a00c
0xc0018648: ee080f37
…..
在最後的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
(關於
之後,執行如下命令把ELF格式的vmlinux轉為 Binary 格式的Image
arm-eabi-objcopy -O binary -R .comment -S
並執行如下命令把 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:
.incbin "arch/arm/boot/compressed/piggy.gzip"
.globl
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
arm-eabi-ld -EL
然後,執行如下命令把帶有壓縮後的vmlinux與解壓縮程式的ELF格式vmlinux轉為 Binary 格式的zImage
arm-eabi-objcopy -O binary -R .comment -S
如此,就完成Linux Kernel Image的產生.
其中有關zImage執行的實體記憶體位址可以透過CONFIG_ZBOOT_ROM_TEXT與CONFIG_ZBOOT_ROM_BSS設定.
而Linux Kernel解壓縮的位址會在 CONFIG_ZBOOT_ROM_TEXT + 16kbytes的位址,以這例子來說就是 0×00008000. 這是在最後產生arch/arm/boot/compressd/vmlinux時,透過 “–defsym zreladdr=0×00008000” 產生zreladdr Syombol傳遞給zImage.
可以參考 boot/compressed/Makefile中
LDFLAGS_vmlinux += –defsym zreladdr=$(ZRELADDR)
而 ZRELADDR是在arch/arm/boot/Makefile 中設定的
ZRELADDR