linux内核启动流程及busybox分析

版权声明:欢迎转载,转载请注明出处 http://blog.csdn.net/itdo_just https://blog.csdn.net/itdo_just/article/details/78832751

内核版本:linux-3.4


启动流程:
1、比较机器ID
2、解析u-boot传入的启动参数
3、挂接根文件系统、执行第一个应用程序


1、比较机器ID
打开 vmlinux.lds.S 里面可以找到内核把初始化参数的段定义为了”.arch.info.init”

.init.arch.info : {
    __arch_info_begin = .;
    *(.arch.info.init)
    __arch_info_end = .;
}

在内核源码的目录下可以输入指令查找一下谁调用了这个段:grep “.arch.info.init” * -nR
搜索到如下内容:

#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             \
    };

定义了一个宏来指定,可以接着在 sourceinsight 工程里面去搜索 MACHINE_START 和 MACHINE_END 这两个宏,看下具体谁使用了它,搜索结果如下 :

MACHINE_START(SUNXI, "sun8i")

    .atag_offset    = 0x100,
    .init_machine   = sunxi_dev_init,
    .init_early     = sunxi_init_early,
    .map_io     = sunxi_map_io,
#ifndef CONFIG_OF
    .init_irq   = sun8i_gic_init,
#endif
    .handle_irq = gic_handle_irq,
    .restart    = sun8i_restart,
    .timer      = &sunxi_timer,
    .dt_compat  = NULL,
    .reserve    = sun8i_reserve,
    .fixup      = sun8i_fixup,
    .nr_irqs    = NR_IRQS,
#ifdef CONFIG_SMP
    .smp        = smp_ops(sunxi_smp_ops),
#ifdef CONFIG_ARCH_SUN8IW6
    .smp_init   = smp_init_ops(sun8i_smp_init_ops),
#endif
#endif

MACHINE_END

MACHINE_START 和 MACHINE_END 这两个就是定义一个结构体,这个结构体被强制的设置为一个属性,把它的段设置为 “.arch.info.init”,那么如果哪个文件下面有这些结构体的话,这些结构体就会被 vmlinux.lds.S 这个链接脚本组合在一起,我们可以把这段定义带入到刚才搜索到的那段 MACHINE_START 和 MACHINE_END 的宏里面,代入后他们就被置换成了如下内容:

static const struct machine_desc __mach_desc_SUNXI  \     
     __used                         \
     __attribute__((__section__(".arch.info.init"))) = {    \
        .nr     = MACH_TYPE_SUNXI,      \
        .name       = "sun8i",

        .atag_offset    = 0x100,
        .init_machine   = sunxi_dev_init,
        .init_early     = sunxi_init_early,
        .map_io     = sunxi_map_io,
    #ifndef CONFIG_OF
        .init_irq   = sun8i_gic_init,
    #endif
        .handle_irq = gic_handle_irq,
        .restart    = sun8i_restart,
        .timer      = &sunxi_timer,
        .dt_compat  = NULL,
        .reserve    = sun8i_reserve,
        .fixup      = sun8i_fixup,
        .nr_irqs    = NR_IRQS,
    #ifdef CONFIG_SMP
        .smp        = smp_ops(sunxi_smp_ops),
    #ifdef CONFIG_ARCH_SUN8IW6
        .smp_init   = smp_init_ops(sun8i_smp_init_ops),
    #endif
    };

上面的属性就是配置了struct machine_desc 这个结构体,和结构体内容一一对应,从内核得出这些内容后我们就可以和 uboot 传入的参数进行对比,uboot 传数据给内核是从 uboot源码里面的 arch/arm/lib/bootm.c 这个文件的 do_boota_linux 函数,具体调用以下函数:

kernel_entry(0, bd->bi_arch_number, bd->bi_boot_params);

这个函数传入的参数刚好和上面的 MACHINE_START(machine_desc结构体) 刚好对应起来,然后内核启动的时候就会从初始化的 begin 开始读到 end :

.init.arch.info : {
    __arch_info_begin = .;
    *(.arch.info.init)
    __arch_info_end = .;
}

把uboot传入的ID和内核的ID做比较,如果吻合的话表示支持这个单板,然后还会接着比较后面的参数


2、解析u-boot传入的启动参数
上一章通过分析 linux 源码的 Makefile 和脚本后发现内容启动的第一个程序是:arch/arm/kernel/head.S
这里截取关键的代码如下:

__lookup_processor_type /* 判断是否支持这个CPU处理器 */ 
__create_page_tables /* 创建页表:因为链接脚本的起始地址并不对应于真实存在的地址,因此需要创建页表来启动MMU */
__enable_mmu      /* 使能MMU */
__mmap_switched   /* 当MMU使能之后会跳到这个switch里面去,复制数据段、清BSS段、设置栈指针、保存processor_id变量、保存机器类型ID */
    start_kernel    /* 启动内核,这是第一个C函数,在里面处理启动参数 */
        init....    /* 一系列的初始化 */
        setup_arch(&command_line); /*  这两句解析uboot传递过来的参数,把参数从 machine_desc 结构体取出来保存 */
        setup_command_line(command_line); 
        ...
        parse_early_param  /* 早期参数初始化 */
            parse_early_options
                do_early_param
                    /* 从 __setup_start 到 __setup_end 这是在 vmlinux.lds.S 里面定义的 INIT_SETUP 段  作用是调用那些用early来标识的函数,但是我们函数传递进来的early是0,所以这里用不着 */
        unknown_bootoption
            obsolete_checksetup
                /* 处理从 __setup_start 到 __setup_end 这个段调用的非early的函数 */
        ...
        rest_init();
            kernel_init 
                sys_open((const char __user *) "/dev/console", O_RDWR, 0) /* 打开终端,标准输出 */
                (void) sys_dup(0); /* dup是复制的意思,这里代表标准输入 */
                (void) sys_dup(0); /*标准错误 ,打开的这三个文件都指向console这个设备,这个设备会给busybox应用 */
                prepare_namespace
                    saved_root_name[0]  /* 从uboot设置好的root名读取保存(root_dev_setup函数)到saved_root_name里面, */
                        /* 如何定义 saved_root_name 的 */
                        __setup("root=", root_dev_setup);  /*这个宏定义了一个结构体,这个结构体里面有两个成员,其实就是一个字符串对应一个处理函数*/

                        #define __setup(str, fn)                    \
                            __setup_param(str, fn, fn, 0)  /* 第四个参数是early,这里被设置为0,所以上面的 parse_early_param 可以忽略*/

                        #define __setup_param(str, unique_id, fn, early)            \
                            static const char __setup_str_##unique_id[] __initconst \
                                __aligned(1) = str; \
                            static struct obs_kernel_param __setup_##unique_id  \
                                __used __section(.init.setup)           \      
                                    /* 这个结构体有个属性,这个属性被强制设备为(.init.setup)这个段,
                                        这个段在链接脚本里面有定义(INIT_SETUP)里面可以找到__setup_start*/
                                __attribute__((aligned((sizeof(long)))))    \
                                = { __setup_str_##unique_id, fn, early }

                        /* 查看它被谁使用,就知道命令行是怎么调用的了,搜索 __setup_start 查看谁调用了,有如下两个函数调用了:*/
                        obsolete_checksetup /* 被 unknown_bootoption 调用 */ 
                        do_early_param      /* 被 parse_early_param 调用 */

                    mount_root();    /* 挂接根文件系统 */

                /* 开始执行应用程序 */
                init_post
                    /* 一旦执行了以下任意一个应用程序就是一个死循环,不会再执行后面的了 */
                    run_init_process(execute_command);  /* 搜索关键字execute_command可以发现init_setup调用了它 */
                        __setup("init=", init_setup) /* init是一个从uboot命令行参数,uboot如果设置了init,则 execute_command 等于设置的值 */
                    run_init_process("/sbin/init"); //执行应用程序,如果不成功则依次往下执行
                    ... 

3、挂接根文件系统、执行第一个应用程序
在上面代码中,先是调用了挂接根文件系统的函数,然后使用函数 run_init_process 执行应用程序,我们可以在 uboot 参数里面定义 init 变量来告诉内核第一个应用程序执行哪个文件,如果没有设置第一个是执行 /sbin/bin,而一般情况下我们第一个程序是 busybox,我们可以先启动系统然后使用 ps 指令查看下当前运行的进程,但是并不会发现有 ls 、cd、cp 等这些程序,但我们一启动就能使用这些命令,那这些程序哪里来的呢?就来源于 busybox 这个应用程序,我们可以使用指令来验证一下:

# ls -l /bin/ls
lrwxrwxrwx    1 root     root             7 Oct 18  2017 /bin/ls -> busybox

可以看出 ls 软链接到了 busybox,所以执行 ls 就等于执行 busybox 里面的 ls,busybox 这个应用存放于 /bin 这个目录。
我的内容在 uboot 中是将 init 设置为了 /init,即 init = /init ,它其实也是链接到了 busybox:

# ls /init -l
lrwxrwxrwx    1 root     root            11 Dec 14  2017 /init -> bin/busybox

而且如果我们没定义 init ,它就会根据我们上面分析的代码去执行 /sbin/init ,而 /sbin/init 也是链接到了 busybox:

# ls /sbin/init -l
lrwxrwxrwx    1 root     root            14 Oct 18  2017 /sbin/init ->../bin/busybox

既然第一个执行的程序是 busybox,下面可以来简单分析一下,看下 init 程序做了些什么
busybox 里面的 init 程序主要执行了以下的一些内容:
1、读取配置文件 –> 这个配置文件指定了后续要读取哪些程序
2、解析配置文件
3、根据配置文件来启动执行“用户程序”


busybox 源码分析:

init_main
    parse_inittab
        file = fopen(INITTAB, "r"); /* 打开"/etc/inittab"这个配置文件 */
        new_init_action  /* 根据配置文件调用这个函数,创建一个init_action结构,填充结构体,把结构放入 init_action_list 链表 */
            new_init_action(ASKFIRST, bb_default_login_shell, VC_2) 为例分析 new_init_action
            const char bb_default_login_shell[] ALIGN1 = LIBBB_DEFAULT_LOGIN_SHELL;
            #define LIBBB_DEFAULT_LOGIN_SHELL      "-/bin/sh"
            # define VC_2 "/dev/tty2"
            带入即等于: new_init_action(ASKFIRST,"-/bin/sh","/dev/tty2")
                for (a = last = init_action_list; a; a = a->next) /* 创建一个init_action结构,填充结构体,把结构放入链表 */

    run_actions(SYSINIT);  /* 运行 SYSINIT 这一类型的动作 */
        waitfor(a, 0);   /* 执行应用程序,等待它执行完毕 */   
        delete_init_action(a); /* 从 init_action_list 链表里删除 */
    run_actions(WAIT); /* 运行 WAIT 这一类型的动作,过程同上 */
    run_actions(ONCE); /* 运行 ONCE 这一类型的动作 */

    while(1){ /* 这个循环用于命令行运行应用程序使用,结束一个线程就等待用户重新输入 */
        run_actions(RESPAWN);
            if (a->pid == 0){ /* 一开始PID是等于0的 */
                a-pid = run(a);
            }
        run_actions(ASKFIRST);
            if (a->pid == 0){
                a-pid = run(a); /* 等待回车,创建子线程 */
                    打印"\nPlease press Enter to activate this console. "
            }
        wpid = wait(NULL); /* 等待子进程退出 */
        while (wpid > 0) {
            a->pid = 0;  /* 退出后,就设置 pid=0,重新回到while循环继续执行上面已退出的子进程 */
        }
    }

上面的 /etc/inittab 配置文件的书写格式如下:

<id>:<runlevels>:<action>:<process>
id : /dev/id
runlevels : 忽略
action : 何时执行,有以下选项
    # <action>: Valid actions include: sysinit, respawn, askfirst, wait, once,
    #                                  restart, ctrlaltdel, and shutdown.
process : 应用程序或脚本

从 busybox 可以看出最小根文件系统至少需要什么东西:

1. /dev/console 或 /dev/null(如果没有定义console,stdout/in/err 就定位到null去)
2. /etc/inintab
3. 配置文件里指定的应用程序
4. C库
5. init本身,即busybox

猜你喜欢

转载自blog.csdn.net/itdo_just/article/details/78832751