歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux內核 >> Linux內核啟動過程概述

Linux內核啟動過程概述

日期:2017/3/1 9:08:38   编辑:Linux內核

  今天給大家帶來的是Linux內核啟動過程概述。希望能夠幫助大家更好的理解Linux內核的啟動,並且創造出自己的內核^_^

  Linux的啟動代碼真的挺大,從匯編到C,從Makefile到LDS文件,需要理解的東西很多。畢竟Linux內核是由很多人,花費了巨大的時間和精力寫出來的。而且直到現在,這個世界上仍然有成千上萬的程序員在不斷完善Linux內核的代碼。今天我們主要講解的是Linux-2.6.22.6這個內核版本。說句實話,博主也不確定自己能夠講好今天這個題目,因為這個題目太大太難。但是博主有信心,將自己學會的內容清楚地告訴大家,希望大家也能夠有所收獲。

1.啟動文件head.S和head-common.S 

  首先,我們必須明確“我們為什麼要啟動Linux內核”。沒錯,當然是因為我們想要使用Linux系統,要明確我們的最終目的是使用Linux上的應用程序。這些應用程序可以是純軟件的,也可以是硬件相關的。博主是做嵌入式開發的,那麼我想要的當然就是用Linux內核來更好的控制我的硬件。無論是做機器人、無人機或者其他智能硬件這都是必然趨勢。首先我們來看內核的啟動文件head.S。

    .section ".text.head", "ax"
    .type    stext, %function
ENTRY(stext)
    msr    cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
                        @ and irqs disabled
    mrc    p15, 0, r9, c0, c0        @ get processor id
    bl    __lookup_processor_type        @ r5=procinfo r9=cpuid
    movs    r10, r5                @ invalid processor (r5=0)?
    beq    __error_p            @ yes, error 'p'
    bl    __lookup_machine_type        @ r5=machinfo
    movs    r8, r5                @ invalid machine (r5=0)?
    beq    __error_a            @ yes, error 'a'
    bl    __create_page_tables

    ldr    r13, __switch_data        @ address to jump to after
                        @ mmu has been enabled
    adr    lr, __enable_mmu        @ return (PIC) address
    add    pc, r10, #PROCINFO_INITFUNC

  首先看這段匯編代碼,它主要是用來做一些內核啟動前的檢測:__lookup_processor_type 檢測內核是否支持當前CPU、__lookup_machine_type檢測是否支持當前單板,並且__create_page_tables創建頁表,__enable_mmu使能MMU。如果在一系列的自檢過程後發現不支持,則跳到__error_p或__error_a。這裡我們首先打開__lookup_machine_type。

    .type    __lookup_machine_type, %function
__lookup_machine_type:
    adr    r3, 3b
    ldmia    r3, {r4, r5, r6}
    sub    r3, r3, r4            @ get offset between virt&phys
    add    r5, r5, r3            @ convert virt addresses to
    add    r6, r6, r3            @ physical address space
1:    ldr    r3, [r5, #MACHINFO_TYPE]    @ get machine type
    teq    r3, r1                @ matches loader number?
    beq    2f                @ found
    add    r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc
    cmp    r5, r6
    blo    1b
    mov    r5, #0                @ unknown machine
2:    mov    pc, lr

3:    .long    .
    .long    __arch_info_begin
    .long    __arch_info_end

  我們在arch\arm\kernel找到__lookup_machine_type被定義在head-common.S文件中。開始分析代碼:首先,讀出3b的地址給r3,這裡的3b就是下面的那個3:所對應的虛擬地址。然後用ldmia指令將r3存放的虛擬地址分別存入r4,r5,r6。所以現在

r4=. ; r5=__arch_info_begin ; r6=__arch_info_end

然後用r3-r4求出偏移地址,再利用這個偏移地址求出r5和r6的實際物理地址。其中__arch_info_begin和__arch_info_end定義在內核目錄arch\arm\kernel下vmlinux.lds文件中,經過起始虛擬地址= (0xc0000000) + 0x00008000逐層疊加得到。

SECTIONS
{



 . = (0xc0000000) + 0x00008000;

 .text.head : {
  _stext = .;
  _sinittext = .;
  *(.text.head)
 }

 .init : { /* Init code and data        */
   *(.init.text)
  _einittext = .;
  __proc_info_begin = .;
   *(.proc.info.init)
  __proc_info_end = .;
  __arch_info_begin = .;
   *(.arch.info.init)
  __arch_info_end = .;

  這裡的__arch_info_begin和__arch_info_end中間存放的是段屬性為.arch.info.init的結構體。這裡我們可以直接在linux下查詢內核中包含.arch.info.init的文件。

Direction:include/asm-arm/arch.h
#define MACHINE_START(_type,_name) \ static const struct machine_desc __mach_desc_##_type \ __used \ __attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_##_type, \ .name = _name, #define MACHINE_END \ };
Direction:arch/arm/mach-s3c2440
MACHINE_START(S3C2440, "SMDK2440") /* Maintainer: Ben Dooks <[email protected]> */ .phys_io = S3C2410_PA_UART, .io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc, .boot_params = S3C2410_SDRAM_PA + 0x100, .init_irq = s3c24xx_init_irq, .map_io = smdk2440_map_io, .init_machine = smdk2440_machine_init, .timer = &s3c24xx_timer, MACHINE_END

  如圖所示,在include/asm-arm/arch.h中找到了定義的結構體類型machine_desc,並且在代碼中它的段屬性被強制定義成了.arch.info.init。這樣做的目的是在剛剛我們看到的vmlinux.lds鏈接腳本文件中,可以將具有.arch.info.init段屬性的結構體統一放在__arch_info_begin和__arch_info_end之間。非常便於處理。那麼現在我們將這個結構體展開,看看它的內容。也就是將arch/arm/mach-s3c2440中的參數傳入。展開後如下:

#define MACHINE_START(_type,_name)            \
static const struct machine_desc __mach_desc_S3C2440    \
 __used                            \
 __attribute__((__section__(".arch.info.init"))) = {    \
    .nr        = MACH_TYPE_S3C2440,        \
    .name        = "SMDK2440",
/* Maintainer: Ben Dooks <[email protected]> */
    .phys_io    = S3C2410_PA_UART,
    .io_pg_offst    = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
    .boot_params    = S3C2410_SDRAM_PA + 0x100,                    //0x30000100

    .init_irq    = s3c24xx_init_irq,
    .map_io        = smdk2440_map_io,
    .init_machine    = smdk2440_machine_init,
    .timer        = &s3c24xx_timer,
};

  現在我們看到,定義的結構體類型machine_desc,內容為.nr到.timer。我們可以看出這個結構體大概是存儲硬件信息。nr存放機器ID,name存放單板名稱,phys_io存放輸入輸出口,io_pg_offst存放IO的偏移地址,boot_params存放uboot傳給內核的啟動參數(TAG),init_irq存放的是中斷初始化信息,map_io為IO的映射表,init_machine存放的是單板的初始化信息,timer存放的是單板的定時器信息。

struct machine_desc {
    /*
     * Note! The first four elements are used
     * by assembler code in head-armv.S
     */
    unsigned int        nr;        /* architecture number    */
    unsigned int        phys_io;    /* start of physical io    */
    unsigned int        io_pg_offst;    /* byte offset for io 
                         * page tabe entry    */

    const char        *name;        /* architecture name    */
    unsigned long        boot_params;    /* tagged list        */

    unsigned int        video_start;    /* start of video RAM    */
    unsigned int        video_end;    /* end of video RAM    */

    unsigned int        reserve_lp0 :1;    /* never has lp0    */
    unsigned int        reserve_lp1 :1;    /* never has lp1    */
    unsigned int        reserve_lp2 :1;    /* never has lp2    */
    unsigned int        soft_reboot :1;    /* soft reboot        */
    void            (*fixup)(struct machine_desc *,
                     struct tag *, char **,
                     struct meminfo *);
    void            (*map_io)(void);/* IO mapping function    */
    void            (*init_irq)(void);
    struct sys_timer    *timer;        /* system tick timer    */
    void            (*init_machine)(void);
};

  我們打開arch.h文件,看到對machine_desc結構體的定義確實和我們剛剛所說的一樣。再回到head-common.S文件,這裡對mmap_switch定義:

    .type    __mmap_switched, %function
__mmap_switched:
    adr    r3, __switch_data + 4

    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

    ldmia    r3, {r4, r5, r6, sp}
    str    r9, [r4]            @ Save processor ID
    str    r1, [r5]            @ Save machine type
    bic    r4, r0, #CR_A            @ Clear 'A' bit
    stmia    r6, {r0, r4}            @ Save control register values
    b    start_kernel

  mmap_switch做了很多工作,這裡我們看到有復制數據段,清BSS段,保存CPU的ID,保存機器ID,清‘A’位,保存控制寄存器的值,然後就到了C語言段——start_kernel函數。

2.C語言段——start_kernel

asmlinkage void __init start_kernel(void)
{ local_irq_disable(); early_boot_irqs_off(); early_init_irq_lock_class(); /* * Interrupts are still disabled. Do necessary setups, then * enable them */ lock_kernel(); tick_init(); boot_cpu_init(); page_address_init(); printk(KERN_NOTICE); printk(linux_banner); setup_arch(&command_line); setup_command_line(command_line); printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line); parse_early_param(); parse_args("Booting kernel", static_command_line, __start___param, __stop___param - __start___param, &unknown_bootoption); init_IRQ(); profile_init(); if (!irqs_disabled()) printk("start_kernel(): bug: interrupts were enabled early\n"); early_boot_irqs_on(); local_irq_enable(); console_init(); rest_init(); }

  接下來進入start_kernel啟動內核的C函數。上面是start_kernel的部分代碼。這部分代碼的主要作用是處理uboot傳遞來的參數,設置與體系結構相關的環境,初始化控制台,最後執行應用程序,實現功能。這裡我把start_kernel函數的幾個主要功能的子函數逐層寫出,幫助大家理解start_kernel的功能結構。

start_kernel
    setup_arch(&command_line);
    setup_command_line(command_line);
    unknown_bootoption
        obsolete_checksetup    
    parse_early_param
        do_early_param        
    rest_init;
        kernel_init
            prepare_namespace
                mount_root
            init_post

  這裡每一個退格(TAB)都代表此函數被上一個函數調用(例如obsolete_checksetup是unknown_bootoption調用的函數)。setup_arch(&command_line)和setup_command_line(command_line)就是用來處理uboot傳遞進來的啟動參數的(處理TAG)。obsolete_checksetup從__setup_start到 __setup_end,調用用非early標識的函數;do_early_param從__setup_start到 __setup_end,調用用early標識的函數(但因為__setup_param(str, fn, fn, 0)中early賦值為0,所以不在這裡調用),所以我們主要用obsolete_checksetup。這在後面我們會提到。mount_root是掛載根文件系統,因為Linux上的應用程序最終要在根文件系統上運行。最後是init_post中運行應用程序。那麼現在就有一個問題,Linux內核是如何接收uboot傳來的根文件系統信息的呢?

bootcmd=nand read.jffs2 0x30007FC0 kernel; bootm 0x30007FC0
bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0

  上面是uboot啟動時打印的環境變量。其中我們能夠看到根文件系統掛載到第4個分區:root=/dev/mtdblock3 (從0分區開始)。上面我們提到過,setup_arch(&command_line)和setup_command_line(command_line)就是用來處理uboot傳遞進來的啟動參數的(處理TAG)。但這個處理只是簡單的復制粘貼而已,這兩個函數將TAG保存,但並未進行真正的處理。那麼真正告訴內核在哪裡掛載的函數是什麼呢?我們通過查看prepare_namespace可以看到一個saved_root_name。查找saved_root_name,發現在Do_mounts.c文件中有對它的調用:

static int __init root_dev_setup(char *line)
{
    strlcpy(saved_root_name, line, sizeof(saved_root_name));
    return 1;
}

__setup("root=", root_dev_setup);   //傳入一個字符串,一個函數

  根據我們之前的經驗,我們可以猜測這個__setup宏,也是定義了一個結構體。通過查找__setup我們找到了它的宏定義:

Dir:init.h
#define __setup(str, fn)                    \
    __setup_param(str, fn, fn, 0)


#define __setup_param(str, unique_id, fn, early)            \
    static char __setup_str_##unique_id[] __initdata = str;    \
    static struct obs_kernel_param __setup_##unique_id    \
        __attribute_used__                \
        __attribute__((__section__(".init.setup")))    \
        __attribute__((aligned((sizeof(long)))))    \
        = { __setup_str_##unique_id, fn, early }

  在init.h文件裡,定義__setup等於__setup_param。那麼在__setup_param的宏定義裡,我們可以知道:它先定義了一個字符串,然後定義了一個結構體類型obs_kernel_param __setup。這個結構體的段屬性為.init.setup,內容為一個字符串,一個函數,還有early。具備這個屬性的結構體被鏈接腳本文件放到一起,從__setup_start到 __setup_end搜索調用。在vmlinux.lds中
__setup_start = .;
*(.init.setup)
__setup_end = .;

  但是在Flash裡沒有分區,只能和uboot一樣,將分區在代碼裡寫死。一般在啟動Linux的時候,Linux會自動打印出分區的信息。這裡我的分區是這樣的:

Creating 4 MTD partitions on "NAND 256MiB 3,3V 8-bit":
0x00000000-0x00040000 : "bootloader"
0x00040000-0x00060000 : "params"
0x00060000-0x00260000 : "kernel"
0x00260000-0x10000000 : "root"

  我們搜索這個分區名 grep "\"bootloader\"" * -nR。在arch/arm/plat-s3c24xx中找到分區代碼:

static struct mtd_partition smdk_default_nand_part[] = {
    [0] = {
        .name   = "bootloader",
        .size   = 0x00040000,
        .offset    = 0,
    },
    [1] = {
        .name   = "params",
        .offset = MTDPART_OFS_APPEND,
        .size   = 0x00020000,
    },
    [2] = {
        .name   = "kernel",
        .offset = MTDPART_OFS_APPEND,
        .size   = 0x00200000,
    },
    [3] = {
        .name   = "root",
        .offset = MTDPART_OFS_APPEND,
        .size   = MTDPART_SIZ_FULL,
    }
};

  就是這樣,在處理完uboot傳遞的參數,進行CPU和單板的校驗,掛載根文件系統等一系列操作後,最終內核執行init_post()中的應用程序。內核啟動流程講解完畢^_^

題外話:最近博主在自學Linux kernel和Linux device driver,感覺有難度。但是還是很有意義的,因為能夠看到前輩的代碼,心裡真的很高興。我就希望自己也能夠修改Linux源代碼,寫出適合自己硬件的Linux系統。不僅如此,我還希望能夠將自己的代碼開源,分享給更多的人。完善Linux內核,讓它變得更快更方便是博主的最終目標。博主會繼續學習,然後把知識更好的分享給大家!

Copyright © Linux教程網 All Rights Reserved