當電源按鈕按下后,到shell命令起來,能理解4個CPU核到底發生了什么是非常重要的,嵌入Linux內核的引導過程和pc是不一樣的, 原因是環境設置和可用硬件都不一樣了。比如,嵌入式沒有硬盤和PC BIOS,取而代之的是一個引導監控器和flash 盤。所以兩者基本的 差一點是“找內核並裝載它”,一旦內核裝載到了內存,所有CPU架構的事件處理過程和負載分配都相同。 Linux的引導過程分三個階段: 1、加電(按下電源按鈕); 2、System Startup Boot Monitor; 3、Bootloader uboot; 4、ARM Linux Startup; 5、進入shell command狀態;
說明: 1、當按下power on鍵,引導監視器代碼運行(從一個預先定義好的地址NOR flash 內存的0地址); 2、啟動監視器初始化PB11MPCore 硬件周邊設備,然后啟動真正的bootloader U-Boot; 3、在啟動監視器下,利用一個自動腳本,也可以由用戶手動輸入適當的命令來完成U-Boot初始化內存,並copy 壓縮的內核映像(uImage)到主內存, 這個映像可以放在NOR上,MMC上,CompactFlash上或者主機PC上。 4、由ARM11 MPCore來執行; 5、然后傳遞一些初始化參數給內核; 6、內核映像自己解壓自己,並開始初始化自己的數據結構,如建立一些用戶進程,引導所有的CPU核,並且在用戶空間運行命令行環境;
第一章、System startup (Boot Monitor 引導監視器)
1、當系統加電或reset,ARM11 MPCore的所有CPUs取下一條指令(從 restet 向量地址,即NOR flash 的 0x00000000地址)到它們自己的PC寄存器,這個地址放了引導監視器的程序; 2、只有CPU0 繼續執行引導監視器代碼,並且其它CPUs執行WFI 指令,這是一個循環,檢測SYS_FLAGS 寄存器。其它CPUs在Linux 內核引導過程中,才開始執行有意義的代碼,在隨后的ARM Linux一節中有詳細 描述; 引導監視器是一個是由ARM平台庫組成的、標准的ARM 應用程序,它運行在系統引導完成后。 當系統reset的情況下,引導監視器完成下面的動作: a、執行CPU0上的主代碼和其它CPUs上執行WFI指令; b、初始化內存控制器、配置主板外設; c、在內存中建立一個棧; d、Copy自己到主內存DRAM里; e、復位引導內存的映射; f、重新映射和直接訪問依賴於PB11MPCore 的C庫I/O例程,如輸出口UART0 或LCD, 輸入口UART0 或 keyboard) g、NOR flash上如果有就自動運行一個引導腳本,並且把PB11MPCore的面板切換到ON,引導監視器也可以 進到shell命令行的提示符狀態; 所以,基本上引裝在板子上的導監視器應用類似於PC機器的BIOS。它的功能有限,不能引導一個Linux映像。 因此,另一個bootloader需要完成引導過程,它就是U-Boot。U-Boot 代碼編譯成ARM平台格式,並燒到NOR flash 上,最后的步驟是從引導監視器命令行啟動U-Boot映像。這一步也可以用一個腳本或手工輸入適當的命令來做。
第二章、Bootloader(U-Boot)
1、當放在NOR flash的bootloader被引導監視器調用的時候,它還不能訪問系統RAM,因為這個時候內存控制器還 沒有初始化成U-Boot所希望的那樣; 2、U-Boot是如何從flash memory移動自己到主 memory的呢? 3、為了得到能正常工作的C環境並運行初始化代碼,U-Boot需要分配最小的棧,ARM11 MPCore是在鎖定L1 data cache memory的情況下完成的。U-Boot的初始化階段,SDRAM控制器初始化完成前,cache內存用作臨時數據存儲; 4、然后,U-Boot初始化ARM11 MPCore、它的caches和它的SCU; 5、下一步,所有可用的內存bank被初步的映射,並且進行簡單的內存測試,用來確定SDRAM所有banks的大小; 6、最后,bootloader安裝它自己在SDRAM高端(upper end of)區域,並且分配內存用來給malloc()函數用, 和保存全局board信息數據用。 7、在低端內存,異常處理代碼向量被copied進來; 8、最后,建立棧。 在這個階段,第二個bootloader U-Boot是在主內存里的,並且C環境建立了。bootloader先傳遞一些引導參數給內核,然后 准備從預先設置好的地址啟動Linux內核映像。另外,還要初始化串口或控制台給內核用。最后,它調用內核映像,方法是jumping到 start標簽的代碼(arch/arm/boot/compressed/head.s 匯編代碼),這是Linux內核解壓自己的代碼的開始; bootloader能完成很多功能,最小集如下: 1、配置系統主內存: 內核不具備建立和配置RAM的能力和知識,找到並初始化內存是bootloader的任務,內核只負責使用這些內存保存數據。 傳遞物理內存layout給內核是通過ATAG_MEM 參數,下面有具體說明。 2、在確定的的地址裝載內核映像: uImage映像是由一個特定魔數的頭信息和數據區組成,頭信息和數據合起來有一個checksum。在數據區,保存有開始和 結束偏移量,用以確定壓縮映像的長度,以便於知道多大內存需要分配。ARM Linux 內核被定位在主內存的0x7fc0地址。 3、初始化控制台: 因為對所有平台來說,為了debug工具的需要,一個串口控制台是最基本的。bootloader應該在目標板上初始化並使能一個串口, 然后傳遞相關的控制台參數選項給內核,目的是通知內核已經准備好的串口號。 4、初始化啟動參數,並傳遞給內核: bootloader必須以tags的形式傳遞參數,描述setup已經完成,內存的大小和輪廓,可選的各種測試見下表: Tag name Description ATAG_NONE Empty tag used to end list ATAG_CORE First tag used to start list ATAG_MEM Describes a physical area of memory ATAG_VIDEOTEXT Describes a VGA text display ATAG_RAMDISK Describes how the ramdisk will be used in kernel ATAG_INITRD2 Describes where the compressed ramdisk image is placed in memory ATAG_SERIAL 64 bit board serial number ATAG_REVISION 32 bit board revision number ATAG_VIDEOLFB Initial values for vesafb-type framebuffers ATAG_CMDLINE Command line to pass to kernel 5、獲得 ARM Linux的機器類型: bootloader 也會提供機器類型,它是一個系統唯一的數字id。因為它是預定義的,所以它可以被硬編碼代碼中, 否則就從board登記處讀出。機器類型數字可以從ARM-Linux項目網頁上獲取。 6、帶着合適的寄存器值進入內核運行: 最后,開始運行內核之前,ARM11 MPCore寄存器必須合理的設置: a、監管模式(SVC); b、IRQ和FIQ中斷禁止; c、MMU 關閉; d、數據cache 關閉; e、指令cache可以開也可以關; f、CPU register0=0; g、CPU register1=ARM Linux 機器類型; h、CPU register2=傳遞參數的物理地址; 7、uboot啟動中函數調用過程 a、匯編code -> b、board_init_r(init_sequence數組定義了一系列初始化函數,包括arch_cpu_init、board_early_init_f、init_func_i2c、dram_init等) -> c、這里可以進入cmd模式,輸入某個命令來測試u-boot,但默認進入do_cboot -> d、do_cboot(判斷啟動方式選擇進入recovery_mode、fastboot_mode、autodloader_mode、normal_mode之一的啟動模式),但默認進入normal_mode -> e、normal_mode -> f、vlx_nand_boot -> g、vlx_entry -> h、start_linux
第三章、ARM Linux
上述所說,bootloader會跳到壓縮的內核映像代碼,並傳遞一些ATAG標記的初始化參數,壓縮內核是以‘start’標簽開始, 這個標簽定義在arch/arm/boot/compressed/head.s 匯編文件里。從這一步開始,引導過程包含3個階段。 一、內核首先解壓自己; 二、處理器依賴部分的內核代碼執行:主要初始化CPU和內存; 三、最后,獨立於處理器部分的內核代碼執行:即開始ARM多核處理,通過啟動所有ARM11的核,並且初始化所有內核組件和數據結構; 下圖是ARMLinux內核的引導概圖:
Figure 2 ARM Linux kernel boot
啟動分三步: 1、映像解壓: a、U-Boot跳到“start”標簽,標簽在 /arm/boot/compressed/head.S文件里; b、參數通過U-Boot r0保存(CPU架構ID)和r1(ATAG參數列表指針)來傳遞; c、執行cpu架構相關代碼,然后關閉緩存和MMU; d、正確C環境設置; e、分配適當的值給寄存器和堆棧指針。如 r4 = 內核物理起始地址 - sp = 解壓器代碼地址; f、再一次把cache memory打開,cache memory例程遍歷proc_type 列表,找出對應的arm 架構類型, 對ARM11 多核(ARM v6): __armv4_mmu_cache_on 打開 __armv4_mmu_cache_off 關閉 __armv6_mmu_cache_flush 刷新緩存內存到內存 g、確定將要解壓的內核映像是否會覆蓋壓縮的內核映像,並跳到相應的處理例程; h、調用解壓例程:decompress_kernel(),位置在arch/arm/boot/compressed/misc.c 這個函數會在輸出終端上顯示“Uncompressing Linux...“消息; 接着調用gunzip()函數,並顯示“done, booting the kernel” 消息; i、調用__armv6_mmu_cache_flush函數刷新cache 內存的內容到RAM; j、調用__armv4_mmu_cache_off關閉,因為內核初始化例程希望cache在開始的時候是關閉的; k、跳到內存中內核的開始地址,這個地址保存在r4寄存器中; l、內核的起始地址依據不同的平台架構而不同,對PB11MPCore核,保存在arch/arm/mach-realview/Makefile.boot文件里的變量zreladdr-y中, zreladdr-y := 0x00008000 2、處理器(ARM)依賴的內核代碼 內核開始入口點定義在文本文件:arch/arm/kernel/head.S中,解壓器關閉MMU、cache內存、設置好合適的寄存器值后跳到這個入口。共包含以下 事件序列: a、確保CPU運行在超級模式並禁用所有中斷; b、調用 __lookup_processor_type(arch/arm/kernel/head-common.S)查找處理器類型,該函數返回一個指向proc_info_list(變量定義在 arch/arm/mm/proc-v6.S)的指針,這一項是ARM11對應的處理器信息; c、調用 __lookup_machine_type(arch/arm/kernel/head-common.S)查找機器類型,該函數返回一個指向 machine_desc 結構的指針,這一項 專門為 PB11MPCore 核定義的; d、 調用 __create_page_tables建立頁表,個數是內核運行所需要的數量;也就是說在內核執行過程中映射用; e、跳到__v6_setup procedure例程,(arch/arm/mm/proc-v6.S),這個例程初始化CPU0的TLB,cache,MMU; f、使能MMU,函數是__enable_mmu(),它設置一些配置位后調用函數__turn_mmu_on()(arch/arm/kernel/head.S); g、在函數__turn_mmu_on中,會設置合適的控制寄存器值,然后跳到__switch_data,執行第一個函數__mmap_switched() (在arch/arm/kernel/head-common.S文件中); h、在__mmap_switched()中,copy到RAM的數據段和BSS段被清0,最后跳到start_kernel()例程,該函數在init/main.c,這個是LINUX的開始處。 3、處理器(ARM)無關的內核代碼 從這個階段開始,就是公共的處理序列,是獨立於硬件架構的Linux內核引導過程;但仍有一些函數依賴於硬件,並且會覆蓋獨立於硬件的代碼的執行。我們會 專注於多核Linux部分的啟動和cpus的初始化,主要針對ARM11的多核架構而言。 第一步、函數 start_kernel(): (init/main.c) <目前我們在處理器0> a、用local_irq_disable()函數屏蔽CPU0上的中斷 (include/linux/irqflags.h); b、用lock_kernel()函數鎖內核,避免被高優先級中斷搶占(include/linux/smp-lock.h); c、用函數boot_cpu_init() (init/main.c)激活CPU0; d、用函數tick_init() (kernel/time/tick-common.c)初始化內核tick控制器; e、用函數page_address_init() (mm/highmem.c)初始化內存子系統; f、用函數printk(linux_banner) (init/version.c)打印內核版本信息到終端; g、用函數setup_arch(&command_line)設置架構特有的子系統如內存、I/O、處理器、等等,其中command_line 是從U-Boot傳來的參數列表 (arch/arm/kernel/setup.c); 1)在函數setup_arch(&command_line) 中, 我們執行架構相關的代碼。對ARM11 多核, 是調用smp_init_cpus()函數,這個函數初始化cpu的映射。 就是在這一階段,內核知道在ARM11系統架構里,有4個核。(arch/arm/mach-realview/platsmp.c) 2)用函數cpu_init()初始化一個處理器(這一步是指CPU0 ),它復制cache信息,初始化多核相關的信息,並設置每一個CPU的棧(arch/arm/kernel/setup.c); h、用函數setup_per_cpu_areas()設置多處理器環境,這個函數確定單個CPU所需要的內存大小,並分配和初始化4個核分別所需要的內存,這樣一來,每一個CPU有 了自己的區域放置數據;(init/main.c) i、用函數smp_prepare_boot_cpu()來允許正在引導的處理器(CPU0)訪問自己的初始化過的數據;(arch/arm/kernel/smp.c); j、用函數sched_init() (kernel/sched.c)設置Linux調度器; 1)為每一個cpu相應的數據初始化一個運行隊列; 2)用函數init_idle(current, smp_processor_id())為cpu0 fork一個idle線程; k、用函數build_all_zonelists() (mm/page_alloc.c)初始化內存區域:包括DMA, normal, high三個區; l、用函數 parse_early_param() (init/main.c) 和函數 parse_args() (kernel/params.c)解析傳遞過來的命令行參數列表; m、初始化中斷表、GIC、異常處理向量表(用函數init_IRQ() (arch/arm/kernel/irq.c) 和函數 trap_init() (arch/arm/kernel/traps.c)),並為每一個 中斷分配CPU親和力值; n、用函數softirq_init() (kernel/softirq.c)引導CPU(這里是CPU0)能接受由tasklet傳來的通知; o、初始化並運行系統timer,用函數time_init() (arch/arm/kernel/time.c); p、使能CPU0的本地中斷,用函數local_irq_enable() (include/linux/irqflags.h); q、初始化顯示終端,用函數console_init() (drivers/char/tty_io.c); r、找出所有內存區域的free的內存頁總數,用函數mem_init() (arch/arm/mm/init.c); s、初始化內存分配器,用函數kmem_cache_init() (mm/slab.c); t、確定CPU的時鍾的速度,相當於BogoMips的值,用函數calibrate_delay() (init/calibrate.c); u、初始化內核內部組件,如page tables, SLAB caches, VFS, buffers, signals queues, 線程和進程最大值等; v、初始化proc/文件系統,用函數proc_root_init() (fs/proc/root.c); w、調用函數 rest_init()建立進程1; 第二步、函數rest_init(): (init/main.c) a、建立 “init process”,這個進程又叫進程1,所用函數kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND); b、建立內核守護進程,又叫進程2,所用函數是:pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES) (kernel/kthread.c) ,它是所有內 核線程的父親; c、釋放內核鎖kernel lock,它是在start_kernel() 里鎖上的。所用函數unlock_kernel()(include/linux/smp-lock.h); d、執行schedule(),開始運行調度器;(文件kernel/sched.c); e、運行cpu0上的的idle線程,所用函數cpu_idle(),這個線程使CPU0成為調度器,當沒有其它未執行的進程運行在CPU0上時候,它將返回。CPU的idle線程負責節 省電力,並保持低耗電狀態;(arch/arm/kernel/process.c) 第三步、函數kernel_init(): (init/main.c) <進程1> A、通過調用函數smp_prepare_cpus()開始准備SMP環境(arch/arm/mach-realview/platsmp.c); 1、使能CPU0上的本地timer,所用函數local_timer_setup(cpu) (arch/arm/mach-realview/localtimer.c); 2、移動CPU0相應的數據信息到它自己的存儲區,所用函數smp_store_cpu_info(cpu) (arch/arm/kernel/smp.c) ; 3、初始化當前使用的CPU情況,描述了CPU的設置,所用函數cpu_set(i,cpu_present_map)。這將告訴內核,有4個cpu; 4、初始化Snoop控制器,所用函數scu_enable() (arch/arm/mach-realview/platsmp.c); 5、調用函數poke_milo(),它關心正在啟動的次要處理器;(arch/arm/mach-realview/platsmp.c) a、在函數poke_milo()中,它通過清除SYS_FLAGSCLR寄存器的低2位,來觸發其它CPU執行realview_secondary_startup例程,並把 realview_secondary_startup例程的起始地址寫入SYS_FLAGSSET (arch/arm/mach-realview/headsmp.S); b、在realview_secondary例程中,次要CPUs都等待一個同步信號,這個信號將從運行在CPU0上的內核發出,意思是他們都已經准備好被初始化,當所有的 處理器都已經ready,他們將被初始化,所用函數secondary_startup()(arch/arm/mach-realview/headsmp.S) ; c、secondary_startup例程,使次要CPUs做了和CPU0類似的初始化過程;(arch/arm/mach-realview/headsmp.S) 1)切換到超級模式,屏蔽所有中斷; 2)找處理器類型,用函數__lookup_processor_type(),這個函數返回一個指針,指向proc_info_list列表,(ARM11多核架構定義在文件 arch/arm/mm/proc-v6.S中); 3)每一個CPU都使用__cpu_up()函數提供的頁表,__cpu_up下面有講; 4)跳到__v6_setup例程,(arch/arm/mm/proc-v6.S) 初始化對應於每一個CPU的TLB, cache 和MMU; 5)用__enable_mmu 例程使能MMU,它設置一些配置位,然后調用__turn_mmu_on (arch/arm/kernel/head.S); 6)在__turn_mmu_on函數中,會設置一些控制寄存器,然后跳到__secondary_data,這里會執行__secondary_switched例程(arch/arm/kernel/head.S); 7)__secondary_switched例程,會跳到secondary_start_kernel例程( arch/arm/kernel/smp.c),這個例程設置一些棧指針到線程結構里, 線程結構是通過運行在CPU0上的cpu_up函數分配的; 8)secondary_start_kernel (arch/arm/kernel/smp.c) 是次要處理器的官方起始點,它被看作是一個運行在對應的CPU上的內核線程, 在這個線程里,下面的步驟是進一步的初始化動作: 一、用函數cpu_init()初始化CPU,它復制cache信息, 初始化SMP特定的信息, 並建立每個cpu棧(arch/arm/kernel/setup.c); 二、用函數platform_secondary_init(cpu),來和CPU0上的引導線程同步,使能一些對應CPU上的分發中斷控制器接口,如timer、irq; (arch/arm/mach-realview/platsmp.c) 三、用函數local_irq_enable() 和 local_fiq_enable() (include/linux/irqflags.h)使能本地中斷; 四、建立對應CPU上的本地timer,所用函數:local_timer_setup(cpu) (arch/arm/mach-realview/localtimer.c); 五、確定CPU 時鍾的 BogoMips,所用函數: calibrate_delay() (init/calibrate.c); 六、移動對應CPU的數據到它自己的存儲區,所用函數smp_store_cpu_info(cpu) (arch/arm/kernel/smp.c); 七、在二級CPU上執行idle線程,也可以叫0號線程,所用函數cpu_idle()。這個函數當沒有其他等待進程運行在CPUx上時候,返回。 (arch/arm/kernel/process.c) B、調用函數smp_init() (init/main.c) <在CPU0上> 1、引導每一個離線CPU(CPU1,CPU2 and CPU3),所用函數cpu_up(cpu): (arch/arm/kernel/smp.c); a、用函數fork_idle(cpu)手動建立新的idle線程,並指派它到相應的CPU的數據結構; b、分配並初始化內存頁表,並允許二級CPU安全地使能MMU,所用函數pgd_alloc(); c、通知二級CPU到哪里去找它的棧、和頁表; d、引導二級CPU,所用函數boot_secondary(cpu,idle): (arch/arm/mach-realview/platsmp.c); 1)用鎖機制spin_lock(&boot_lock)同步CPU0和二級CPU上的引導進程; 2)通知二級處理器,它可以開始引導內核它自己的部分; 3)用函數smp_cross_call(mask_cpu)發一個軟件中斷,喚醒二級核起來 (include/asm-arm/mach-realview/smp.h); 4)等待二級處理器完成各自的引導和校准,所用函數secondary_start_kernel(),這個函數前面已經講過了; e、在每一個CPU上重復這個過程; 2、在終端上顯示內核信息:“SMP: Total of 4 processors activated (334.02 BogoMIPS)“,所用函數smp_cpus_done(max_cpus) (arch/arm/kernel/smp.c); C、調用函數sched_init_smp() (kernel/sched.c) 1、建立調度器作用域,所用函數arch_init_sched_domains(&cpu_online_map),它將設置多核的拓撲結構(kernel/sched.c); 2、檢查多少個在線CPU存在,並適當地調整調度器粒度值,所用函數sched_init_granularity() (kernel/sched.c); 3、do_basic_setup()函數初始化driver 的模式,用函數driver_init()(drivers/base/init.c)初始化系統控制接口、網絡socket接口, 用init_workqueues()接口支持工作隊列,最后調用do_initcalls ()初始化內嵌的驅動例程;(init/main.c) D、調用函數init_post() (init/main.c); 第四步、函數init_post() (init/main.c): 這里是我們切換到用戶模式的地方,調用下面的序列: run_init_process("/sbin/init"); run_init_process("/etc/init"); run_init_process("/bin/init"); run_init_process("/bin/sh"); 第五步、/sbin/init 進程執行,並在終端上顯示很多信息,並且最后它把控制權交給終端,停留在激活狀態。