基于PowerPC的Linux内核之旅:第2站-__secondary_start(start_he

前面一篇的early_init执行完成后,CPU启动早期的基本初始化工作算是做完了,这时内核会开始重定向并复制运行,代码如下:

blreloc_offsetmrr26,r3addisr4,r3,KERNELBASE@h/* current address of _start */lisr5,PHYSICAL_START@hcmplw0,r4,r5/* already running at PHYSICAL_START? */bnerelocate_kernel     /*Juan内核重定向,经典启动必备*/

这里的第一句mr是将当前偏移量保存在r26中,后面relocate_kernel会使用。之后内核会判断是否需要重定向,KERNELBASE为内核的虚拟起始地址,PHYSICAL_START为内核的实际起始地址,而内核则必须要从物理地址运行start函数。下面是relocate_kernel的详细代码:

relocate_kernel:addisr9,r26,klimit@ha/* fetch klimit */lwzr25,klimit@l(r9)   /*r25 = kilmit + offset*/addisr25,r25,-KERNELBASE@h    /*最后得到的r25为内核大小*/lisr3,PHYSICAL_START@h   /* 拷贝目标基地址 */lir6,0/* 实际地址,不偏移 */lir5,0x4000/* 先拷贝 16K字节*/blcopy_and_flush addir0,r3,4f@l/* 跳到4f */mtctrr0/* in copy and do the rest. */bctr/* jump to the copy */4:mrr5,r25blcopy_and_flush/* copy the rest */bturn_on_mmu    /*打开MMU*/

机制很简单,就是获取内核大小后,先拷16K,再把剩下的拷过去,然后打开MMU,打开MMU的代码和关闭的类似,这里就不再列举了,看一下拷贝函数copy_and_flush,实现的是拷贝内核到内存物理起始处,并关闭cache。代码如下:

_ENTRY(copy_and_flush)addir5,r5,-4addir6,r6,-44:lir0,L1_CACHE_BYTES/4   /*L1_CACHE_BYTES:0b10000=16*/mtctrr03:addir6,r6,4/* copy a cache line */lwzxr0,r6,r4     /*读单字(4Byte),通过Cache*/stwxr0,r6,r3     /*写单字,从r4加载,存在r3*/bdnz  3b     /*递减计数器,循环每次拷4个字*/dcbstr6,r3/*Data Cache Block Store,再将r3的值写到内存*/syncicbir6,r3/*Instruction Cache Block Invalidate,强制清空指令Cache */cmplw0,r6,r5blt4b     /*循环写内存,直到写完(r6>=r5)*/sync/* additional sync needed on g4 */isyncaddir5,r5,4addir6,r6,4blr

这里的r4是在上面调用relocate_kernel的时候赋的值,为虚拟起始地址-偏移量(偏移量是负的,remember?),即拷贝的源地址。执行完拷贝后,内核会跳转到trun_on_mmu中,该函数在SRR0中写入了start_here的地址,执行完使能MMU后,中断返回指令自动将SRR1更新为MSR,并在新的MSR控制下将SRR0更新为PC指针,实现绝对跳转,处理器即正式跳到start_here中。在此之后,就不再有前面说的链接地址与实际运行地址不同的事情了,即访问变量时也不用加上reloc_offset了。

辛辛苦苦跳了这么久,终于到了执行内核代码的时候了!!这个函数叫start_here,代码比较长,分两段来分析,先看第一段:

start_here:/* ptr to current */lisr2,init_task@horir2,r2,init_task@l   /*默认初始化的task_struct结构体*//* Set up for using our exception vectors */tophys(r4,r2)   /*获取物理地址*/addir4,r4,THREAD/* 初始化线程的CPU相关的状态,THREAD为thread在task_struct中的偏移 */CLR_TOP32(r4)   /*空的??*/mtsprSPRN_SPRG_THREAD,r4    /*将当前线程信息写入SPRG3*/lir3,0mtsprSPRN_SPRG_RTAS,r3/* 写SPRG2为0,使其不在RTAS中 *//* 堆栈初始化 */lisr1,init_thread_union@haaddir1,r1,init_thread_union@llir0,0stwur0,THREAD_SIZE-STACK_FRAME_OVERHEAD(r1)/* 平台相关的初始化操作和配置MMU */mrr3,r31mrr4,r30blmachine_initbl__save_cpu_setupblMMU_init

在该阶段,首先是一个线程和堆栈的初始化过程,Linux在完成MMU和中断向量的初始化后,将创建程序运行于init_thread_union的Stack中,它位于线性映射的第一部分。这是首要工作,即为init_task的运行做准备,先取得Task结构体地址,再将结构体的指针保存在SPRG3(系统专用)中。需要注意的是,PPC32上的数据结构必须要8K个(1<<13)字节对齐,因为堆栈的大小就是8K。

再这之后就是板件平台相关的初始化工作,先来看machine_init,它主要实现两个功能:1、寻找当前所在的板件类型(Probe),进而确定当前处理器的ppc_md结构;2:将前期early_boot的数据保存,分析OF Tree结构,获得当前处理器的内存使用情况,创建MEMBLOCK结构,同时获得当前处理器系统在OF Tree中的其他硬件信息,如CPU频率、内部寄存器基地址、中断系统等。注意,此时的内存依旧只是少量可用。该函数就是简单的几个函数的调用,具体代码就不贴了。先是lockdep_init和udbg_early_init,这两个函数功能很简单,前者用于启动Lock Dependency Validator(内核依赖的关系表),本质上就是建立两个散列表calsshash_table和chainhash_table,并初始化全局变量lockdep_initialized,标志已初始化完成。后者用于初始化早期调试输出,可以通过配置config文件使能其中的一个,一般都是NS16550的串口打印调试,这个不是很懂,待以后研究了。然后是early_init_devtree,它用于启动时对扁平设备树(FDT)的初始化,用来获取内核前期初始化所需的启动参数和cmd_line等引导信息,后面还会调用unflatten_device_tree来解析dts文件,先看下带注释的early_init_devtree实际代码(位于Prom.c中):

void __init early_init_devtree(void *params){phys_addr_t limit;  /*内核所在物理地址*//* 参数params,由machine_init传入,用于存放设备树的有效地址*/initial_boot_params = params;/* 从设备树中获取chosen节点的信息,包括 * platform type,initrd location及size, TCE reserve ...*/of_scan_flat_dt(early_init_dt_scan_chosen, NULL);/*初始化MEMBLOCKs并检索设备树内存节点 */memblock_init();of_scan_flat_dt(early_init_dt_scan_root, NULL);of_scan_flat_dt(early_init_dt_scan_memory_ppc, NULL);/*将bootloader传递的命令行参数保存在boot_command_line中 */strlcpy(boot_command_line, cmd_line, COMMAND_LINE_SIZE);parse_early_param(); /*解析命令行参数*//*将内核和initrd使用的空间在MEMBLOCK中预留*/memblock_reserve(PHYSICAL_START, __pa(klimit) - PHYSICAL_START);/*若relocatable, 则将内存起始32k空间为中断向量预留*/if (PHYSICAL_START > MEMORY_START)memblock_reserve(MEMORY_START, 0x8000);/*为kdump预留64K的空间,即memblock_reserve(0, 0x10000);*/reserve_kdump_trampoline();/*为崩溃内核预留空间,代码很长,但都是在计算起始地址及长度*/reserve_crashkernel();early_reserve_mem();phyp_dump_reserve_mem();limit = memory_limit;if (! limit) {phys_addr_t memsize;/*确保内存大小页对齐,否则mark_bootmem()会出错 */memblock_analyze();memsize = memblock_phys_mem_size();if ((memsize & PAGE_MASK) != memsize)limit = memsize & PAGE_MASK;}/*按照memory limit裁剪memblock的区域大小*/memblock_enforce_memory_limit(limit);memblock_analyze();memblock_dump_all();DBG("Phys. mem: %llx\n", memblock_phys_mem_size());/*若设备树超出内存或处于崩溃的内核区,则执行搬运操作*/move_device_tree();    /*使用于PPC64,在32位中为空函数*/allocate_pacas();/*获得当前系统的CPU个数,并决定当前使用哪一 *个作为系统的BSP(Boot Strap Processor)*/of_scan_flat_dt(early_init_dt_scan_cpus, NULL);}

主要的功能就是检查设备树的chosen节点确定设备的基本信息,然后为设备初始化一个MEMBLOCK,并预留相应空间。再来就是probe_machine,看它的字面意思就可以清楚,它是用来循环查询所有的ppc_md结构体,进而找到适合当前板件类型的结构,定义于Setup-commen.c,看下代码:

void probe_machine(void){extern struct machdep_calls __machine_desc_start;extern struct machdep_calls __machine_desc_end;/* 循环查询ppc_md 结构体的过程*/DBG("Probing machine type ...\n");for (machine_id = &__machine_desc_start;     machine_id < &__machine_desc_end;     machine_id++) {DBG("  %s ...", machine_id->name);memcpy(&ppc_md, machine_id, sizeof(struct machdep_calls));if (ppc_md.probe()) {DBG(" match !\n");break;}DBG("\n");}/*没找到就死循环 */if (machine_id >= &__machine_desc_end) {DBG("No suitable machine found !\n");for (;;);}printk(KERN_INFO "Using %s machine description\n", ppc_md.name);}

两个外部变量__machine_desc_*定义于vmlinux.lds.S中,结构体在mpc83xx平台上的具体实现如下(位于platform/83xx/Mpc831x_rdb.c):

define_machine(mpc831x_rdb) {.name= "MPC831x RDB",.probe= mpc831x_rdb_probe,.setup_arch= mpc831x_rdb_setup_arch,.init_IRQ= mpc831x_rdb_init_IRQ,.get_irq= ipic_get_irq,.restart= mpc83xx_restart,.time_init= mpc83xx_time_init,.calibrate_decr= generic_calibrate_decr,.progress= udbg_progress,};

其实查询的过程mpc831x_rdb_probe很简单,就是与之前early_init_devtree保存在启动参数中的根节点的compatible属性做比对,若匹配则找到。在这个过程中,设备树还未必unflatten。之后,程序运行到setup_kdump_trampoline函数,该函数主要是为kdump创建指令备份,kdump 是一个新的、而且非常可信赖的内核崩溃转储机制。崩溃转储数据可以从一个新启动的内核的上下文中获取,而不是从已经崩溃的内核的上下文。当系统崩溃时,kdump使用kexec启动到第二个内核。第二个内核通常叫做捕获内核(capture kernel),以很小内存启动,并且捕获转储镜像。第一个内核保留了内存的一部分,第二个内核可以用来启动,其实整个就是一个热备。这个kdump的主要好处除了服务器的稳定外,还可以用于普通linux异常复位的问题定位,由于当第一个kernel死掉时保存了coredump,所以切换后可以查看coredump来确认异常复位原因。由此可知,要实现此函数,必需要CONFIG_CRASH_DUMP使能,具体含义未细究。

最后的cpu_has_feature为一个内联函数,实际上就是检查当前CPU是否有某种特性,代码如下:

static inline int cpu_has_feature(unsigned long feature){return (CPU_FTRS_ALWAYS & feature) ||       (CPU_FTRS_POSSIBLE& cur_cpu_spec->cpu_features& feature);}

这里意为检查CPU是否有休眠功能,若有则调用ppc6xx_idle执行保存CPU的基本信息操作。其中的CPU_FTRS_ALWAYS和CPU_FTRS_POSSIBLE实际上就是枚举了CPU所有的特性,然后和之前保存的cpu_spec的features属性一一比对。另外就是ppc6xx_idle函数,之前它的类似函数init_idle_6xx就出现在了early_init阶段的__after_mmu_off函数中,该文件中的三个函数init_idle_6xx、ppc6xx_idle和power_save_ppc32_restore分别用于初始化并保存相关寄存器、使能电源休眠和从休眠中唤醒。

这之后,machine_init就执行完了,跳转执行__save_cpu_setup函数,该函数定义于cpu_setup_6xx.S中,只用于6xx处理器,对于其他类型的处理器,该函数就是一个简单的blr指令,没有意义。该函数用于备份CPU 0状态的上下文内容,在休眠时也会被调用,它不包括Cache以及MMU的配置,主要保存HIDx和MSSCR0等寄存器的值。具体代码如下:

_GLOBAL(__save_cpu_setup)/* Some CR fields are volatile, we back it up all *//*CR共32位,被分为8段,每段4位,  *分别表示LT小于,GT大于,EQ等于和SO溢出*/mfcrr7/* Get storage ptr */lisr5,cpu_state_storage@horir5,r5,cpu_state_storage@l  /*获取数组指针并保存到r5*//* Save HID0 (common to all CONFIG_6xx cpus) */mfsprr3,SPRN_HID0stwr3,CS_HID0(r5)   /*将HID0的值保存到数组*//* Now deal with CPU type dependent registers */mfsprr3,SPRN_PVR  /*PVR:Processor Version Reg*/srwir3,r3,16  /*将r3右移16位,若是603则为0x8086*/

由于后面有一个很长的代码段用来比对具体的CPU型号,这里就不再贴了,判断后的处理和上面的类似,就是先想MSSCR0的值保存,再就是HID1和HID2的。再后面的一个函数为MMU_init,它用于为内核创建基本的内存映射,包括RAM和一些I/O区域,创建页表,以及准备MMU硬件。定义于mm/Init_32.c中,删减版的代码如下:

void __init MMU_init(void){if (ppc_md.progress)  /*实际上就相当于串口输出当前过程*/ppc_md.progress("MMU:enter", 0x111);/*设定初始化MMU所能访问的地址范围,8xx为8M,601为16M,其余的为256M*/if (PVR_VER(mfspr(SPRN_PVR)) == 1)__initial_memory_limit_addr = 0x01000000;if (PVR_VER(mfspr(SPRN_PVR)) == 0x50)__initial_memory_limit_addr = 0x00800000;/*解析引导程序命令行参数,nobats和noltlbs*/MMU_setup();       /*判断当前系统存储器区域个数*/if (memblock.memory.cnt > 1) {#ifndef CONFIG_WIImemblock.memory.cnt = 1;memblock_analyze();  /*只使用第一段物理地址连续的内存空间*/printk(KERN_WARNING "Only using first contiguous memory region");#elsewii_memory_fixups();#endif}   /*将第一段物理地址连续的内存空间保存到total_lowmem中*/total_lowmem = total_memory = memblock_end_of_DRAM() - memstart_addr;lowmem_end_addr = memstart_addr + total_lowmem;#ifdef CONFIG_FSL_BOOKE/*用于Freescale Book-E,83xx中不用*/adjust_total_lowmem();#endif /* CONFIG_FSL_BOOKE */if (total_lowmem > __max_low_memory) {total_lowmem = __max_low_memory;lowmem_end_addr = memstart_addr + total_lowmem;#ifndef CONFIG_HIGHMEMtotal_memory = total_lowmem;memblock_enforce_memory_limit(lowmem_end_addr);memblock_analyze();#endif /* CONFIG_HIGHMEM */}/*初始化MMU 硬件*/MMU_init_hw();  /*ppc_mmu_32.c中,初识化mmu硬件*//*将RAM全部映射到KERNELBASE */mapin_ram();/* Initialize early top-down ioremap allocator */ioremap_bot = IOREMAP_TOP;}

对于memblock的操作,在前面的early_init_devtree中就已经见过了,操作基本上都是大同小异。对于83xx处理器的系统,MMU硬件初始化的函数MMU_init_hw相对复杂,除了常规的flush指令缓存之外,还要初始化Hash表,补全hash_low_32.S中的指令。

至于e300体系下的MMU的硬件机制,其初始化是段很长的过程,我会将它和最后的真正打开MMU放在一起再细细分析一下。

鸟的翅膀在空气里振动,那是一种喧嚣而凛裂的,

基于PowerPC的Linux内核之旅:第2站-__secondary_start(start_he

相关文章:

你感兴趣的文章:

标签云: