分析Linux內核啓動過程:從start_kernel到init

鄭德倫 原創作品轉載請註明出處《Linux內核分析》MOOC課程
http://mooc.study.163.com/course/USTC-1000029000
STEP1:在自己的linux系統中搭建實驗環境。
1.下載linux-3.18.6的內核源碼,並且編譯

 cd ~/LinuxKernel/  wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.6.tar.xz
  xz -d linux-3.18.6.tar.xz
  tar -xvf linux-3.18.6.tar
  cd linux-3.18.6
  make i386_defconfig
  make # 一般要編譯很長時間,少則20分鐘多則數小時

這裏寫圖片描述
這裏寫圖片描述
2.製作根文件系統

  cd ~/LinuxKernel/
  mkdir rootfs
  git clone  https://github.com/mengning/menu.git
  cd menu
  gcc -o init linktable.c menu.c test.c -m32 -static –lpthread
  cd ../rootfs
  cp ../menu/init ./
  find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img

3.啓動MenuOS

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

然後就啓動了MenuOS。
這裏寫圖片描述
STEP2:使用GDB調試內核跟蹤啓動過程。
在使用gdb跟蹤調試內核之前需要先重新配置編譯Linux使其攜帶調試信息。
由於make menuconfig需要ncurses-dev和tk4-dev庫。
所以我們先輸入命令sudo apt-get install ncurses-dev
和sudo apt-get install tk4-dev
然後輸入make menuconfig進入Kernel Configuration界面
這裏寫圖片描述
選擇Kernel hacking進入
這裏寫圖片描述
選擇Compile-time checks and compilr options —>進入
這裏寫圖片描述
按Y選擇Compile the kernel with debug info
然後執行make重新編譯內核。
編譯完成之後輸入以下的命令,讓CPU凍結在開始的時候。

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選項

然後打開GDB遠程調試,另外開啓一個終端
輸入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之前,也可以在之後

這裏寫圖片描述
在start_kernel設上斷點然後
(gdb)c 繼續執行到達斷點處
輸出(gdb)list顯示出上下文
這裏寫圖片描述
我們繼續設置斷點, break rest_init()
然後輸入c執行到斷點處
然後輸入list顯示出上下文。
這裏寫圖片描述
STEP3:分析start_kernel的代碼
內核幾乎所有模塊的初始化都會經過start_kernel來進行,

asmlinkage __visible void __init start_kernel(void)
{
.......
/*init_task即手工創建的PCB,0號進程就是最終的idle進程*/
set_task_stack_end_magic(&init_task);
........
/*初始化中斷向量*/
trap_init();
/*內存管理模塊初始化*/
mm_init();
/*調度模塊初始化*/
sched_init();
....
/*其他初始化*/
rest_init()
}

我們再來看下最後的rest_init()

/*rest_init()會一直存在,是0號進程,並且創建了1號進程,並創建了一些其他的服務進程*/
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.
     */
   /*初始化了第一個用戶態的進程1號進程*/
    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_idle_loop, cpu_idle_loop是一個while(1)循環,當系統沒有任何需要執行的進程的時候就調度到idle進程*/
    cpu_startup_entry(CPUHP_ONLINE);
}

由此可見,rest_init()最後執行cpu_startup_entry();cpu_startup_entry會調用cpu_idle_loop(), 在cpu_idle_loop()裏面有個while(1)的循環一直執行,作爲idle進程,pid是0號,此進程會一直執行下去,並且在系統沒有任何需要執行的進程時,調度到此進程。
Linux內核的啓動在宏觀上來看,就是start_kernel()來進行各種初始化工作,最終執行到rest_init()來初始化0號進程和1號用戶態的進程。然後操作系統就運行起來了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章