《Linux内核分析》(三)分析Linux内核的启动过程
原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一、实验过程:
使用实验楼的虚拟机打开shell
- cd LinuxKernel/
- qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
使用gdb跟踪调试内核
- qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明:
- # -S freeze CPU at startup (use ’c’ to start execution)
- # -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项另开一个shell窗口
- gdb
- (gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表
- (gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
- (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_kernel到init进程启动的过程(也即1号进程是怎么来的),以及0号进程是怎么来的:
进程1又称为init进程,是所有用户进程的祖先,是由Linux内核直接启动的,即由进程0在start_kernel调用rest_init创建。
start_kernel()是内核的汇编与C语言的交接点,在该函数以前,内核的代码都是用汇编写的,完成一些最基本的初始化与环境设置工作,比如内核代码载入内存并解压缩(现在的内核一般都经过压缩),CPU的最基本初始化,为C代码的运行设置环境(C代码的运行是有一定环境要求的,比如stack的设置等)。这里一个不太确切的比喻是start_kernel()就像是C代码中的main()。我们知道对应用程序员而言,main()是他的入口,但实际上程序的入口是被包在了C库中,在链接阶段,linker会把它链接入你的程序中。而它的任务中有一项就是为main()准备运行环境。main()中的argc,argv等都不是平白无故来的,都是在调用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号进程是怎么来的,我在上面的叙述中已经说得很清楚了。
浙公网安备 33010602011771号