《Linux内核分析》(三)分析Linux内核的启动过程

      原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

 

一、实验过程:

使用实验楼的虚拟机打开shell

  1. cd LinuxKernel/
  2. qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img

使用gdb跟踪调试内核

  1. qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明:
  2. # -S freeze CPU at startup (use ’c’ to start execution)
  3. # -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项另开一个shell窗口
  1. gdb
  2. (gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表
  3. (gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
  4. (gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后

 

实验截图如下:

启动menuos;

 

 跟踪start_kernel()函数:

 

 下面使用gdb跟踪一下trap_init()mm_init()sched_init()、 rest_init()、 rest_init()这几个函数。

 trap_init():中断的初始化

它在set_intr_gate里设置了很多中断门。set_system_trap_gate(SYSCALL_VECTOR &system_call)表示设置系统调用的中断门。

 

 mm_init():内存管理的初始化

 

sched_init():调度函数的初始化。

 

 

 rest_init():这是Linux内核初始化的尾声。

 

 

二、实验分析:

  接下来仔细分析内核从start_kernelinit进程启动的过程(也即1号进程是怎么来的),以及0号进程是怎么来的:

  进程1又称为init进程,是所有用户进程的祖先,是由Linux内核直接启动的,即由进程0start_kernel调用rest_init创建。

  start_kernel()是内核的汇编与C语言的交接点,在该函数以前,内核的代码都是用汇编写的,完成一些最基本的初始化与环境设置工作,比如内核代码载入内存并解压缩(现在的内核一般都经过压缩),CPU的最基本初始化,为C代码的运行设置环境(C代码的运行是有一定环境要求的,比如stack的设置等)。这里一个不太确切的比喻是start_kernel()就像是C代码中的main()。我们知道对应用程序员而言,main()是他的入口,但实际上程序的入口是被包在了C库中,在链接阶段,linker会把它链接入你的程序中。而它的任务中有一项就是为main()准备运行环境。main()中的argcargv等都不是平白无故来的,都是在调用main()以前的代码做的准备。

  在start_kernel()Linux将完成整个系统的内核初始化。内核初始化的最后一步就是启动init进程这个所有进程的祖先。

  下面是start_kernel()函数的代码(start_kernel函数所在文件的路径为"/linux-3.18.6/init/main.c"):

 

asmlinkage __visible void __init start_kernel(void)
501{
502    char *command_line;
503    char *after_dashes;
504
505    /*
506     * Need to run as early as possible, to initialize the
507     * lockdep hash:
508     */
509    lockdep_init();
510    set_task_stack_end_magic(&init_task);
511    smp_setup_processor_id();
512    debug_objects_early_init();
513
514    /*
515     * Set up the the initial canary ASAP:
516     */
517    boot_init_stack_canary();
518
519    cgroup_init_early();
520
521    local_irq_disable();
522    early_boot_irqs_disabled = true;
523
524/*
525 * Interrupts are still disabled. Do necessary setups, then
526 * enable them
527 */
528    boot_cpu_init();
529    page_address_init();
530    pr_notice("%s", linux_banner);
531    setup_arch(&command_line);
532    mm_init_cpumask(&init_mm);
533    setup_command_line(command_line);
534    setup_nr_cpu_ids();
535    setup_per_cpu_areas();
536    smp_prepare_boot_cpu();    /* arch-specific boot-cpu hooks */
537
538    build_all_zonelists(NULL, NULL);
539    page_alloc_init();
540
541    pr_notice("Kernel command line: %s\n", boot_command_line);
542    parse_early_param();
543    after_dashes = parse_args("Booting kernel",
544                  static_command_line, __start___param,
545                  __stop___param - __start___param,
546                  -1, -1, &unknown_bootoption);
547    if (!IS_ERR_OR_NULL(after_dashes))
548        parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
549               set_init_arg);
550
551    jump_label_init();
552
553    /*
554     * These use large bootmem allocations and must precede
555     * kmem_cache_init()
556     */
557    setup_log_buf(0);
558    pidhash_init();
559    vfs_caches_init_early();
560    sort_main_extable();
561    trap_init();
562    mm_init();
563
564    /*
565     * Set up the scheduler prior starting any interrupts (such as the
566     * timer interrupt). Full topology setup happens at smp_init()
567     * time - but meanwhile we still have a functioning scheduler.
568     */
569    sched_init();
570    /*
571     * Disable preemption - early bootup scheduling is extremely
572     * fragile until we cpu_idle() for the first time.
573     */
574    preempt_disable();
575    if (WARN(!irqs_disabled(),
576         "Interrupts were enabled *very* early, fixing it\n"))
577        local_irq_disable();
578    idr_init_cache();
579    rcu_init();
580    context_tracking_init();
581    radix_tree_init();
582    /* init some links before init_ISA_irqs() */
583    early_irq_init();
584    init_IRQ();
585    tick_init();
586    rcu_init_nohz();
587    init_timers();
588    hrtimers_init();
589    softirq_init();
590    timekeeping_init();
591    time_init();
592    sched_clock_postinit();
593    perf_event_init();
594    profile_init();
595    call_function_init();
596    WARN(!irqs_disabled(), "Interrupts were enabled early\n");
597    early_boot_irqs_disabled = false;
598    local_irq_enable();
599
600    kmem_cache_init_late();
601
602    /*
603     * HACK ALERT! This is early. We're enabling the console before
604     * we've done PCI setups etc, and console_init() must be aware of
605     * this. But we do want output early, in case something goes wrong.
606     */
607    console_init();
608    if (panic_later)
609        panic("Too many boot %s vars at `%s'", panic_later,
610              panic_param);
611
612    lockdep_info();
613
614    /*
615     * Need to run this when irqs are enabled, because it wants
616     * to self-test [hard/soft]-irqs on/off lock inversion bugs
617     * too:
618     */
619    locking_selftest();
620
621#ifdef CONFIG_BLK_DEV_INITRD
622    if (initrd_start && !initrd_below_start_ok &&
623        page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
624        pr_crit("initrd overwritten (0x%08lx < 0x%08lx) - disabling it.\n",
625            page_to_pfn(virt_to_page((void *)initrd_start)),
626            min_low_pfn);
627        initrd_start = 0;
628    }
629#endif
630    page_cgroup_init();
631    debug_objects_mem_init();
632    kmemleak_init();
633    setup_per_cpu_pageset();
634    numa_policy_init();
635    if (late_time_init)
636        late_time_init();
637    sched_clock_init();
638    calibrate_delay();
639    pidmap_init();
640    anon_vma_init();
641    acpi_early_init();
642#ifdef CONFIG_X86
643    if (efi_enabled(EFI_RUNTIME_SERVICES))
644        efi_enter_virtual_mode();
645#endif
646#ifdef CONFIG_X86_ESPFIX64
647    /* Should be run before the first non-init thread is created */
648    init_espfix_bsp();
649#endif
650    thread_info_cache_init();
651    cred_init();
652    fork_init(totalram_pages);
653    proc_caches_init();
654    buffer_init();
655    key_init();
656    security_init();
657    dbg_late_init();
658    vfs_caches_init(totalram_pages);
659    signals_init();
660    /* rootfs populating might need page-writeback */
661    page_writeback_init();
662    proc_root_init();
663    cgroup_init();
664    cpuset_init();
665    taskstats_init_early();
666    delayacct_init();
667
668    check_bugs();
669
670    sfi_init_late();
671
672    if (efi_enabled(EFI_RUNTIME_SERVICES)) {
673        efi_late_init();
674        efi_free_boot_services();
675    }
676
677    ftrace_init();
678
679    /* Do the rest non-__init'ed, we're now alive */
680    rest_init();
681}

 在start_kernel()的最后,是rest_init(),它是Linux内核初始化的尾声,rest_init函数里将启动init进程。

rest_init()函数的代码如下:

 

static noinline void __init_refok rest_init(void)
394{
395    int pid;
396
397    rcu_scheduler_starting();
398    /*
399     * We need to spawn init first so that it obtains pid 1, however
400     * the init task will end up wanting to create kthreads, which, if
401     * we schedule it before we create kthreadd, will OOPS.
402     */
403    kernel_thread(kernel_init, NULL, CLONE_FS);
404    numa_default_policy();
405    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
406    rcu_read_lock();
407    kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
408    rcu_read_unlock();
409    complete(&kthreadd_done);
410
411    /*
412     * The boot idle thread must execute schedule()
413     * at least once to get things moving:
414     */
415    init_idle_bootup_task(current);
416    schedule_preempt_disabled();
417    /* Call into cpu_idle with preempt disabled */
418    cpu_startup_entry(CPUHP_ONLINE);
419}

其中,

405    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);

 创建一个内核进程来管理系统资源。

 

403    kernel_thread(kernel_init, NULL, CLONE_FS);

 创建一个内核进程,由此进入kernel_init函数。

 kernel_init函数的代码如下: 

static int __ref kernel_init(void *unused)
931{
932    int ret;
933
934    kernel_init_freeable();
935    /* need to finish all async __init code before freeing the memory */
936    async_synchronize_full();
937    free_initmem();
938    mark_rodata_ro();
939    system_state = SYSTEM_RUNNING;
940    numa_default_policy();
941
942    flush_delayed_fput();
943
944    if (ramdisk_execute_command) {
945        ret = run_init_process(ramdisk_execute_command);
946        if (!ret)
947            return 0;
948        pr_err("Failed to execute %s (error %d)\n",
949               ramdisk_execute_command, ret);
950    }
951
952    /*
953     * We try each of these until one succeeds.
954     *
955     * The Bourne shell can be used instead of init if we are
956     * trying to recover a really broken machine.
957     */
958    if (execute_command) {
959        ret = run_init_process(execute_command);
960        if (!ret)
961            return 0;
962        pr_err("Failed to execute %s (error %d).  Attempting defaults...\n",
963            execute_command, ret);
964    }
965    if (!try_to_run_init_process("/sbin/init") ||
966        !try_to_run_init_process("/etc/init") ||
967        !try_to_run_init_process("/bin/init") ||
968        !try_to_run_init_process("/bin/sh"))
969        return 0;
970
971    panic("No working init found.  Try passing init= option to kernel. "
972          "See Linux Documentation/init.txt for guidance.");
973}

 run_init_process(ramdisk_execute_command):这是启动init进程(也即是1号进程)的代码。

run_init_process是通过execve()来运行init进程的,默认是根目录下的init进程(通过分析ramdisk_execute_command这个变量的值的变化,可知其默认为”/init”,也即根目标下的init进程)。如果根目标下没有init文件,则运行"/sbin/init"、"/etc/init"、"/bin/init"或"/bin/sh"。

 

  以上分析了init进程是怎么来的,也即1号进程是怎么来的,但还没有分析0号进程是怎么来的。在rest_init函数的最后,有一个cpu_startup_entry函数,cpu_startup_entry函数调用cpu_idle_loop函数,分析cpu_idle_loop()可知,当系统没有进程需执行时就会调度到idle进程(也即0号进程)。rest-init实际上start-kernel从内核进程启动后会一直存在,也即idle进程(0号进程)从start-kernel启动后会一直存在。

 

三、实验总结

    通过这次实验,我对Linux系统的启动过程有了一定的理解,Linux首先完成一些最基本的初始化与环境设置工作,比如内核代码载入内存并解压缩(现在的内核一般都经过压缩),CPU的最基本初始化,为C代码的运行设置环境(C代码的运行是有一定环境要求的,比如stack的设置等),然后,运行start_kernel()函数,在start_kernel()中Linux将完成整个系统的内核初始化。内核初始化的最后一步就是启动init进程(即1号进程)这个所有用户进程的祖先。至于idle进程和1号进程是怎么来的,我在上面的叙述中已经说得很清楚了。

posted on 2015-03-21 18:55  xyon  阅读(335)  评论(0)    收藏  举报