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 进程执行,并在终端上显示很多信息,并且最后它把控制权交给终端,停留在激活状态。
原文地址:https://www.cnblogs.com/lh03061238/p/15638985.html