一. 跟踪Linux内核的启动过程
1.操作系统的两把宝剑和三大法宝
两把宝剑
一把是中断上下文的切换——保存现场和恢复现场
另一把是进程上下文的切换
三大法宝
存储程序计算机
函数调用堆栈机制
中断
2.跟踪分析Linux内核的启动过程
构建Linux系统MenuOS
$ cd ~/LinuxKernel/ $ qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明: # 1. -S # -S freeze CPU at startup (use ’c’ to start execution) # 2. -s # -s shorthand for -gdb tcp::1234 # 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项
# 打开 GDB 调试器 gdb # 在 GDB 中输入以下命令: # 在gdb界面中targe remote之前加载符号表 (gdb)file linux-3.18.6/vmlinux # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行 (gdb)target remote:1234 # 断点的设置可以在target remote之前,也可以在之后 (gdb)break start_kernel
重新开一个shell 输入如下命令:
cd ~/LinuxKernel/
开启gdb调试 命令如上设置断点 在start_kernel处 然后命令:c 开始调试
使用list命令,可以看到start_kernel函数的上下文
start_kernel函数:
asmlinkage __visible void __init start_kernel(void) { char *command_line; char *after_dashes; /* * Need to run as early as possible, to initialize the * lockdep hash: */ lockdep_init(); set_task_stack_end_magic(&init_task); smp_setup_processor_id(); debug_objects_early_init(); /* * Set up the the initial canary ASAP: */ boot_init_stack_canary(); cgroup_init_early(); local_irq_disable(); early_boot_irqs_disabled = true; /* * Interrupts are still disabled. Do necessary setups, then * enable them */ boot_cpu_init(); page_address_init(); pr_notice("%s", linux_banner); setup_arch(&command_line); mm_init_cpumask(&init_mm); setup_command_line(command_line); setup_nr_cpu_ids(); setup_per_cpu_areas(); smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */ build_all_zonelists(NULL, NULL); page_alloc_init(); pr_notice("Kernel command line: %s ", boot_command_line); parse_early_param(); after_dashes = parse_args("Booting kernel", static_command_line, __start___param, __stop___param - __start___param, -1, -1, &unknown_bootoption); if (!IS_ERR_OR_NULL(after_dashes)) parse_args("Setting init args", after_dashes, NULL, 0, -1, -1, set_init_arg); jump_label_init(); /* * These use large bootmem allocations and must precede * kmem_cache_init() */ setup_log_buf(0); pidhash_init(); vfs_caches_init_early(); sort_main_extable(); trap_init(); mm_init(); /* * Set up the scheduler prior starting any interrupts (such as the * timer interrupt). Full topology setup happens at smp_init() * time - but meanwhile we still have a functioning scheduler. */ sched_init(); /* * Disable preemption - early bootup scheduling is extremely * fragile until we cpu_idle() for the first time. */ preempt_disable(); if (WARN(!irqs_disabled(), "Interrupts were enabled *very* early, fixing it ")) local_irq_disable(); idr_init_cache(); rcu_init(); context_tracking_init(); radix_tree_init(); /* init some links before init_ISA_irqs() */ early_irq_init(); init_IRQ(); tick_init(); rcu_init_nohz(); init_timers(); hrtimers_init(); softirq_init(); timekeeping_init(); time_init(); sched_clock_postinit(); perf_event_init(); profile_init(); call_function_init(); WARN(!irqs_disabled(), "Interrupts were enabled early "); early_boot_irqs_disabled = false; local_irq_enable(); kmem_cache_init_late(); /* * HACK ALERT! This is early. We're enabling the console before * we've done PCI setups etc, and console_init() must be aware of * this. But we do want output early, in case something goes wrong. */ console_init(); if (panic_later) panic("Too many boot %s vars at `%s'", panic_later, panic_param); lockdep_info(); /* * Need to run this when irqs are enabled, because it wants * to self-test [hard/soft]-irqs on/off lock inversion bugs * too: */ locking_selftest(); #ifdef CONFIG_BLK_DEV_INITRD if (initrd_start && !initrd_below_start_ok && page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) { pr_crit("initrd overwritten (0x%08lx < 0x%08lx) - disabling it. ", page_to_pfn(virt_to_page((void *)initrd_start)), min_low_pfn); initrd_start = 0; } #endif page_cgroup_init(); debug_objects_mem_init(); kmemleak_init(); setup_per_cpu_pageset(); numa_policy_init(); if (late_time_init) late_time_init(); sched_clock_init(); calibrate_delay(); pidmap_init(); anon_vma_init(); acpi_early_init(); #ifdef CONFIG_X86 if (efi_enabled(EFI_RUNTIME_SERVICES)) efi_enter_virtual_mode(); #endif #ifdef CONFIG_X86_ESPFIX64 /* Should be run before the first non-init thread is created */ init_espfix_bsp(); #endif thread_info_cache_init(); cred_init(); fork_init(totalram_pages); proc_caches_init(); buffer_init(); key_init(); security_init(); dbg_late_init(); vfs_caches_init(totalram_pages); signals_init(); /* rootfs populating might need page-writeback */ page_writeback_init(); proc_root_init(); cgroup_init(); cpuset_init(); taskstats_init_early(); delayacct_init(); check_bugs(); sfi_init_late(); if (efi_enabled(EFI_RUNTIME_SERVICES)) { efi_late_init(); efi_free_boot_services(); } ftrace_init(); /* Do the rest non-__init'ed, we're now alive */ rest_init(); }
开启gdb调试 命令如上设置断点 在rest_init处 然后命令:c 开始调试
使用list命令,可以看到rest_init函数的上下文
rest_init函数:
static noinline void __init_refok rest_init(void) { int pid; rcu_scheduler_starting(); /* * We need to spawn init first so that it obtains pid 1, however * the init task will end up wanting to create kthreads, which, if * we schedule it before we create kthreadd, will OOPS. */ kernel_thread(kernel_init, NULL, CLONE_FS); numa_default_policy(); pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); rcu_read_lock(); kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns); rcu_read_unlock(); complete(&kthreadd_done); /* * The boot idle thread must execute schedule() * at least once to get things moving: */ init_idle_bootup_task(current); schedule_preempt_disabled(); /* Call into cpu_idle with preempt disabled */ cpu_startup_entry(CPUHP_ONLINE); }
关于函数的解释:
setup_arch(&command_line); //设置与初始化硬件体系相关的环境并调用 sched_init() //初始化调度器,先于中断开始前 printk(boot_command_line); //提取分析核心启动参数过程(从 bootloader 中传递) trap_init(); //自陷入口函数初始化,针对此版本arm中直接return early_irq_init(); //中断初始化过程 init_IRQ(); init_timers(); //初始化定时器,开启定时器软中断服务以及注册服务程序以及初始化各CPU中的tev_base等init_timers()->run_timer_softirq()->__run_timers() timekeeping_init(); time_init(); //设置定时器及返回当前时间 console_init() //初步的初始化控制台,此控制台只能打印出一些简单//的启动信息… mem_init(); //初始化内存并计算可用内存大小 kmem_cache_init(); // 初始化SLAB缓存分配器 calibrate_delay(); //延迟校准,jiffy,记录系统的定时器的节拍数,每变化一次代表了系统定时器2个连续节拍时间的间隔。 fork_init(num_physpages); //初始化max_threads,init_task参数为fork()提供参考 buffer_init(); //初始化块设备读写缓冲区 vfs_caches_init(num_physpages); //初始化虚拟文件系统 inode_init() ->files_init() ->mnt_init()... signals_init(); //初始化内核信号队列…. rest_init(); //最后实际进入reset_init()函数,包括所有剩下的硬件//驱动,线程初始化等过程…这也最终完成//start_kernel//的启动过程。
3.分析结果
start_kernel()函数在main.c中起着main函数的作用,打开系统以后,首先对硬件系统进行初始化,给C代码的运行配置好环境,start_kernel函数被调用,在start_kernel函数在开始运行后,它会调用各个内核模块的初始化函数,主要包括:trap_init()
中断向量初始化,mm_init()
内存管理初始化,sched_init()
调度模块初始化等,在整个的初始化过程中有一个init_task,它是一个进程描述符,负责内核模块的初始化,也就是0号进程,0号进程会新建kernel_init线程(1号内核线程)和kthreadd线程(2号内核线程),初始化工作完成后,init_task()调用cpu_idle()转化为ideal空进程。
0号进程由系统自动创建, 运行在内核态,其前身是系统创建的第一个进程,也是唯一一个没有通过fork或者kernel_thread产生的进程。完成加载系统后,演变为进程调度、交换。
1号进程由idle通过kernel_thread创建,在内核空间完成初始化后,加载init程序,由0进程创建,完成系统的初始化,是系统中所有其它用户进程的祖先进程。Linux中的所有进程都是有init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成完成后,init将变为守护进程监视系统其他进程。
2号进程由idle通过kernel_thread创建,并始终运行在内核空间, 负责所有内核线程的调度和管理。它的任务就是管理和调度其他内核线程kernel_thread, 会循环执行一个kthread的函数,该函数的作用就是运行kthread_create_list全局链表中维护的kthread, 当我们调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程。