ARM多核引導過程(轉)


轉載:https://blog.csdn.net/cs0301lm/article/details/41078599?spm=1035.2023.3001.6557&utm_medium=distribute.pc_relevant_bbs_down.none-task-blog-2~default~OPENSEARCH~default-2.nonecase&depth_1-utm_source=distribute.pc_relevant_bbs_down.none-task-blog-2~default~OPENSEARCH~default-2.nonecase

 

 當電源按鈕按下后,到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 進程執行,並在終端上顯示很多信息,並且最后它把控制權交給終端,停留在激活狀態。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM