Linux内核启动过程

Linux内核启动过程

本文主要是对《嵌入式Linux应用开发完全手册》中内容的整理和总结,在进行这一部分学习之前,有必要对Linux内核源码组织结构进行了解。


Linux内核在启动过程中执行了很多的函数,不可能像学习U-Boot时一样将所有相关的代码查看一遍,主要了解其总体的启动过程和其中的一些函数进行了解。

启动过程概述
启动过程分成两个部分,第一阶段用汇编代码编写,第二阶段是用C语言进行编写的。

第一阶段

  • 检查内核是否支持CPU、开发板型号。

  • 连接内核时使用的是虚拟地址,所以需要设置页表、使能MMU。

  • 调用C函数start_kernel之前的常规工作,包括复制数据段、清除BBS段、调用start_kernel函数。

第二阶段

第二阶段使用C语言编写,它进行内核的初始化的全部工作,最后调用rest_init函数启动init过程,创建系统第一个进程:init进程。在第二阶段,仍有部分架构/开发板相关的代码,比如setup_arch函数用于进行架构/开发板相关的设置(比如重新设置页表、设置系统时钟、初始化串口等)。

Linux内核启动过程
图片摘自《嵌入式Linux应用开发完全手册》


第一阶段代码分析

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

上述的代码摘自连接文件,可以看出段.text.head位于Linux内核镜像中的最前面的部分,也是最先执行的。

.section ".text.head", "ax"
ENTRY(stext)
    setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ 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  __vet_atags
    bl  __create_page_tables

上述代码摘自head.S文件,这一部分是Linux内核镜像的入口点。

    setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 

首先将处理器设置为SVC模式,同时禁止FIR和IRQ。

    mrc p15, 0, r9, c0, c0      
    bl  __lookup_processor_type     
    movs    r10, r5             
    beq __error_p

从协处理寄存器中获得CPU的型号,然后与内核支持的CPU型号进行比较,如果没有相符合的就跳转到错误处理,错误处理会将CPU挂起来,这样只能重启CPU了。

CPU型号信息由结构体proc_info_list保存,该结构体的原型在/arch/arm/include/asm/procinfo.h中定义。

具体支持的型号在/arch/arm/mm/proc-arm920.S中

    .section ".proc.info.init", #alloc, #execinstr

    .type   __arm920_proc_info,#object
__arm920_proc_info:
    .long   0x41009200
    .long   0xff00fff0
    ...

可见该结构体在连接时,将会出现在.proc.info.init段中,相应的在连接文件中有如下代码:

        __proc_info_begin = .;
            *(.proc.info.init)
        __proc_info_end = .;

所以在检测是否支持目标板的CPU类型时,只要将该段中的CPU逐一比较即可。

bl  __lookup_machine_type       
    movs    r8, r5          
    beq __error_a

接下来是比较U-boot传递的开发板ID是否符合编译的内核Linux的开发板ID,如果不一致同样会进入错误代码处理,将CPU挂起,只能重启CPU。

U-boot是通过将机器ID保存在R1寄存器中进行传递的,Linux中的机器ID保存在一个machine_desc结构中,它定义了开发板相关的一些属性及函数,比如机器类型ID、起始I/O物理地址,Bootloader传入的参数的地址、中断初始化函数、I/O映射函数。该结构体和体系架构关系密切,所以定义在体系架构代码部分,比如对于SDMK2440开发板,在/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

它是以宏的形式表示的,宏的定义在arch/arm/include/asm/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             \
};

展开后会有一个MACH_TYPE_S3C2440宏,该宏定义在arch/arm/tools/mach-types中定义,最后会被转换成一个头文件mach-types.h供其他文件包含。
该结构体将会被连接到一个单独的段.arch.info.init中,该段在连接文件中如下:

        __arch_info_begin = .;
            *(.arch.info.init)
        __arch_info_end = .;

第一阶段接下来的部分是用来创建一级页表,使能MMU的,具体的代码不分析,用到的时候再仔细研究。

第二阶段代码分析

在进入start_kernel函数之后,如果在串口上没有看到内核的启动信息,一般而言有两个原因:Bootloader传入的命令行参数不对,或者setup_arch函数针对开发板的设置不正确。

在调用setup_arch函数之前已经调用了printk(linux_banner),但是并不会立即在控制台中显示,而只是放在缓冲区中,当控制台初始化完毕之后才会显示该内容。

setup_arch函数分析

在第二阶段中,setup_arch函数是非常重要的一个函数,它完成和开发板有关的初始化。该函数在arch/arm/kernel/setup.c中定义。

setup_arch函数流程

上面的图片摘自《嵌入式Linux应用开发完全手册》

下面是函数setup_arch:

void __init setup_arch(char **cmdline_p)
{
    ...
    setup_processor();
    mdesc = setup_machine(machine_arch_type);
    ...
    if (__atags_pointer)
        tags = phys_to_virt(__atags_pointer);
    else if (mdesc->boot_params)
        tags = phys_to_virt(mdesc->boot_params);
    ...
    if (tags->hdr.tag == ATAG_CORE) {
        if (meminfo.nr_banks != 0)
            squash_mem_tags(tags);
        save_atags(tags);
        parse_tags(tags);
    }
    ...
    memcpy(boot_command_line, from, COMMAND_LINE_SIZE);
    boot_command_line[COMMAND_LINE_SIZE-1] = '\0';
    parse_cmdline(cmdline_p, from);
    paging_init(mdesc);
    ...
}

首先进行setup_processor();它会调用lookup_procsee_info以获得CPU的proc_info_list结构体,并进行相应地初始化。

mdesc = setup_machine(machine_arch_type);

setup_machine()函数本来应该对开发板进行一些初始化,但是,对于ARM开发板,该函数不进行任何相应的初始化。

    if (__atags_pointer)
        tags = phys_to_virt(__atags_pointer);
    else if (mdesc->boot_params)
        tags = phys_to_virt(mdesc->boot_params);

确定Bootloader传递的tags所在的地址,因为这个时候已经建立了页表,所以要进行物理地址到虚拟地址的转换。tags的地址是事先商量好的一个内存地址,不通过Bootloader传递该地址的值。R2寄存器保存Bootloader传递给内核tags标记列表的基地址。

    if (tags->hdr.tag == ATAG_CORE) {
        if (meminfo.nr_banks != 0)
            squash_mem_tags(tags);
        save_atags(tags);
        parse_tags(tags);
    }

如果在内核中已经定义了meminfo结构体,就忽略tags中传递的meminfo结构体。然后遍历tags中的所有标记,并且做相应的处理。

    memcpy(boot_command_line, from, COMMAND_LINE_SIZE);
    boot_command_line[COMMAND_LINE_SIZE-1] = '\0';
    parse_cmdline(cmdline_p, from);
    paging_init(mdesc);

对命令行进行一些先期的处理,然后重新初始化一次页表映射。paging_init(mdesc);这是一个和开发板相关的函数。

paging_init -> devicemaps_init -> mdesc->map_io()

paging_init函数将会调用devicemaps_init函数,然后devicemaps_init函数将会调用mdesc结构体中map_io函数。

static void __init smdk2440_map_io(void)
{
    s3c24xx_init_io(smdk2440_iodesc, ARRAY_SIZE(smdk2440_iodesc));
    s3c24xx_init_clocks(16934400);
    s3c24xx_init_uarts(smdk2440_uartcfgs, ARRAY_SIZE(smdk2440_uartcfgs));
}

至此,在setup_arch函数中执行的内容结束了。现在继续回到start_kernel:

在start_kernel中会执行函数console_init(void),接下来分析这个函数:

void __init console_init(void)
{
    initcall_t *call;
    tty_ldisc_begin();
    call = __con_initcall_start;
    while (call < __con_initcall_end) {
        (*call)();
        call++;
    }
}

它会调用从__con_initcall_start__con_initcall_end之间的所有的函数,这些函数使用console_initcall宏来指定。
在连接文件中有如下定义:

        __con_initcall_start = .;
            *(.con_initcall.init)
        __con_initcall_end = .;

在Samsung.h中有如下定义:

static int __init s3c_serial_console_init(void)         
{                               
    return s3c24xx_serial_initconsole(__drv, __inf);    
}                               
console_initcall(s3c_serial_console_init)

根据上面的分析可见,在执行console_init函数时,s3c24xx_serial_initconsole(__drv, __inf);将会被执行。

int s3c24xx_serial_initconsole(struct platform_driver *drv,struct s3c24xx_uart_info *info)

{
    ...
    register_console(&s3c24xx_serial_console);
    return 0;
}

这个函数将会执行register_console(&s3c24xx_serial_console);函数,这个函数将会将s3c24xx_serial_console结构体链接到控制台链表中。

static struct console s3c24xx_serial_console = {
    .name       = S3C24XX_SERIAL_NAME,
    .device     = uart_console_device,
    .flags      = CON_PRINTBUFFER,
    .index      = -1,
    .write      = s3c24xx_serial_console_write,
    .setup      = s3c24xx_serial_console_setup
};

这个结构体中保存着控制台设备的信息。

#define S3C24XX_SERIAL_NAME "ttySAC"
    .index      = -1,

这行代码表示序号可以是任意的,也就是说控制台参数可以为ttySAC0/1/2。
所以我们在Bootloader中传递的命令行参数中的console可以等于以上几个值,结果都是一样的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值