寻找程序入口

和uboot一样,分析启动流程需要先分析链接脚本文件lds找到程序的入口
对应uboot.lds,linux的链接脚本路径为arch/arm/kernel/vmlinux.lds。分析该文件,我们可以看到Linux内核的入口程序为stext

  1. OUTPUT_ARCH(arm)
  2. ENTRY(stext) //entry指定了入口程序
  3. jiffies = jiffies_64;
  4. SECTIONS
  5. {
  6. //...
  7. }

stext入口程序

stext是linux内核的入口函数,位于arch/arm/kernel/head.S中。其主要调用关系如下:
image.png

start_kernel

start_kernel位于init/main.c中,由一系列子函数完成Linux启动之前的初始化工作。
下面列举了比较重要的函数:

asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
{
    //...
    //设置任务栈结束魔术数,用于栈溢出检测
    set_task_stack_end_magic(&init_task);
    //设置多核处理器ID
    smp_setup_processor_id();
    //debug相关的初始化
    debug_objects_early_init();
    //cgroup初始化,用于控制系统资源
    cgroup_init_early();
    //关闭CPU中断
    local_irq_disable();

    //...
    boot_cpu_init();//CPU初始化
    page_address_init();//页地址初始化
    pr_notice("%s", linux_banner);//打印Linux版本号,编译时间等信息

    //...
    //架构相关初始化,解析传进来的设备树.
    //会根据dtb里model和compatible查找linux是否支持这个板卡.
    //获取dtb中chosen节点的bootargs参数(uboot),保存到command_line
    setup_arch(&command_line);
    setup_boot_config(command_line);
    //存储命令行参数
    setup_command_line(command_line);
    setup_nr_cpu_ids();//如果是多核cpu,获取cpu核心数量
    setup_per_cpu_areas();//多核环境,设置每个cpu的per-cpu数据

    //...
    boot_cpu_hotplug_init();
    //建立系统内存页区链表
    build_all_zonelists(NULL);
    page_alloc_init();//处理用于热插拔cpu的页
    //打印命令行参数
    pr_notice("Kernel command line: %s\n", saved_command_line);
    parse_early_param();//解析命令行中console参数

    //...
    setup_log_buf(0);//设置log使用的buffer
    //预先初始化虚拟文件系统(目录项和索引节点缓存)
    vfs_caches_init_early();
    sort_main_extable();//定义内核异常列表
    trap_init();//初始化系统保留中断向量
    mm_init();//内存管理初始化

    //...
    sched_init();//初始化调度器,主要是一些结构体
    preempt_disable();//关闭优先级抢占
    radix_tree_init();//基数树相关数据结构初始化

    //...
    rcu_init();//初始化RCU,即read copy update
    trace_init();//跟踪调试相关的初始化

    //...
    //初始中断初始化,主要是注册irq_desc结构体(用于描述一个中断)
    early_irq_init();
    init_IRQ();//中断初始化
    tick_init();//tick初始化
    rcu_init_nohz();
    init_timers();//初始化定时器
    hrtimers_init();//初始化高精度定时器
    softirq_init();//软中断初始化

    //...
    boot_init_stack_canary();//栈溢出检测初始化
    time_init();//初始化系统时间

    //...
    local_irq_enable();//enable中断
    kmem_cache_init_late();//初始化linux内存分配器slab
    //初始化控制台,初始化之后printk的信息才能打印出来
    console_init();
    lockdep_init();//死锁检测模块,初始化两个哈希表
    locking_selftest();//锁自测

    //...
    calibrate_delay();//测定BogoMIPS值,值越大,CPU性能越好
    anon_vma_init();//生成anon_vma slab缓存
    fork_init();//初始化结构体以支持fork函数
    proc_caches_init();//给各种资源管理结构分配缓存
    uts_ns_init();
    buffer_init();//初始化缓冲缓存
    key_init();//初始化密钥
    security_init();//安全相关初始化
    dbg_late_init();
    vfs_caches_init();//为虚拟文件系统创建缓存
    pagecache_init();
    signals_init();//初始化信号
    seq_file_init();
    proc_root_init();//注册并挂在proc文件系统
    nsfs_init();
    //初始化cpuset,cpuset是将cpu和内存资源以逻辑性和层次性集成的一种机制
    cpuset_init();
    cgroup_init();//初始化cgroup
    taskstats_init_early();//进程状态初始化
    delayacct_init();

    poking_init();
    check_bugs();//检查写缓冲一致性

    //...
    arch_call_rest_init();//内部调用rest_init函数
}

在函数最后,调用了rest_init函数,接下来看看这个函数的作用。

rest_init

该函数还在init/main.c文件中,主要内容如下

  1. 创建1号线程,即 init线程,线程函数是kernel_init
  2. 创建 2 号线程,即 ktbreadd线程,负责创建内核线程
  3. 0 号线程最终变成空闲线程

    noinline void __ref rest_init(void)
    {
     //...
     rcu_scheduler_starting();//启动RCU锁调度器
     //创建大名鼎鼎的init内核进程,pid=1
     pid = kernel_thread(kernel_init, NULL, CLONE_FS);
    
     //...
     //创建kthreadd内核进程,负责所有内核新城的调度和管理,pid=2
     pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
    
     //...
     //该函数调用cpi_idle_loop进入idle进程.pid=0,也叫空闲进程,有本函数执行的主进程演变而来
     cpu_startup_entry(CPUHP_ONLINE);
    }
    

    之后CPU将主要用于init内核进程的执行,下面看看init进程在干些什么。

    init进程

    init进程由kernel_init函数生成,该函数还在init/main.c中。其主要任务是找到linux文件系统(先挂载文件系统)中的init程序并运行:

    kernel_init_freeable

    上面提到需要先挂载文件系统,不然后面没法找到init或shell程序的位置。挂载文件系统和其他一些初始化工作都是在kernel_init_freeable中执行的:
    image.png

    运行init用户进程

    static int __ref kernel_init(void *unused)
    {
     int ret;
    
     kernel_init_freeable();//挂载FS等其他初始化工作
     async_synchronize_full();//等待所有的异步调用执行完成
    
     //...
     free_initmem();//释放init段内存
     system_state = SYSTEM_RUNNING;//标记系统处于正在运行状态
    
     //查找init程序位置,运行成功则返回,否则继续查找
     if (ramdisk_execute_command) {
         //全局变量,值为/init,即执行根目录init程序
         ret = run_init_process(ramdisk_execute_command);
         if (!ret)
             return 0;
         pr_err("Failed to execute %s (error %d)\n",
                ramdisk_execute_command, ret);
     }
    
     if (execute_command) {
         //上面变量为空,则尝试执行本变量,直到找到可运行的init程序为止
         ret = run_init_process(execute_command);
         if (!ret)
             return 0;
         panic("Requested init %s failed (error %d).",
               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;
    }
    

    如果最后init和备用的shell程序都运行失败,则Linux内核启动失败。

    SMP系统的引导

    对称多处理器 ( Symmetric Multi-Processor, SMP ) 系统包含多个处理器,并且每个处理器的地位平等。在启动过程中(start_kernel阶段),处理器的地位不是平等的,0号处理器称为引导处理器,负责执行引导程序和初始化内核;其他处理器称为从处理器,等待引导处理器完成初始化。引导处理器初始化内核以后,启动从处理器:
    图片.png