Linux内核启动及文件系统加载过程_linux内核没有启动起来uncompressing linux... done, booting -程序员宅基地

技术标签: linux驱动开发  

  

   

u-boot开始执行bootcmd命令,就进入linux内核启动阶段

u-boot 类似,普通 Linux 内核的启动过程也可以分为两个阶段,但针对压缩了的内核如 uImage 就要包括内核自解压过程了。 第一阶段为内核自解压过程,第二阶段主要工作是设置ARM处理器工作模式、使能 MMU 、设置一级页表等,而第三阶段则主要为C代码,包括内核初始化的全部工作,下面是详细介绍。

一、Linux内核自解压过程

内核压缩和解压缩代码都在目录kernel/arch/arm/boot/compressed,编译完成后将产生head.o、misc.o、piggy.gzip.o、vmlinux、decompress.o这几个文件,head.o是内核的头部文件,负责初始设置;misc.o将主要负责内核的解压工作,它在head.o之后;piggy.gzip.o是一个中间文件,其实是一个压缩的内核(kernel/vmlinux),只不过没有和初始化文件及解压文件链接而已;vmlinux是没有(zImage是压缩过的内核)压缩过的内核,就是由piggy.gzip.o、head.o、misc.o组成的,而decompress.o是为支持更多的压缩格式而新引入的。

BootLoader完成系统的引导以后并将Linux内核调入内存之后,调用boot_linux(),这个函数将跳转到kernel的起始位置。如果kernel没有被压缩,就可以启动了。如果kernel被压缩过,则要进行解压,在压缩过的kernel头部有解压程序。压缩过的kernel入口第一个文件源码位置在arch/arm/boot/compressed/head.S。它将调用函数decompress_kernel(),这个函数在文件arch/arm/boot/compressed/misc.c中,decompress_kernel()又调用proc_decomp_setup(),arch_decomp_setup()进行设置,然后打印出信息“Uncompressing Linux...”解压缩linux后,调用gunzip()[或者unlz4或者bunzip2或者unlz]将内核放于指定的位置。

下面简单介绍一下解压缩过程,也就是函数decompress_kernel实现的功能:解压缩代码位于kernel/lib/inflate.c,inflate.c是从gzip源程序中分离出来的,包含了一些对全局数据的直接引用,在使用时需要直接嵌入到代码中。gzip压缩文件时总是在前32K字节的范围内寻找重复的字符串进行编码, 在解压时需要一个至少为32K字节的解压缓冲区,它定义为window[WSIZE]inflate.c使用get_byte()读取输入文件,它被定义成宏来提高效率。输入缓冲区指针必须定义为inptr,inflate.c中对之有减量操作。inflate.c调用flush_window()来输出window缓冲区中的解压出的字节串,每次输出长度用outcnt变量表示。在flush_window()中,还必须对输出字节串计算CRC并且刷新crc变量。在调用gunzip()开始解压之前,调用makecrc()初始化CRC计算表。最后gunzip()返回0表示解压成功。我们在内核启动的开始都会看到这样的输出:

UncompressingLinux...done, booting the kernel.

重要代码:

//解压内核代码

①、在lk层

bootable\bootloader\lk\app\mt_boot\Decompressor.c中:

bool decompress_kernel(unsigned char *in, void *out, int inlen, int outlen)
{
    unsigned long lenp = inlen;
    return gunzip(in, &lenp, out, outlen);
}

在文件bootable\bootloader\lk\app\mt_boot\Mt_boot.c中:

extern bool decompress_kernel(unsigned char *in, void *out, int inlen, int outlen);

//它被mt_boot.c 中的boot_linux函数调用
int boot_linux_fdt(void *kernel, unsigned *tags,
                   char *cmdline, unsigned machtype,
                   void *ramdisk, unsigned ramdisk_size)
{

、、、、、、、、、、、、、、、、、、、、、、、

/* for 64bit decompreesed size.
         * LK start: 0x41E00000, Kernel Start: 0x40080000
         * Max is 0x41E00000 - 0x40080000 = 0x1D80000.
         * using 0x1C00000=28MB for decompressed kernel image size */
        if (decompress_kernel((unsigned char *)zimage_addr, (void *)g_boot_hdr->kernel_addr, (int)zimage_size, (int)0x1C00000)) {
            dprintf(CRITICAL,"decompress kernel image fail!!!\n");
            while (1)
                ;
        }

、、、、、、、、、、、、、、、、、、、、

}


上面的函数被bootable\bootloader\lk\app\mt_boot\Mt_boot.c中的boot_linux函数调用,如下:

/*
初始化DTB(device tree block);

准备各种cmdline参数传入kernel;

关闭I/D-cache、MMU;

打印关键信息,正式拉起kernel.

到这里,bootloader两个阶段就完了!
*/
void boot_linux(void *kernel, unsigned *tags,
                char *cmdline, unsigned machtype,
                void *ramdisk, unsigned ramdisk_size)
{

、、、、、、、、、、、、、、、、、、、、、、、、、、、、


// 新架构都是走fdt分支.  boot_linux_fdt()函数很重要##################################
#ifdef DEVICE_TREE_SUPPORT   //DEVICE_TREE_SUPPORT := yes
    boot_linux_fdt((void *)kernel, (unsigned *)tags,
                   (char *)cmdline, machtype,
                   (void *)ramdisk, ramdisk_size);

    while (1) ;
#endif

、、、、、、、、、、、、、、、、、、、、、、、、、、、、

}

上面的函数被bootable\bootloader\lk\app\mt_boot\Mt_boot.c中的boot_linux_from_storage函数调用,如下:

/*###########################重要########################################*/
/* 这里干的事情就比较多了,跟进g_boot_mode选择各种启动模式,例如:
normal、facotry、fastboot、recovery等,然后从ROM中的boot.img分区找到(解压)
ramdisk跟zImage的地址loader到DRAM的特定地址中,kernel最终load到DRAM中的地址
(DRAM_PHY_ADDR + 0x8000) == 0x00008000.  (这个数据时原始的)
read the data of boot (size = 0x811800)
*/

//boot_linux_from_storage从函数的名称就可以看出来,"从存储其中启动linux内核"
//在此函数中,将cus_param的信息添加到cmdline上
//具体流程请查看我的博客:android 利用cmdline,将参数从preloader传递到kernel
//http://blog.csdn.net/ffmxnjm/article/details/71217309

####################################################################

int boot_linux_from_storage(void)
{
    int ret=0;
#define CMDLINE_TMP_CONCAT_SIZE 100     //only for string concat, 200 bytes is enough

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

/*
   从EMMC的boot分区取出bootimage载入到DRAM  
   在bootable\bootloader\lk\platform\mt6735\load_image.c中有如下打印信息:
    dprintf(CRITICAL, " > from - 0x%016llx (skip boot img hdr)\n",start_addr);
    dprintf(CRITICAL, " > to   - 0x%x (starts with kernel img hdr)\n",addr);
    len = partition_read(part_name, g_boot_hdr->page_size, (uchar*)addr, (size_t)g_bimg_sz); //<<= 系统调用load到DRAM
    
   开机log:
   [4400]  &gt; from - 0x0000000001d80800 (skip boot img hdr)
   [4400]  &gt; to   - 0x45000000 (starts with kernel img hdr)
 */

#ifdef MTK_GPT_SCHEME_SUPPORT
            ret = mboot_android_load_bootimg("boot", kimg_load_addr);  //加载bootimage
#else

            ret = mboot_android_load_bootimg(PART_BOOTIMG, kimg_load_addr);
#endif

            if (ret < 0) {
                msg_img_error("Android Boot Image");
            }
#ifdef LK_PROFILING
            dprintf(CRITICAL,"[PROFILE] ------- load boot.img takes %d ms -------- \n", (int)get_timer(time_load_bootimg));
#endif
            break;

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

  /* 准备启动linux kernel 跳转到 boot_linux,正式拉起kernel;*/******************************************
    if (g_boot_hdr != NULL) {
        boot_linux((void *)g_boot_hdr->kernel_addr, (unsigned *)g_boot_hdr->tags_addr,
                   (char *)cmdline_get(), board_machtype(), (void *)g_boot_hdr->ramdisk_addr, g_rimg_sz);
    } else {
        boot_linux((void *)CFG_BOOTIMG_LOAD_ADDR, (unsigned *)CFG_BOOTARGS_ADDR,
                   (char *)cmdline_get(), board_machtype(), (void *)CFG_RAMDISK_LOAD_ADDR, g_rimg_sz);
    }

    while (1) ;

    return 0;
}


②、在kernel层:

在kernel-3.18\arch\arm\boot\compressed中:

/*
 * The C runtime environment should now be setup sufficiently.
 * Set up some pointers, and start decompressing.
 *   r4  = kernel execution address
 *   r7  = architecture ID
 *   r8  = atags pointer
 */
        mov    r0, r4
        mov    r1, sp            @ malloc space above stack
        add    r2, sp, #0x10000    @ 64k max
        mov    r3, r7
        bl    decompress_kernel
        bl    cache_clean_flush
        bl    cache_off
        mov    r1, r7            @ restore architecture number
        mov    r2, r8            @ restore atags pointer

在文件kernel-3.18\arch\arm\boot\compressed\Misc.c中:

decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,unsigned long free_mem_ptr_end_p,int arch_id)
{
    int ret;

    __stack_chk_guard_setup();

    output_data        = (unsigned char *)output_start;
    free_mem_ptr        = free_mem_ptr_p;
    free_mem_end_ptr    = free_mem_ptr_end_p;
    __machine_arch_type    = arch_id;

    arch_decomp_setup();

    putstr("Uncompressing Linux...");  //解压缩linux
    ret = do_decompress(input_data, input_data_end - input_data,
                output_data, error);
    if (ret)
        error("decompressor returned an error");
    else
        putstr(" done, booting the kernel.\n");
}

二、Linux内核启动第一阶段stage1

承接上文,这里所以说的第一阶段stage1就是内核解压完成并出现Uncompressing Linux...done,booting the kernel.之后的阶段。该部分代码实现在arch/arm/kernel【或者kernel-3.18\arch\arm64\kernel】的 head.S中,该文件中的汇编代码通过查找处理器内核类型和机器码类型调用相应的初始化函数,再建立页表,最后跳转到start_kernel()函数开始内核的初始化工作。检测处理器类型是在汇编子函数__lookup_processor_type中完成的。

以arm32位为例:通过以下代码可实现对它的调用:bl__lookup_processor_type(在文件./arch/arm/kernel/head-commom.S实现)。__lookup_processor_type调用结束返回原程序时,会将返回结果保存到寄存器中。其中r5寄存器返回一个用来描述处理器的结构体地址,并对r5进行判断,如果r5的值为0则说明不支持这种处理器,将进入__error_pr8保存了页表的标志位,r9 保存了处理器的ID 号,r10保存了与处理器相关的struct proc_info_list结构地址。Head.S核心代码如下:


  1. ENTRY(stext)  
  2. setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @设置SVC模式关中断  
  3.       mrc p15, 0, r9, c0, c0        @ 获得处理器ID,存入r9寄存器  
  4.       bl    __lookup_processor_type        @ 返回值r5=procinfo r9=cpuid  
  5.       movs      r10, r5                         
  6.  THUMB( it eq )        @ force fixup-able long branch encoding  
  7.       beq __error_p                   @如果返回值r5=0,则不支持当前处理器'  
  8.       bl    __lookup_machine_type         @ 调用函数,返回值r5=machinfo  
  9.       movs      r8, r5            @ 如果返回值r5=0,则不支持当前机器(开发板)  
  10. THUMB( it   eq )             @ force fixup-able long branch encoding  
  11.       beq __error_a                   @ 机器码不匹配,转__error_a并打印错误信息  
  12.       bl    __vet_atags  
  13. #ifdef CONFIG_SMP_ON_UP    @ 如果是多核处理器进行相应设置  
  14.       bl    __fixup_smp  
  15. #endif  
  16.       bl    __create_page_tables  @最后开始创建页表 
对应arm64位如下(其原理差不多):

ENTRY(stext)
    mov    x21, x0                // x21=FDT
    bl    el2_setup            // Drop to EL1, w20=cpu_boot_mode
    bl    __calc_phys_offset        // x24=PHYS_OFFSET, x28=PHYS_OFFSET-PAGE_OFFSET
    bl    set_cpu_boot_mode_flag
    mrs    x22, midr_el1            // x22=cpuid
    mov    x0, x22
    bl    lookup_processor_type
    mov    x23, x0                // x23=current cpu_table
    /*
     * __error_p may end up out of range for cbz if text areas are
     * aligned up to section sizes.
     */
    cbnz    x23, 1f                // invalid processor (x23=0)?
    b    __error_p
1:
    bl    __vet_fdt
    bl    __create_page_tables        // x25=TTBR0, x26=TTBR1

检测机器码类型是在汇编子函数 __lookup_machine_type (同样在文件 head-common.S实现) 中完成的。与 __lookup_processor_type类似,通过代码:“ bl __lookup_machine_type”来实现对它的调用。该函数返回时,会将返回结构保存放在 r5、r6 和 r7三个寄存器中。其中 r5寄存器返回一个用来描述机器(也就是开发板)的结构体地址,并对 r5进行判断,如果 r5的值为 0则说明不支持这种机器(开发板),将进入 __error_a,打印出内核不支持 u-boot传入的机器码的错误如图2。 r6保存了 I/O基地址, r7 保存了  I/O的页表偏移地址。  当检测处理器类型和机器码类型结束后,将调用__create_page_tables子函数来建立页表,它所要做的工作就是将 RAM 基地址开始的1M 空间的物理地址映射到 0xC0000000开始的虚拟地址处。对本项目的开发板 DM3730而言, RAM挂接到物理地址 0x80000000处,当调用 __create_page_tables 结束后  0x80000000 ~ 0x80100000物理地址将映射到  0xC0000000~0xC0100000虚拟地址处。当所有的初始化结束之后, 使用如下代码来跳到C 程序的入口函数start_kernel()处,开始之后的内核初始化工作: bSYMBOL_NAME(start_kernel) 。 

可以通过:http://blog.csdn.net/shiyongyue/article/details/73785082  

ARM LINUX内核如何确定自己的实际物理地址来理解一下地址的东西。


三、Linux内核启动第二阶段stage2

内核的初始化过程由start_kernel函数开始,至第一个用户进程init结束,调用了一系列的初始化函数对所有的内核组件进行初始化。其中,start_kernel、rest_init、kernel_init、init_post等4个函数构成了整个初始化过程的主线。

 从start_kernel函数开始

Linux内核启动的第二阶段从start_kernel函数开始。start_kernel是所有Linux平台进入系统内核初始化后的入口函数,它主要完成剩余的与硬件平台相关的初始化工作,在进行一系列与内核相关的初始化后,调用第一个用户进程- init 进程并等待用户进程的执行,这样整个 Linux内核便启动完毕。该函数位于init/main.c文件中,主要工作流程如图3所示:

                                                                                 图3 start_kernel流程图

kernel3.18\init\main.c:

好一点的文章解释路径:

https://wenku.baidu.com/view/c933f4026c175f0e7cd137d4.html  start_kernel函数分析

asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;  //用来存放bootloader存放过来的参数
    char *after_dashes;

    /*
     * Need to run as early as possible, to initialize the需要运行,尽可能早地初始化
     * lockdep hash:
     */

/* //死锁检测模块,于2006年引入内核-------来初始化hash表,这个hash表就是一个全局的锁链表,就是一个前后指向的指针结构体数组,lock dependency哈希表。个人理解是锁的初始化,不再深入研究 */

lockdep_init()函数的主要作用是初始化锁的状态跟踪模块。由于内核大量使用锁来进行多进程多处理器的同步操作,死锁就会在代码不合理的时候出现,但是要定位哪个锁比较困难,用哈希表可以跟踪锁的使用状态。死锁情况:一个进程递归加锁同一把锁;同一把锁在两次中断中加锁;几把锁形成闭环死锁】

    lockdep_init();
    set_task_stack_end_magic(&init_task);
    /*

      * smp_setup_processor_id当只有一个CPU的时候这个函数就什么都不做,

      * 但是如果有多个CPU的时候那么它就返回在启动的时候的那个CPU的号   

针对SMP处理器,用于获取当前CPU的硬件ID,如果不是多核,函数为空

【判断是否定义了CONFIG_SMP,如果定义了调用read_cpuid_mpidr读取寄存器CPUID_MPIDR的值,就是当前正在执行初始化的

CPU ID,为了在初始化时做个区分,初始化完成后,所有处理器都是平等的,没有主从】

      */  
    smp_setup_processor_id();

/*

//初始化哈希桶(hash buckets)并将static object和Pool object放入poll

列表,这样堆栈就可以完全操作了 

【这个函数的主要作用就是对调试对象进行早期的初始化,就是HASH锁和静态对象池进行初始化,执行完后,object tracker已经开始完全运作了】


*/

    debug_objects_early_init();

    /*
     * Set up the the initial canary ASAP:
     */

//初始化堆栈保护的加纳利值,防止栈溢出攻击的堆栈保护关键字

    boot_init_stack_canary();
/*

//cgroup_init_early()在系统启动时初始化cgroups,同时初始化需要early_init的子系统 

【这个函数作用是控制组(control groups)早期的初始化,控制组就是定义一组进程具有相同资源的占有程度,比如,可以指定一组进程使用CPU为30%,磁盘IO为40%,网络带宽为50%。目的就是为了把所有进程分配不同的资源。

*/
    cgroup_init_early();

    local_irq_disable();  /* 关闭当前CPU的所有中断相应,操作CPSR寄存器,对应后面的 */  
    early_boot_irqs_disabled = true; //系统中断关闭标志,当early_init完毕后,会恢复中断设置标志为false


/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them  中断仍然是禁用的。做必要的设置去使能他们
 */
/*

boot_cpu_init();设置当前引导系统的CPU在物理上存在,在逻辑上可以使用,并且初

始化准备好,即激活当前CPU 

【在多CPU的系统里,内核需要管理多个CPU,那么就需要知道系统有多少个CPU

,在内核里使用

cpu_present_map位图表达有多少个CPU,每一位表示一个CPU的存在。如果是单个CPU,就是第0位设置为1。虽然系统里有多个CPU存在,但是每个CPU不一定可以使用,或者没有初始化,在内核使用cpu_online_map位图来表示那些CPU可以运行内核代码和接受中断处理。随着移动系统的节能需求,需要对CPU进行节能处理,比如有多个CPU运行时可以提高性能,但花费太多电能,导致电池不耐用,需要减少运行的CPU个数,或者只需要一个CPU运行。这样内核又引入了一个cpu_possible_map位图,表示最多可以使用多少个CPU。在本函数里就是依次设置这三个位图的标志,让引导的

CPU物理上存在,已经初始化好,最少需要运行的CPU。】

*/
    boot_cpu_init();

/*

page_address_init();初始化高端内存的映射表 【在这里引入了高端内存的概念,那么什么叫做高端内存呢?为什么要使用高端内存呢?其实高端内存是相对于低端内存而存在的,那么先要理解一下低端内存了。在32位的系统里,最多能访问的总内存是4G,其

中3G空间给应用程序,而内核只占用1G的空间。因此,内核能映射的内存空间,只有1G大小,但实际上比这个还要小一些,大概是896M,另外128M空间是用来映射高端内存使用的。因此0到896M的内存空间,就叫做低端内存,而高于896M

的内存,就叫高端内存了。如果系统是64位系统,当然就没未必要有高端内存存在了,因为64位有足够多的地址空间给内核使用,访问的内存可以达到10G

都没有问题。在32位系统里,内核为了访问超过1G的物理内存空间,需要使用高端内存映射表。比如当内核需要读取1G的缓存数据时,就需要分配高端内存来使用,这样才可以管理起来。使用高端内存之后,32位的系统也可以访问达到64G内存。在移动操作系统里,目前还没有这个必要,最多才1G多内存】 


*/

    page_address_init();   /* 初始化页地址,使用链表将其链接起来 */  
    pr_notice("%s", linux_banner);   /* 显示内核的版本信息 */  
     /*
       * 每种体系结构都有自己的setup_arch()函数,是体系结构相关的,具体编译哪个
       * 体系结构的setup_arch()函数,由源码树顶层目录下的Makefile中的ARCH变量决定

       //★很重要的一个函数★ arch/arm/kernel/setup.c

       【内核架构相关初始化函数,是非常重要的一个初始化步骤。其中包含了处理器相关参数的初始化、内核启动参数(tagged list)的获取和前期处理、内存子系统的早期初始化(bootmem分配器)】

主要完成了4个方面的工作,一个就是取得MACHINE和PROCESSOR的信息然或将他们赋值给kernel相应的全局变量,然后呢是对boot_command_line和tags接行解析,再然后呢就是memory、cach的初始化,最后是为kernel的后续运行请求资源。

    */ 
    setup_arch(&command_line);

//每一个任务都有一个mm_struct结构来管理内存空间,init_mm是内核的mm_struct

    mm_init_cpumask(&init_mm);
    setup_command_line(command_line);//对comline进行备份和保存
    setup_nr_cpu_ids(); //设置最多有多少个nr_cpu_ids结构

    //setup_per_cpu_areas()为系统中每个cpu的per_cpu变量申请空间,同时草被初始化段里的数据
    setup_per_cpu_areas(); /* 每个CPU分配pre-cpu结构内存, 并复制.data.percpu段的数据 */  
    smp_prepare_boot_cpu();    /* arch-specific boot-cpu hooks */

    build_all_zonelists(NULL, NULL); /*建立内存区域链表*/
    page_alloc_init();/*内存页初始化*/ 

    pr_notice("Kernel command line: %s\n", boot_command_line);
    parse_early_param();/*解析参数*/
    after_dashes = parse_args("Booting kernel",

                  static_command_line, __start___param,
                  __stop___param - __start___param,
                  -1, -1, &unknown_bootoption); //执行命令行解析,若参数不存在,则调用、、//unknown_bootoption 
    if (!IS_ERR_OR_NULL(after_dashes))
        parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
               set_init_arg);

    jump_label_init();


    /*
     * These use large bootmem allocations and must precede
     * kmem_cache_init()
     */
    setup_log_buf(0);
    pidhash_init();   /* 初始化hash表,便于从进程的PID获得对应的进程描述符指针 */
    vfs_caches_init_early(); /* 虚拟文件系统的初始化 ,下面还有一个vfs_caches_init */  
    sort_main_extable();
    /*

    trap_init它的执行可以放到稍微后面一点,没关系。
     trap_init函数完成对系统保留中断向量(异常、非屏蔽中断以及系统调用)的初始化,

     init_IRQ函数则完成其余中断向量的初始化
    */
    trap_init();  /*空函数*/
    mm_init();

    /*
     * Set up the scheduler prior starting any interrupts (such as the
     * timer interrupt). Full topology setup happens at smp_init()
     * time - but meanwhile we still have a functioning scheduler.
     */
     /*
     设置调度程序开始之前的任何中断(如定时器中断)。完整的拓扑结构设置发生在smp_init()时间,
     但与此同时我们还有一个功能调度器
    */
    sched_init(); /* 进程调度器初始化 */ 
    /*
     * Disable preemption - early bootup scheduling is extremely
     * fragile until we cpu_idle() for the first time.
     */
     /*禁用抢占——早期启动调度是非常脆弱的,直到我们cpu_idle之后()的第一次出现。*/
    preempt_disable();   /* 禁止系统调用,即禁止内核抢占 */  

    /*
      /* 检查中断是否已经打开,如果已经打开,则关闭中断 */  
    */

    if (WARN(!irqs_disabled(),
         "Interrupts were enabled *very* early, fixing it\n"))
        local_irq_disable();
    idr_init_cache();
    /*
    RCU机制是Linux2.6之后提供的一种数据一致性访问的机制,从RCU(read-copy-update)的名称上看,
    我们就能对他的实现机制有一个大概的了解,在修改数据的时候,首先需要读取数据,然后生成一个副本,
    对副本进行修改,修改完成之后再将老数据update成新的数据,此所谓RCU。
    在操作系统中,数据一致性访问是一个非常重要的部分,通常我们可以采用锁机制实现数据的一致性访问。
      RCU(Read-Copy Update)是数据同步的一种方式,在当前的Linux内核中发挥着重要的作用。RCU主要针对的
    数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的使用RCU机制读取数据的时候不对链表进行
    耗时的加锁操作。这样在同一时间可以有多个线程同时读取该链表,并且允许一个线程对链表进行修改
    (修改的时候,需要加锁)。RCU适用于需要频繁的读取数据,而相应修改数据并不多的情景,例如在文件系统中,
    经常需要查找定位目录,而对目录的修改相对来说并不多,这就是RCU发挥作用的最佳场景。
    */
    rcu_init();     /* 初始化RCU(Read-Copy Update)机制,为了提高读取数据的速率 ,即初始化互斥机制*/  
    context_tracking_init();

      radix_tree_init();
    /* init some links before init_ISA_irqs() */
    early_irq_init();/*中断向量的初始化*/ 
    init_IRQ();   /*完成其余中断向量的初始化*/ 

/*

tick_init();

//初始化内核时钟系统,tick control,调用clockevents_register_notifier

,就是监听时钟变化事件 

【这个函数主要作用是初始化时钟事件管理器的回调函数,比如当时钟 设备添加时处理。在内核里定义了时钟事件管理器,主要用来管理所有需要周期性地执行任务的设备】

*/

    tick_init(); /*初始化时钟*/ 
    rcu_init_nohz();
    init_timers(); /* 初始化定时器相关的数据结构 */  
    hrtimers_init(); /* 对高精度时钟进行初始化 */  
    softirq_init();  /* 初始化tasklet_softirq和hi_softirq */  
    timekeeping_init();
    time_init();  /* 初始化系统时钟源 */ 
    sched_clock_postinit();
    perf_event_init();
    profile_init();/* 对内核的profile(一个内核性能调式工具)功能进行初始化 */  
    call_function_init();
    WARN(!irqs_disabled(), "Interrupts were enabled early\n");
    early_boot_irqs_disabled = false;
    local_irq_enable();

    kmem_cache_init_late();  /* slab初始化 */ 

    /*
     * HACK ALERT! This is early. We're enabling the console before
     * we've done PCI setups etc, and console_init() must be aware of
     * this. But we do want output early, in case something goes wrong.
     */
     //*攻击警报!这是早期的。我们启用控制台之前我们做PCI设置等,和console_init()必须意识到这一点。但我们确实希望早点输出,以防出现问题。
      /*

         * 初始化控制台以显示printk的内容,在此之前调用的printk

         * 只是把数据存到缓冲区里

         */  
    console_init();;/*打印中断的初始化*/ 
    if (panic_later)
        panic("Too many boot %s vars at `%s'", panic_later,

              panic_param);

    lockdep_info(); /* 如果定义了CONFIG_LOCKDEP宏,则打印锁依赖信息,否则什么也不做 */  

    /*
     * Need to run this when irqs are enabled, because it wants
     * to self-test [hard/soft]-irqs on/off lock inversion bugs
     * too:
     */
    locking_selftest();

#ifdef CONFIG_BLK_DEV_INITRD
    if (initrd_start && !initrd_below_start_ok &&
        page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
        pr_crit("initrd overwritten (0x%08lx < 0x%08lx) - disabling it.\n",
            page_to_pfn(virt_to_page((void *)initrd_start)),
            min_low_pfn);
        initrd_start = 0;
    }
#endif

    page_cgroup_init();
    page_ext_init();
    debug_objects_mem_init();
    kmemleak_init();
    setup_per_cpu_pageset();/*空函数*/ 
    numa_policy_init();/*空函数*/ 
    if (late_time_init)
        late_time_init();
    sched_clock_init();
    /*

       * 一个非常有趣的CPU性能测试函数,可以计算出CPU在1s内执行了多少次一个

       * 极短的循环,计算出来的值经过处理后得到BogoMIPS值(Bogo是Bogus的意思),

    */  
    calibrate_delay(); /*校验延时函数的精确度*/ 
    pidmap_init();/*进程号位图初始化,一般用一个page来只是所有的进程PID占用情况*/ 
    anon_vma_init();/*空函数*/
    acpi_early_init();
#ifdef CONFIG_X86
    if (efi_enabled(EFI_RUNTIME_SERVICES))
        efi_enter_virtual_mode();

#endif
#ifdef CONFIG_X86_ESPFIX64
    /* Should be run before the first non-init thread is created */
    init_espfix_bsp();
#endif
    thread_info_cache_init();/*空函数*/ 
    cred_init();
    fork_init(totalram_pages);  /* 根据物理内存大小计算允许创建进程的数量 */  
    proc_caches_init();/*空函数*/ 
    buffer_init();

    key_init();/*没有键盘为空,有键盘初始化一个高速缓存*/ 
    security_init();/*空函数*/ 
    dbg_late_init();
    vfs_caches_init(totalram_pages);
    signals_init();/*初始化信号量*/ 
    /* rootfs populating might need page-writeback */
    page_writeback_init();/*CPU在内存中开辟高速缓存,CPU直接访问高速缓存提以高速度。当cpu更新了高速缓存的数据后,需要定期将高速缓存的数据写回到存储介质中,比如磁盘和flash等。这个函数初始化写回的周期*/ 
    proc_root_init();/*如果配置了proc文件系统,则需初始化并加载proc文件系统。在根目录的proc文件夹就是proc文件系统,这个文件系统是ram类型的,记录系统的临时数据,系统关机后不会写回到flash中*/ 
    cgroup_init();/*空函数*/
    cpuset_init();/*空函数*/
    taskstats_init_early();/*进程状态初始化,实际上就是分配了一个存储线程状态的高速缓存*/ 
    delayacct_init();/*空函数*/ 


     /*
       * 测试该CPU的各种缺陷,记录检测到的缺陷,以便于内核的其他部分以后可以
       * 使用它们的工作。
     */  
    check_bugs();/*测试CPU的缺陷,记录检测的缺陷,便于内核其他部分工作��需要*/ 

    acpi_subsystem_init();
    sfi_init_late();

    if (efi_enabled(EFI_RUNTIME_SERVICES)) {
        efi_late_init();
        efi_free_boot_services();
    }
    ftrace_init();
    /* Do the rest non-__init'ed, we're now alive */
    //satrt kernel最后执行的初始化,创建初始化进程
    rest_init();
}

该函数所做的具体工作有 :

1) 调用setup_arch()函数进行与体系结构相关的第一个初始化工作;对不同的体系结构来说该函数有不同的定义。对于ARM平台而言,该函数定义在 arch/arm/kernel/setup.c。它首先通过检测出来的处理器类型进行处理器内核的初始化,然后 通过bootmem_init()函数根据系统定义的meminfo结构进行内存结构的初始化,最后调用 paging_init()开启MMU,创建内核页表,映射所有的物理内存和IO空间。 

2) 创建异常向量表和初始化中断处理函数; 

3) 初始化系统核心进程调度器和时钟中断处理机制; 

4) 初始化串口控制台(console_init); 

ARM-Linux 在初始化过程中一般都会初始化一个串口做为内核的控制台,而串口Uart驱动却把串口设备名写死了,如本例中linux2.6.37串口设备名为ttyO0,而不是常用的ttyS0。有了控制台内核在启动过程中就可以通过串口输出信息以便开发者或用户了解系统的启动进程。 

5) 创建和初始化系统cache,为各种内存调用机制提供缓存,包括;动态内存分配,虚拟文件系统(VirtualFile System)及页缓存。 

6) 初始化内存管理,检测内存大小及被内核占用的内存情况; 

7) 初始化系统的进程间通信机制(IPC); 当以上所有的初始化工作结束后,start_kernel()函数会调用rest_init()函数来进行最后的初始化,包括创建系统的第一个进程-init进程来结束内核的启动。

挂载根文件系统并启动init

Linux内核启动的下一过程是启动第一个进程init,但必须以根文件系统为载体,所以在启动init之前,还要挂载根文件系统。

补充:

在文件./arch/arm/kernel/head-commom.S中有相关跳转:

/*
 * The following fragment of code is executed with the MMU on in MMU mode,
 * and uses absolute addresses; this is not position independent.下面的代码片段是MMU在MMU模式下执行的,使用绝对地址,这与位置无关
 *
 *  r0  = cp#15 control register
 *  r1  = machine ID
 *  r2  = atags/dtb pointer
 *  r9  = processor ID
 */
    __INIT

__mmap_switched:
    adr    r3, __mmap_switched_data

    ldmia    r3!, {r4, r5, r6, r7}
    cmp    r4, r5                @ Copy data segment if needed 如果需要,复制数据段
1:    cmpne    r5, r6
    ldrne    fp, [r4], #4
    strne    fp, [r5], #4
    bne    1b

    mov    fp, #0                @ Clear BSS (and zero fp)
1:    cmp    r6, r7
    strcc    fp, [r6],#4
    bcc    1b

 ARM(    ldmia    r3, {r4, r5, r6, r7, sp})
 THUMB(    ldmia    r3, {r4, r5, r6, r7}    )
 THUMB(    ldr    sp, [r3, #16]        )
    str    r9, [r4]            @ Save processor ID   保存处理器ID
    str    r1, [r5]            @ Save machine type  保存机器类型
    str    r2, [r6]            @ Save atags pointer  保存atags指针
    cmp    r7, #0
    strne    r0, [r7]            @ Save control register values  保存控制寄存器的值
    b    start_kernel         //在这里###################
ENDPROC(__mmap_switched)

    .align    2
    .type    __mmap_switched_data, %object


在文件kernel\arch\arm64\kernel\head.S中:

/*
 * The following fragment of code is executed with the MMU on in MMU mode, and
 * uses absolute addresses; this is not position independent.下面的代码片段是MMU在MMU模式下执行的,使用绝对地址,这与位置无关
 */
__mmap_switched:
    adr    x3, __switch_data + 8

    ldp    x6, x7, [x3], #16
1:    cmp    x6, x7
    b.hs    2f
    str    xzr, [x6], #8            // Clear BSS
    b    1b
2:
    ldp    x4, x5, [x3], #16
    ldr    x6, [x3], #8
    ldr    x16, [x3]
    mov    sp, x16
    str    x22, [x4]            // Save processor ID
    str    x21, [x5]            // Save FDT pointer  保存FDT指针
    str    x24, [x6]            // Save PHYS_OFFSET
    mov    x29, #0
    b    start_kernel       //在这里###################
ENDPROC(__mmap_switched)


内核的初始化过程由start_kernel函数开始,至第一个用户进程init结束,调用了一系列的初始化函数对所有的内核组件进行初始化。其中,start_kernel、rest_init、kernel_init、init_post等4个函数构成了整个初始化过程的主线.

我们来看看rest_init、kernel_init、init_post这些函数:

reset_init函数

在start_kernel函数的最后调用了reset_init函数进行后续的初始化。

438 static void noinline __init_refok rest_init(void) 

439     __releases(kernel_lock) 

440 { 

441     int pid; 

442  

        /* reset_init()函数最主要的历史使命就是启动内核线程kernel_init */ 

443     kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND); 

444     numa_default_policy(); 

        /* 启动内核线程kthreadd,运行kthread_create_list全局链表中的kthread */ 

445     pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); 

446     kthreadd_task = find_task_by_pid(pid); 

447     unlock_kernel(); 

448  

449     /* 

450      * The boot idle thread must execute schedule() 

451      * at least once to get things moving: 

452      */ 

        /*  

         * 增加idle进程的need_resched标志,并且调用schedule释放CPU,  

         * 将其赋给更应该获取CPU的进程。 

         */ 

453     init_idle_bootup_task(current); 

454     preempt_enable_no_resched(); 

455     schedule(); 

456     preempt_disable(); 

457  

458     /* Call into cpu_idle with preempt disabled */ 

        /* 

         * 进入idle循环以消耗空闲的CPU时间片,该函数从不返回。然而,当有实际工作 

         * 要处理时,该函数就会被抢占。 

         */ 

459     cpu_idle(); 

460 }

kernel_init函数

kernel_init函数将完成设备驱动程序的初始化,并调用init_post函数启动用户空间的init进程。

813 static int __init kernel_init(void * unused) 

814 { 

815     lock_kernel(); 

816     /* 

817      * init can run on any cpu. 

818      */ 

        /* 修改进程的CPU亲和力 */ 

819     set_cpus_allowed(current, CPU_MASK_ALL); 

820     /* 

821      * Tell the world that we're going to be the grim 

822      * reaper of innocent orphaned children. 

823     

824      * We don't want people to have to make incorrect 

825      * assumptions about where in the task array this 

826      * can be found. 

827      */ 

        /* 把当前进程设为接受其他孤儿进程的进程 */ 

828     init_pid_ns.child_reaper = current; 

829 

830     __set_special_pids(1, 1); 

831     cad_pid = task_pid(current); 

832 

833     smp_prepare_cpus(max_cpus); 

834 

835     do_pre_smp_initcalls(); 

836 

        /* 激活SMP系统中其他CPU */ 

837     smp_init(); 

838     sched_init_smp(); 

839 

840     cpuset_init_smp(); 

841 

        /* 

         * 此时与体系结构相关的部分已经初始化完成,现在开始调用do_basic_setup函数 

         * 初始化设备,完成外设及其驱动程序(直接编译进内核的模块)的加载和初始化 

         */ 

842     do_basic_setup();                        //##############################################################太重要了

843 

844     /* 

845      * check if there is an early userspace init.  If yes, let it do all 

846      * the work 

847      */ 

848 

849     if (!ramdisk_execute_command) 

850         ramdisk_execute_command = "/init"; 

851 

852     if (sys_access((const char __user *)ramdisk_execute_command, 0) != 0) { 

853         ramdisk_execute_command = NULL; 

854         prepare_namespace(); 

855    

856 

857     /* 

858      * Ok, we have completed the initial bootup, and 

859      * we're essentially up and running. Get rid of the 

860      * initmem segments and start the user-mode stuff. 

861      */ 

862     init_post(); 

863     return 0; 

864 } 

init_post函数

到init_post函数为止,内核的初始化已经进入尾声,第一个用户空间进程init将姗姗来迟。

774 static int noinline init_post(void) 

775 { 

第776行,到此,内核初始化已经接近尾声了,所有的初始化函数都已经被调用,因此free_initmem函数可以舍弃内存的__init_begin至__init_end(包括.init.setup、.initcall.init等节)之间的数据。

所有使用__init标记过的函数和使用__initdata标记过的数据,在free_initmem函数执行后,都不能使用,它们曾经获得的内存现在可以重新用于其他目的。

776     free_initmem(); 

777     unlock_kernel();   //整个内核初始化已经结束了

778     mark_rodata_ro(); 

779     system_state = SYSTEM_RUNNING; 

780     numa_default_policy(); 

781  

第782行,如果可能,打开控制台设备,这样init进程就拥有一个控制台,并可以从中读取输入信息,也可以向其中写入信息。

实际上init进程除了打印错误信息以外,并不使用控制台,但是如果调用的是shell或者其他需要交互的进程,而不是init,那么就需要一个可以交互的输入源。如果成功执行open,/dev/console即成为init的标准输入源(文件描述符0)。

782     if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0) 

783         printk(KERN_WARNING "Warning: unable to open an initial console.\n"); 

784  

第785~786行,调用dup打开/dev/console文件描述符两次。这样,该控制台设备就也可以供标准输出和标准错误使用(文件描述符1和2)。假设第782行的open成功执行(正常情况),init进程现在就拥有3个文件描述符--标准输入、标准输出以及标准错误。

785     (void) sys_dup(0); 

786     (void) sys_dup(0); 

787  

第788~804行,如果内核命令行中给出了到init进程的直接路径(或者别的可替代的程序),这里就试图执行init。

因为当kernel_execve函数成功执行目标程序时并不返回,只有失败时,才能执行相关的表达式。接下来的几行会在几个地方查找init,按照可能性由高到低的顺序依次是: /sbin/init,这是init标准的位置;/etc/init和/bin/init,两个可能的位置。

788     if (ramdisk_execute_command) { 

789         run_init_process(ramdisk_execute_command); 

790         printk(KERN_WARNING "Failed to execute %s\n", 

791                 ramdisk_execute_command); 

792    

793  

794     /* 

795      * We try each of these until one succeeds. 

796     

797      * The Bourne shell can be used instead of init if we are 

798      * trying to recover a really broken machine. 

799      */ 

800     if (execute_command) { 

801         run_init_process(execute_command); 

802         printk(KERN_WARNING "Failed to execute %s.  Attempting " 

803                     "defaults...\n", execute_command); 

804    

第805~807行,这些是init可能出现的所有地方。如果在这3个地方都没有发现init,也就无法找到它的同名者了,系统可能就此崩溃。因此,第808行会试图建立一个交互的shell(/bin/sh)来代替,希望root用户可以修复这种错误并重新启动机器。

805     run_init_process("/sbin/init"); 

806     run_init_process("/etc/init"); 

807     run_init_process("/bin/init"); 

808     run_init_process("/bin/sh"); 

809  

第810行,由于某些原因,init甚至不能创建shell。当前面的所有情况都失败时,调用panic。这样内核就会试图同步磁盘,确保其状态一致。如果超过了内核选项中定义的时间,它也可能会重新启动机器

810     panic("No init found.  Try passing init= option to kernel."); 

811 } 


总结:

学完本文章我们一定要清楚内核是从什么地方开始执行,从什么地方结束的,途中主要做的几件大事都有哪些,关键性函数有哪些?

1、内核代码解压缩

内核压缩和解压缩代码都在目录kernel/arch/arm/boot/compressed如果kernel没有被压缩,就可以启动了。如果kernel被压缩过,则要进行解压,在压缩过的kernel头部有解压程序。压缩过的kernel入口第一个文件源码位置在arch/arm/boot/compressed/head.S。   它将调用函数decompress_kernel()这个函数在文件arch/arm/boot/compressed/misc.c】,decompress_kernel()又调用proc_decomp_setup(),arch_decomp_setup()进行设置,然后打印出信息“Uncompressing Linux...”后,调用gunzip()将内核放于指定的位置。

2、查找处理器内核类型和机器码类型调用相应的初始化函数,再建立页表

内核解压完成后,arch/arm/kernelhead.S中,通过查找处理器内核类型和机器码类型调用相应的初始化函数,再建立页表,最后跳转到start_kernel()函数开始内核的初始化工作。  主要在文件kernel\arch\arm64\kernel\head.S中

3、start_kernel函数开始,主要执行main.c函数

start_kernel函数是进入系统内核初始化后的入口函数,它主要完成剩余的与硬件平台相关的初始化工作,在进行一系列与内核相关的初始化后,调用第一个用户进程- init 进程并等待用户进程的执行,这样整个 Linux内核便启动完毕

在文件kernel\arch\arm64\kernel\head.S中: b    start_kernel  

在文件kernel3.18\init\main.c:asmlinkage __visible void __init start_kernel(void)

函数执行顺序:

head.S中: b    start_kernel

----------》main.c:asmlinkage __visible void __init start_kernel(void)

------》main.c:static noinline void __init_refok rest_init(void)用来启动内核线程启动内核线程kernel_init,其中最主要的调用函数为kernel_thread(kernel_init, NULL, CLONE_FS);

------》main,c:原始代码从这里就调用kernel_init函数【间接再调用init_post函数】,在这里合二为一了,而目前实际MTK代码中直接把kernel_init函数转换为int __ref 函数,即如下所示:

static int __ref    //此函数就是kernel_init函数
{
    int ret;

    kernel_init_freeable();  //它调用了prepare_namespace();
    /* need to finish all async __init code before freeing the memory */
    async_synchronize_full();

    free_initmem();
    mark_rodata_ro();
    system_state = SYSTEM_RUNNING;
    numa_default_policy();

    flush_delayed_fput();

#ifdef CONFIG_MTPROF
    log_boot("Kernel_init_done");

#endif

    if (ramdisk_execute_command) {
        ret = run_init_process(ramdisk_execute_command);
        if (!ret)
            return 0;
        pr_err("Failed to execute %s (error %d)\n",
               ramdisk_execute_command, ret);
    }


    /*
     * We try each of these until one succeeds.
     *
     * The Bourne shell can be used instead of init if we are
     * trying to recover a really broken machine.
     */
    if (execute_command) {
        ret = run_init_process(execute_command);

        if (!ret)
            return 0;
        pr_err("Failed to execute %s (error %d).  Attempting defaults...\n",
            execute_command, ret);
    }
    if (!try_to_run_init_process("/sbin/init") ||
        !try_to_run_init_process("/etc/init") ||
        !try_to_run_init_process("/bin/init") ||

        !try_to_run_init_process("/bin/sh"))
        return 0;

    panic("No working init found.  Try passing init= option to kernel. "
          "See Linux Documentation/init.txt for guidance.");
}

重要的log打印信息:

5651.535781:Kernel_init_done

4、调用第一个用户进程- init 进程

在system\core\init\init.cpp中:

int main(int argc, char** argv) {

、、、、、、、、、、、、、、、、、、、、、、、、、、

open_devnull_stdio();
    klog_init();
    klog_set_level(KLOG_NOTICE_LEVEL);

    NOTICE("init %s started!\n", is_first_stage ? "first stage" : "second stage");

、、、、、、、、、、、、、、、、、、、、、、、、、、

重要log信息:

从log中可以看到:

[20:25:51.182] [    5.671509] <0>.(0)[1:init]init: init first stage started!    init第一阶段开始

四、挂载根文件系统

根文件系统至少包括以下目录:

 /etc/:存储重要的配置文件。

 /bin/:存储常用且开机时必须用到的执行文件。

 /sbin/:存储着开机过程中所需的系统执行文件。

 /lib/:存储/bin//sbin/的执行文件所需的链接库,以及Linux的内核模块。

 /dev/:存储设备文件。

  注:五大目录必须存储在根文件系统上,缺一不可。

以只读的方式挂载根文件系统,之所以采用只读的方式挂载根文件系统是因为:此时Linux内核仍在启动阶段,还不是很稳定,如果采用可读可写的方式挂载根文件系统,万一Linux不小心宕机了,一来可能破坏根文件系统上的数据,再者Linux下次开机时得花上很长的时间来检查并修复根文件系统。

    挂载根文件系统的而目的有两个:一是安装适当的内核模块,以便驱动某些硬件设备或启用某些功能;二是启动存储于文件系统中的init服务,以便让init服务接手后续的启动工作。

执行init服务

Linux内核启动后的最后一个动作,就是从根文件系统上找出并执行init服务。Linux内核会依照下列的顺序寻找init服务:

1)/sbin/是否有init服务

2)/etc/是否有init服务

3)/bin/是否有init服务

4)如果都找不到最后执行/bin/sh

找到init服务后,Linux会让init服务负责后续初始化系统使用环境的工作,init启动后,就代表系统已经顺利地启动了linux内核。启动init服务时,init服务会读取/etc/inittab文件,根据/etc/inittab中的设置数据进行初始化系统环境的工作。/etc/inittab定义init服务在linux启动过程中必须依序执行以下几个Script

 /etc/rc.d/rc.sysinit

 /etc/rc.d/rc

/etc/rc.d/rc.local

/etc/rc.d/rc.sysinit主要的功能是设置系统的基本环境,当init服务执行rc.sysinit时 要依次完成下面一系列工作:

(1)启动udev

(2)设置内核参数

执行sysctl –p,以便从/etc/sysctl.conf设置内核参数

(3)设置系统时间

将硬件时间设置为系统时间

(4)启用交换内存空间

执行swpaon –a –e,以便根据/etc/fstab的设置启用所有的交换内存空间。

(5)检查并挂载所有文件系统

检查所有需要挂载的文件系统,以确保这些文件系统的完整性。检查完毕后以可读可写的方式挂载文件系统。

(6)初始化硬件设备

      Linux除了在启动内核时以静态驱动程序驱动部分的硬件外,在执行rc.sysinit时,也会试着驱动剩余的硬件设备。rc.sysinit驱动的硬件设备包含以下几项:

  a)定义在/etc/modprobe.conf的模块

  b)ISA PnP的硬件设备

  c)USB设备

(7)初始化串行端口设备

 Init服务会管理所有的串行端口设备,比如调制解调器、不断电系统、串行端口控制台等。Init服务则通过rc.sysinit来初始化linux的串行端口设备。当rc.sysinit发现linux才能在这/etc/rc.serial时,才会执行/etc/rc.serial,借以初始化所有的串行端口设备。因此,你可以在/etc/rc.serial中定义如何初始化linux所有的串行端口设备。

(8)清除过期的锁定文件与IPC文件

(9)建立用户接口

在执行完3个主要的RC Script后,init服务的最后一个工作,就是建立linux的用户界面,好让用户可以使用linux。此时init服务会执行以下两项工作:

(10)建立虚拟控制台

 Init会在若干个虚拟控制台中执行/bin/login,以便用户可以从虚拟控制台登陆linuxlinux默认在前6个虚拟控制台,也就是tty1~tty6,执行/bin/login登陆程序。当所有的初始化工作结束后,cpu_idle()函数会被调用来使系统处于闲置(idle)状态并等待用户程序的执行。至此,整个Linux内核启动完毕。整个过程见图4。

      

                          图4:linux内核启动及文件系统加载全过程    










另外总结:

Linux 内核启动挂载android根文件系统过程分析

顺便罗列一下内核启动流程:

/arch/arm/boot/compressed/head.S:

Start:
Decompressed_kernel()             //在/arch/arm/boot/compressed/misc.c 中
Call_kernel()


Stext:
/init/main.c
Start_kernel()
Setup_arch()

Rest_init()
Init()
Do_basic_setup()
Prepare_namespace()

看到了这里,我已激动得说不出话了,因为来到我与挂载根文件系统最重要的接口函数。


static int noinline init_post(void)
{
free_initmem();
unlock_kernel();
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n");
(void) sys_dup(0);
(void) sys_dup(0);
current->signal->flags |= SIGNAL_UNKILLABLE;
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
printk(KERN_WARNING "Failed to execute %s\n",ramdisk_execute_command);
}

if (execute_command) {
run_init_process(execute_command);
printk(KERN_WARNING "Failed to execute %s. Attempting ""defaults...\n",
execute_command);
}
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel.");
}

其中,我们看到行代码run_init_process(execute_command);
execute_command 是从UBOOT 传递过来的参数,一般为/init,也就是调用文件系统里的init 初始化进程。如果找不到init 文件就会在
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
中找,否则报错。

在这里由于我们的根文件系统是从/linuxrc 开始的,所以我硬性把它改为
if (execute_command) {
run_init_process("/linuxrc");
printk(KERN_WARNING "Failed to execute %s. Attempting "
"defaults...\n", execute_command);
}


Android 文件系统初始化核心Init.c文件分析

       上面我们说的init 这个文件是由android 源代码编译来的,编译后在/out/target/product/generic/root/

其源码在/system/core/init/init.c  (/system/core/init/init.cpp

Init.c 主要功能:

(1)安装SIGCHLD 信号。(如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。因此需要对SIGCHLD 信号做出处理,回收僵尸进程的资源,避免造成不必要的资源浪费。)
(2)对umask 进行清零。
        何为umask,请看 http://www.szstudy.cn/showArticle/53978.shtml
(3)为rootfs 建立必要的文件夹,并挂载适当的分区。
/dev (tmpfs)
/dev/pts (devpts)
/dev/socket
/proc (proc)
/sys (sysfs)
(4)创建/dev/null 和/dev/kmsg 节点。
(5)解析/init.rc,将所有服务和操作信息加入链表。
(6)从/proc/cmdline 中提取信息内核启动参数,并保存到全局变量。
(7)先从上一步获得的全局变量中获取信息硬件信息和版本号,如果没有则从/proc/cpuinfo 中提取,并保存到全局变量。
(8)根据硬件信息选择一个/init.(硬件).rc,并解析,将服务和操作信息加入链表。
在G1 的ramdisk 根目录下有两个/init.(硬件).rc:init.goldfish.rc 和init.trout.rc,init 程序会根据上一步获得的硬件信息选择一个解析。
(9)执行链表中带有“early-init”触发的的命令。
(10)遍历/sys 文件夹,是内核产生设备添加事件(为了自动产生设备节点)。
(11)初始化属性系统,并导入初始化属性文件。
(12)从属性系统中得到ro.debuggable,若为1,則初始化keychord 監聽(监听)。
(13)打開console,如果cmdline 中沒有指定console 則打開默認的 /dev/console
(14)读取/initlogo.rle(一張565 rle 壓縮的位圖),如果成功則在
/dev/graphics/fb0 所示Logo,如果失敗則將/dev/tty0 設為TEXT 模式并打開/dev/tty0,輸出文“ANDROID”字樣。
(15)判斷cmdline 中的參數,并设置属性系统中的参数:
1、 如果 bootmode 為
- factory,設置ro.factorytest 值為1
- factory2,設置ro.factorytest 值為2
- 其他的設ro.factorytest 值為0
2、如果有serialno 参数,則設置ro.serialno,否則為""
3、如果有bootmod 参数,則設置ro.bootmod,否則為"unknown"
4、如果有baseband 参数,則設置ro.baseband,否則為"unknown"
5、如果有carrier 参数,則設置ro.carrier,否則為"unknown"
6、如果有bootloader 参数,則設置ro.bootloader,否則為"unknown"
7、通过全局变量(前面从/proc/cpuinfo 中提取的)設置ro.hardware 和
ro.version。
(16)執行所有触发标识为init 的action。
(17)開始property 服務,讀取一些property 文件,這一動作必須在前面
那些ro.foo 設置后做,以便/data/local.prop 不能干預到他們。

- /system/build.prop
- /system/default.prop
- /data/local.prop
- 在读取默認的 property 后读取 presistent propertie,在 /data/property 中
(18)為 sigchld handler 創建信号机制
(19)确认所有初始化工作完成:
device_fd(device init 完成)
property_set_fd(property server start 完成)
signal_recv_fd (信号机制建立)
(20) 執行所有触发标识为early-boot 的action
(21) 執行所有触发标识为boot 的action
(22)基于當前property 狀態,執行所有触发标识为property 的action
(23)注冊轮询事件:
- device_fd
- property_set_fd
-signal_recv_fd
-如果有keychord,則注冊keychord_fd
(24)如果支持BOOTCHART,則初始化BOOTCHART
(25)进入主进程循环:
- 重置輪詢事件的接受狀態,revents 為0
- 查詢action 隊列,并执行。
- 重啟需要重啟的服务
- 輪詢注冊的事件
- 如果signal_recv_fd 的revents 為POLLIN,則得到一個信號,獲取并處

- 如果device_fd 的revents 為POLLIN,調用handle_device_fd
- 如果property_fd 的revents 為POLLIN,調用handle_property_set_fd
- 如果keychord_fd 的revents 為POLLIN,調用handle_keychord
到了这里,整个android 文件系统已经起来了


##################################################################################

初始化核心的核心init.rc文件分析

在上面红色那一行(5)解析/init.rc,将所有服务和操作信息加入链表。

       parse_config_file("/init.rc");//在init.c 中代码 (有关 /init.rc的脚本我就不贴出来了)

名词解释:
       Android 初始化語言由四大类声明组成:行为类(Actions)、命令类(Commands)、服务类(Services)、选项类(Options)。
       初始化语言以行为单位,由以空格间隔的语言符号組成。C 风格的反斜杠转义符可以用来插入空白到语言符号。双引号也可以用来防止文本被空格分成多个语言符号。当反斜杠在行末时,作为换行符。

       * 以#开始(前面允许空格)的行为注释。
       * Actions 和Services 隐含声明一个新的段落。所有该段落下Commands 或 Options 的声明属于该段落。第一段落前的Commands 或Options 被忽略。
       * Actions 和Services 拥有唯一的命名。在他们之后声明相同命名的类将被当作错误并忽略。
          Actions 是一系列命令的命名。Actions 拥有一个触发器(trigger)用来決定action 何時执行。当一个action 在符合触发条件被执行时,如果它还没被加入到待执行队列中的话,則加入到队列最后。队列中的action 依次执行,action 中的命令也依次执行。

           Init 在执行命令的中间处理其他活动(设备创建/销毁,property 设置,进程重启)。

          Actions 的表现形式:
          on <trigger>
          <command>
          <command>
          <command>

          重要的数据结构两个列表,一个队列。
static list_declare(service_list);
static list_declare(action_list);
static list_declare(action_queue);

         *.rc 脚本中所有 service 关键字定义的服务将会添加到 service_list 列表中。
         *.rc 脚本中所有 on 关键开头的项将会被会添加到 action_list 列表中。每个action 列表项都有一个列表,此列表用来保存该段落下的 Commands。

脚本解析过程:
parse_config_file("/init.rc")
int parse_config_file(const char *fn)
{
    char *data;
    data = read_file(fn, 0);
    if (!data)                                                                                                                                                     return -1;
    parse_config(fn, data);
    DUMP();
    return 0;
}
static void parse_config(const char *fn, char *s)                                                                                          {
    ...
   case T_NEWLINE:
   if (nargs) {
       int kw = lookup_keyword(args[0]);
       if (kw_is(kw, SECTION)) {
           state.parse_line(&state, 0, 0);
           parse_new_section(&state, kw, nargs, args);
       } else {
           state.parse_line(&state, nargs, args);
       }
       nargs = 0;
    }
...

parse_config 会逐行对脚本进行解析,如果关键字类型为 SECTION ,那么将会执行parse_new_section();
类型为 SECTION 的关键字有: on 和 sevice

关键字类型定义在 Parser.c (system\core\init) 文件中
Parser.c (system\core\init)
#define SECTION 0x01
#define COMMAND 0x02
#define OPTION 0x04
关键字                 属性
capability,          OPTION, 0, 0)
class,                 OPTION, 0, 0)
class_start,        COMMAND, 1, do_class_start)
class_stop,       COMMAND, 1, do_class_stop)
console,             OPTION, 0, 0)
critical,              OPTION, 0, 0)
disabled,          OPTION, 0, 0)
domainname,    COMMAND, 1, do_domainname)
exec,                 COMMAND, 1, do_exec)
export,              COMMAND, 2, do_export)
group,              OPTION, 0, 0)
hostname,         COMMAND, 1, do_hostname)
ifup,                   COMMAND, 1, do_ifup)
insmod,            COMMAND, 1, do_insmod)
import,               COMMAND, 1, do_import)
keycodes,         OPTION, 0, 0)
mkdir,               COMMAND, 1, do_mkdir)
mount,              COMMAND, 3, do_mount)
on,                   SECTION, 0, 0)
oneshot,           OPTION, 0, 0)
onrestart,         OPTION, 0, 0)
restart,             COMMAND, 1, do_restart)
service,           SECTION, 0, 0)
setenv,             OPTION, 2, 0)
setkey,             COMMAND, 0, do_setkey)
setprop,          COMMAND, 2, do_setprop)
setrlimit,           COMMAND, 3, do_setrlimit)
socket,           OPTION, 0, 0)
start,                COMMAND, 1, do_start)
stop,             COMMAND, 1, do_stop)
trigger,           COMMAND, 1, do_trigger)
symlink,           COMMAND, 1, do_symlink)
sysclktz,         COMMAND, 1, do_sysclktz)
user,             OPTION, 0, 0)
write,             COMMAND, 2, do_write)
chown,             COMMAND, 2, do_chown)
chmod,            COMMAND, 2, do_chmod)
loglevel,          COMMAND, 1, do_loglevel)
device,             COMMAND, 4, do_device)

parse_new_section()中再分别对 service 或者 on 关键字开头的内容进行解
析。
...
case K_service:
        state->context = parse_service(state, nargs, args);
        if (state->context) {
             state->parse_line = parse_line_service;
           return;
        }
        break;
case K_on:
        state->context = parse_action(state, nargs, args);
        if (state->context) {
            state->parse_line = parse_line_action;
           return;
        }
        break;
...

对 on 关键字开头的内容进行解析
static void *parse_action(struct parse_state *state, int nargs, char **args)
{
    ...
    act = calloc(1, sizeof(*act));
    act->name = args[1];
    list_init(&act->commands);
    list_add_tail(&action_list, &act->alist);
    ...
}

对 service 关键字开头的内容进行解析
static void *parse_service(struct parse_state *state, int nargs, char **args)
{
    struct service *svc;
    if (nargs < 3) {
        parse_error(state, "services must have a name and a program\n");
        return 0;
    }
    if (!valid_name(args[1])) {
        parse_error(state, "invalid service name '%s'\n", args[1]);
        return 0;
     }
//如果服务已经存在service_list 列表中将会被忽略
    svc = service_find_by_name(args[1]);
    if (svc) {
        parse_error(state, "ignored duplicate definition of service '%s'\n", args[1]);
        return 0;
    }
    nargs -= 2;
    svc = calloc(1, sizeof(*svc) + sizeof(char*) * nargs);
     if (!svc) {
        parse_error(state, "out of memory\n");
        return 0;
    }
     svc->name = args[1];
    svc->classname = "default";
    memcpy(svc->args, args + 2, sizeof(char*) * nargs);
    svc->args[nargs] = 0;
    svc->nargs = nargs;
    svc->onrestart.name = "onrestart";
    list_init(&svc->onrestart.commands);
//添加该服务到 service_list 列表
    list_add_tail(&service_list, &svc->slist);
    return svc;
}

服务的表现形式:
service <name> <pathname> [ <argument> ]*
<option>
<option>
...

       申请一个service 结构体,然后挂接到service_list 链表上,name 为服务的名称,pathname 为执行的命令,argument为命令的参数。之后的 option 用来控制这个service 结构体的属性,parse_line_service 会对 service 关键字后的内容进行解析并填充到 service 结构中,当遇到下一个service 或者on 关键字的时候此service 选项解析结束。
例如:
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
    socket zygote stream 666
   onrestart write /sys/android_power/request_state wake
服务名称为: zygote
启动该服务执行的命令: /system/bin/app_process
命令的参数: -Xzygote /system/bin --zygote --start-system-server
socket zygote stream 666: 创建一个名为:/dev/socket/zygote 的 socket ,
类型为:stream

当前信息详细来源:http://blog.sina.com.cn/s/blog_68bc1cab0100j7mv.html










版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/ffmxnjm/article/details/75097171

智能推荐

MobaXterm详细使用教程_mobaxterm_personal-程序员宅基地

文章浏览阅读10w+次,点赞292次,收藏2.3k次。这一篇MobaXterm详细使用教程,我们来介绍一下如何设置并用MobaXterm来连接Linux服务器。MobaXterm 又名 MobaXVT,是一款增强型终端、X 服务器和 Unix 命令集(GNU/ Cygwin)工具箱。MobaXterm 可以开启多个终端视窗,以最新的 X 服务器为基础的X.Org,可以轻松地来试用 Unix/Linux 上的 GNU Unix 命令。这样一来,我们可以不用安装虚拟机来试用虚拟环境,然后只要通过 MobaXterm 就可以使用大多数的 linux 命令。._mobaxterm_personal

PTA甲级 1013 Battle Over Cities (25分) BFS,DFS,查并集_pta甲级1013第二个检查点-程序员宅基地

文章浏览阅读151次。强烈推荐,刷PTA的朋友都认识一下柳神–PTA解法大佬本文由参考于柳神博客写成柳神的程序员宅基地,这个可以搜索文章柳神的个人博客,这个没有广告,但是不能搜索还有就是非常非常有用的 算法笔记 全名是算法笔记 上级训练实战指南 //这本都是PTA的题解算法笔记PS 今天也要加油鸭题目原文It is vitally important to have all the cities connected by highways in a war. If a city is occupied _pta甲级1013第二个检查点

一个WP8样式的Android ProgressBar_android wp样式-程序员宅基地

文章浏览阅读1.1k次。一个WP8样式的进度对话框_android wp样式

NOI2018T1 归程_noi2018-t1-程序员宅基地

文章浏览阅读563次。  原题传送门 luoguP4768 NOI2018T1 归程题目大意  本题的故事发生在魔力之都,在这里我们将为你介绍一些必要的设定。 魔力之都可以抽象成一个 nnn 个节点、mmm 条边的无向连通图(节点的编号从 111 至 nnn)。我们依次用 l,al,al,a 描述一条边的长度、海拔。  作为季风气候的代表城市,魔力之都时常有雨水相伴,因此道路积水总是不可避免的。由于整个城市的排水系统连通,因此有积水的边一定是海拔相对最低的一些边。我们用水位线来描述降雨的程度,它的意义是:所有海拔不超过水位_noi2018-t1

C++Primer_课后习题第十二章_#include < iostream.h> #include < string.h> class -程序员宅基地

文章浏览阅读296次。本文答案,部分参考于C++ Primer 习题集前面章节的习题答案第一章第二章第三章第四章第五章第六章第七章第八章第九章第十章第十一章配套的学习资料https://www.jianguoyun.com/p/DTK5uJgQldv8CBjKv80D12.1都是4个StrBlob中的那个vector是共享的.12.2#pragma once#ifndef MY_STRBLOB_H#define MY_STRBLOB_H#include<vector>#i_#include < iostream.h> #include < string.h> class strtype { public: strtype(

创建user01-user20 随机六位数密码 a-z A-Z 0-9_sg85,cc-程序员宅基地

文章浏览阅读752次。一:创建的要求1)创建user01-user022)生成20组随机密码3)设置密码4)输出清单二:代码及测试结果adduser () {pw_txt=`mktemp pw.XXXXX`#1)创建用户for i in `seq -s ' ' -w 1 20` #-s表示横着输出,-w表示保持宽度,即把1 2 3 变成01 02 03这样do useradd user$idone#2)生成随机密码cat /dev/urandom | strings -6 |e_sg85,cc

随便推点

scala中的特质_scala success特质-程序员宅基地

文章浏览阅读2k次。scala中的特质1.特质中定义的方法可以实现,【有了大括号的就是已经实现过的方法,例如下面Animal中的listen和run】;也可以不实现【例如Animal类中的speak方法】示例代码如下:package cookBook.chapter8trait Animal{ //没有实现 def speak def listen: Unit ={ } ..._scala success特质

SQL语句练习题目-程序员宅基地

文章浏览阅读875次。sql语句练习题目员工信息表 staff: user_id , name, store_id, salary商店表store:store_id, name,city题目1:找出平均工资小于5000的商店所在的城市SELECT a.ctity,AVG(b.salary) FROM store a,staff bWHERE a.store_id=b.store_idG...

HTML期末作业:基于html+css+javascript+jquery实现古诗词网页 学生网页设计作品 web前端开发技术 web课程设计 网页规划与设计-程序员宅基地

文章浏览阅读198次。 关于作者: 历任研发工程师,技术组长,教学总监;曾于2016年、2020年两度荣获CSDN年度十大博客之星。 十载寒冰,难凉热血;多年过去,历经变迁,物是人非。 然而,对于技术的探索和追求从未停歇。 坚持原创,热衷分享,初心未改,继往开来!茶文化网站、️‍中华传统文化题材、京剧文化水墨风书画、中国民间年画文化艺术网站 、等网站的设计与制作。️ 这个首页代码运用了DIV盒子的使用方法,如盒子的嵌套、浮动、margin、border、background等属性的使用,外部大盒

图像直方图均衡化和局部增强处理_基于直方图的局部增强-程序员宅基地

文章浏览阅读1.9w次,点赞15次,收藏123次。一、基本原理1.1直方图均衡化(一提高图像的对比度,二使像素值几乎成均匀分布的图像0表示黑,1表示白,中心思想是把原始图像的灰度直方图从比较集中的某个灰度区间变成在全部灰度范围内的均匀分布,增加许多图像的局部对比度,亮度可以更好的在直方图上分布。) 灰度级的直方图就是反映一幅图像中的灰度级与出现这种灰度的概率之间的关系的图形。设变量代表图像中的像素灰度级。在图像中,像素的灰度级可作归一化处理..._基于直方图的局部增强

echarts饼状图圆环位置修改样式调整_echarts环形图位置-程序员宅基地

文章浏览阅读5k次。作为笔记(echarts篇)pie图例组件:legend: { type: 'scroll',//图例数量过多时,可以使用滚动 left: 'center', bottom: this.ringData.legendBottom || 0,//设置位置 data: this.ringData.name, textStyle: { color: '#fff'//..._echarts环形图位置

机器学习-AdaBoost算法_adaboostclassifier()-程序员宅基地

文章浏览阅读600次。简介Adaboost算法是一种提升方法,将多个弱分类器,组合成强分类器。AdaBoost,是英文”Adaptive Boosting“(自适应增强)的缩写,由Yoav Freund和Robert Schapire在1995年提出。它的自适应在于:前一个弱分类器分错的样本的权值(样本对应的权值)会得到加强,权值更新后的样本再次被用来训练下一个新的弱分类器。在每轮训练中,用总体(样本总体)训练新的弱分类器,产生新的样本权值、该弱分类器的话语权,一直迭代直到达到预定的错误率或达到指定的最大迭代次数。总体—_adaboostclassifier()

推荐文章

热门文章

相关标签