03-kernel启动流程之内核启动

 

kernel启动流程之内核启动

内核最终目的: 挂接根文件系统, 运行应用程序(在根文件系统)

因为kernel是由uboot来引导的, 而uboot将控制权交给kernel前在约定地址设置了TAG参数,
kernel启动首先使要去处理uboot传来的参数

从arch/arm/kernel/head.S开始分析:
根据链接脚本vmlinux.lds可以知道, 内核vmlinux首先放所有文件的.tex.head段,
这些段排放的先后顺序是根据的vmlinux依赖文件的顺序来决定的, 首先放arch/arm/kernel/head.o文件的.text.head段

所以kernel代码一开始是运行arch/arm/kernel/head.o文件.tex.head段的代码

    .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                    @读寄存器获取CPUID
    bl    __lookup_processor_type        @ r5=procinfo r9=cpuid  @判断这个版本的kernel是否支持这个处理器
    movs    r10, r5                @ invalid processor (r5=0)?
    beq    __error_p            @ yes, error 'p'                                  @如果kernel不支持这个处理器, 跳到__error_p
    
    @判断这个版本的kernel是否支持uboot传进来的机器ID bi_arch_number
    @对于S3C2440来说 MACH_TYPE_S3C2440 = 362
    bl    __lookup_machine_type    @ r5=machinfo   
    movs    r8, r5                @ invalid machine (r5=0)?
    beq    __error_a                @ yes, error 'a'
    
    @ 创建页表
    @ 因为vmlinux.lds对应的地址是虚拟地址,并不对应真实的物理地址,所以要创建页表
    bl    __create_page_tables     
    

下面分析__lookup_processor_type函数:

uboot启动内核时是调用theKernel函数:
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);    
==> 对于2440来说
theKernel (0, 362, bd->bi_boot_params);      // bd->bi_boot_params是uboot设置的一些kernel启动参数
传进来的3个参数会放在r0,r1,r2三个寄存器中
r0 = 0, r1 = 362, r2 = bd->bi_boot_params

3:    .long    .
    .long    __arch_info_begin
    .long    __arch_info_end

@这个函数时判断这个版本的kernel是否支持uboot传进来的机器ID
__lookup_machine_type:
    adr    r3, 3b  @r3的地址等于上面3:标签的地址, 实际存在的地址
    
    @ r4 = "." 是虚拟地址, r5 = __arch_info_begin, r6 = __arch_info_end
    @ __arch_info_begin和__arch_info_end是在链接脚本vmlinux.lds中定义的
    ldmia    r3, {r4, r5, r6}
    
    @因为r3是物理地址, r4是虚拟地址, 这里是计算物理地址和虚拟地址的偏差
    @r5 = __arch_info_begin加上偏差得到__arch_info_begin的物理地址
    @r5 = __arch_info_end加上偏差得到__arch_info_end的物理地址
    sub    r3, r3, r4            @ get offset between virt&phys
    add    r5, r5, r3            @ convert virt addresses to
    add    r6, r6, r3            @ physical address space
    
    @循环查找machine type是否和uboot传进来的匹配
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


下面是对链接脚本.arch.info.init段的说明:
__arch_info_begin = .;
   *(.arch.info.init)  // 所有文件的.arch.info.init段, 表示所有架构相关的初始化信息放到一起
  __arch_info_end = .;

整个内核查找: grep ".arch.info.init" * -nR
发现在include/asm-xxx/mach/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                \
};

搜索MACHINE_START宏看在哪里使用了, 发现很多架构相关的.c文件中有使用到
比如: arch/arm/mach-s3c2440/mach-smdk2440.c中有使用

MACHINE_START(S3C2440, "SMDK2440")
    /* Maintainer: Ben Dooks <ben@fluff.org> */
    .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


将其展开:
static const struct machine_desc __mach_desc_S3C2440    \
 __used                            \
 __attribute__((__section__(".arch.info.init"))) = {    \
    .nr            = MACH_TYPE_S3C2440,        \
    .name        = "SMDK2440",
    /* Maintainer: Ben Dooks <ben@fluff.org> */
    .phys_io    = S3C2410_PA_UART,    // 0x50000000 就是UART0控制寄存器的地址
    .io_pg_offst    = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
    .boot_params    = S3C2410_SDRAM_PA + 0x100,  // 0x30000000 + 0x100 = 就是存放内核启动参数的地址

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

可以发现MACHINE_START宏是定义了一个struct machine_desc类型的结构体,
这个结构体的特殊地方就在于它的属性被强制定义为了.arch.info.init,
当链接脚本链接时就会把所有架构已经定义了的struct machine_desc结构体组合在一起

------------------------------------------------------

下面继续分析head.S
    @ 当MMU使能之后会跳到__switch_data去执行
    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
    
下面分析跳转__switch_data:
    .type    __switch_data, %object
__switch_data:
    .long    __mmap_switched
    .long    __data_loc                @ r4
    .long    __data_start            @ r5
    .long    __bss_start                @ r6
    .long    _end                    @ r7
    .long    processor_id            @ r4
    .long    __machine_arch_type        @ r5
    .long    cr_alignment            @ r6
    .long    init_thread_union + THREAD_START_SP @ sp

/*
 * The following fragment of code is executed with the MMU on in MMU mode,
 * and uses absolute addresses; this is not position independent.
 *
 *  r0  = cp#15 control register
 *  r1  = machine ID
 *  r9  = processor ID
 */
    .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)  清BSS段
1:    cmp    r6, r7
    strcc    fp, [r6],#4
    bcc    1b

    ldmia    r3, {r4, r5, r6, sp}
    str    r9, [r4]            @ Save processor ID 保存处理器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        @ 最终会调用start_kernel启动内核, 内核第一个C函数

综上: 内核的启动过程
1. __lookup_processor_type 确定这个版本的kernel是否支持这个处理器
2. __lookup_machine_type 确定这个版本的kernel是否支持uboot传进来的机器ID
3. 创建页表, 因为vmlinux.lds对应的地址是虚拟地址,并不对应真实的物理地址,所以要创建页表    
4. __enable_mmu 使能MMU
5. 复制数据段
6. 清BSS段
7.保存处理器ID, uboot传进来的机器ID
8. 调用start_kernel


----------------------------------------------
下面分析init/main.c文件里的start_kernel:
asmlinkage void __init start_kernel(void)
{
    ....
    printk(linux_banner); // 输出内核版本信息
    setup_arch(&command_line);                  // 解析uboot传进入的kernel启动参数
    setup_command_line(command_line);    // 解析uboot传进入的kernel启动参数
    ...
    parse_early_param();
        -->do_early_param 早期的参数处理
            --> 从__setup_start到__setup_end调用setup_func函数, setup_func就是用来保存命令行参数的
    unknown_bootoption
        -->obsolete_checksetup
            --> 从__setup_start到__setup_end调用setup_func函数, setup_func就是用来保存命令行参数的
    ...
    rest_init();
    --> kernel_init()
        // 上面的setup_arch和setup_command_line这两个函数只不过是把uboot传进来的参数记录下来而已
        --> prepare_namespace()// 确定/根文件系统在哪里挂接, 确定挂接哪个根文件系统
            --> mount_root(); // 挂接根文件系统
        --> init_post(); // 打开/dev/console, 执行应用程序/sbin/init等等            
}

下面分析setup_arch: 对uboot传进来的参数做解析
void __init setup_arch(char **cmdline_p)
{
    ...
    struct machine_desc *mdesc;
    char *from = default_command_line; // 默认命令行参数

    mdesc = setup_machine(machine_arch_type);  // 拿到当前架构定义的struct machine_desc类型的结构体,
    
    if (mdesc->boot_params)
        tags = phys_to_virt(mdesc->boot_params); // 拿到uboot设置的kernel启动参数tag

    if (tags->hdr.tag == ATAG_CORE) {
        ...
        parse_tags(tags);  // 解析TAG
    }
    
    parse_cmdline(cmdline_p, from); // 解析命令行
}

----------------------------------------------------------------------------
在mount_root挂接根文件系统之前肯定要先知道在哪里挂接根文件系统,是由prepare_namespace函数决定的
下面分析prepare_namespace函数:
void __init prepare_namespace(void)
{
    ...
    if (saved_root_name[0]) {
        root_device_name = saved_root_name;
        ...
        ROOT_DEV = name_to_dev_t(root_device_name); // ROOT设备在这里被设置
        ...
    }
}

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

__setup("root=", root_dev_setup);  // 这是一个宏

#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 }
展开:
    static char __setup_str_root_dev_setup[] __initdata = "root=";    \
    static struct obs_kernel_param __setup_root_dev_setup    \
        __attribute_used__                \
        __attribute__((__section__(".init.setup")))    \
        __attribute__((aligned((sizeof(long)))))    \
        = { __setup_str_root_dev_setup, root_dev_setup, 0 }
        
可以看到这个宏展开后
定义了一个__initdata字符串和结构体struct obs_kernel_param结构体(属性是.init.setup)
.init.setup属性在vmlinux.lds中用到, 会把所有.init.setup属性的结构放到一个段
  __setup_start = .;
   *(.init.setup)
  __setup_end = .;


搜索__setup_start是被谁调用就知道命令行是怎么被使用的
发现do_early_param和obsolete_checksetup两个函数调用了__setup_start

do_early_param函数进行早期参数的初始化,
从__setup_start到__setup_end调用setup_func函数, setup_func就是用来保存命令行参数的


综上:
bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0
mount_root挂接根文件系统是由命令行参数root=/dev/mtdblock3决定的,
uboot传进来的命令行参数是由__setup定义的结构体去保存和定义处理函数,被链接脚本放到__setup_start到__setup_end之间,
由do_early_param和obsolete_checksetup函数从__setup_start到__setup_end一一调用处理函数做处理的,

----------------------------------------------------------------------------
分析start_kernel函数中可以看出最终调用init_post函数
在调用init_post函数之前kernel的启动初始化(包括挂接根文件系统等)已经基本完成,
init_post函数启动用户模式,开始运行应用程序

下面分析init_post函数:

static int noinline init_post(void)
{
    ...
    // 打开/dev/console设备文件, 这个文件称为终端, 对应UART0
    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);    // 重定向, 复制一个打开的文件号, 指向/dev/console
    (void) sys_dup(0);    // 重定向, 复制一个打开的文件号, 指向/dev/console
    ...
    run_init_process("/sbin/init"); // 执行/sbin/init应用程序
    ...
}

kernel目的是挂接根文件系统, 运行根文件系统上的应用程序, 根文件系统哪里来, 可以通过busybox构建

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值