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










發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章