下面給出核心映像完整的啟動過程:
arch/x86/boot/header.S:
--->header第一部分(以前的bootsector.S): 載入bootloader到0x7c00處,設定核心屬性
--->_start() bzImage映像的入口點(實模式),header的第二部分(以前的setup.S)
--->code32_start=0x100000 0x100000為解壓後的核心的載入位址(1M高端位址)
--->設定大量的bootloader參數、建立棧空間、檢查簽名、清空BSS
--->arch/x86/boot/main.c:main() 實模式核心的主函數
--->copy_boot_params() 把位于第一個扇區的參數複制到boot_params變量中,boot_params位于setup的資料段
--->檢查記憶體布局、設定鍵盤擊鍵重複頻率、查詢Intel SpeedStep(IST)資訊
--->設定視訊控制器模式、解析指令行參數以便傳遞給decompressor
--->arch/x86/boot/pm.c:go_to_protected_mode() 進入保護模式
--->屏蔽PIC中的所有中斷、設定GDT和IDT
--->arch/x86/boot/pmjump.S:protected_mode_jump(boot_params.hdr.code32_start,...) 跳轉到保護模式
--->in_pm32() 跳轉到32位保護模式的入口處(即0x100000處)
--->jmpl *%eax 跳轉到arch/i386/boot/compressed/head_32.S:startup_32()處執行
arch/i386/boot/compressed/head_32.S:startup_32() 保護模式下的入口函數
--->leal boot_stack_end(%ebx), %esp 設定堆棧
--->拷貝壓縮的核心到緩沖區尾部
--->清空BSS
--->compressed/misc.c:decompress_kernel() 解壓核心
--->lib/decompress_bunzip2.c:decompress()
--->lib/decompress_bunzip2.c:bunzip2()
--->lib/decompress_bunzip2.c:start_bunzip() 解壓動作
--->parse_elf() 将解壓後的核心ELF檔案(.o檔案)解析到記憶體中
--->計算vmlinux編譯時的運作位址與實際裝載位址的距離
--->jmp *%ebp 跳轉到解壓後的核心的arch/x86/kernel/head_32.S:startup_32()處運作
arch/x86/kernel/head_32.S:startup_32() 32位核心的入口函數,即程序0(也稱為清除程序)
--->拷貝boot_params以及boot_command_line
--->初始化頁表:這會建立PDE和頁表集
--->開啟記憶體分頁功能
--->為可選的浮點單元(FPU)檢測CPU類型
--->head32.c:i386_start_kernel()
--->init/main.c:start_kernel() Linux核心的啟動函數,包含建立rootfs,加載核心子產品和cpio-initrd
--->很多初始化操作
--->setup_command_line() 把核心啟動參數複制到boot_command_line數組中
--->parse_early_param() 體系結構代碼會先調用這個函數,做時期的參數檢查
--->parse_early_options()
--->do_early_param() 檢查早期的參數
--->parse_args() 解析子產品的參數
--->fs/dcache.c:vfs_caches_init() 建立基于記憶體的rootfs(一個VFS)
--->fs/namespace.c:mnt_init()
--->fs/ramfs/inode.c:init_rootfs()
--->fs/filesystems.c:register_filesystem() 注冊rootfs
--->fs/namespace.c:init_mount_tree()
--->fs/super.c:do_kern_mount() 在核心中挂載rootfs
--->fs/fs_struct.c:set_fs_root() 将rootfs配置為目前記憶體中的根檔案系統
--->rest_init()
--->arch/x86/kernel/process.c:kernel_thread(kernel_init,...) 啟動一個核心線程來運作kernel_init函數,進行核心初始化
--->cpu_idle() 進入空閑循環
--->排程器周期性的接管控制權,提供多任務處理
init/main.c:kernel_init() 核心初始化過程入口函數,加載initramfs或cpio-initrd,或傳統的image-initrd,把工作交給它
--->sys_open("/dev/console",...) 啟動控制台裝置
--->do_basic_setup()
--->do_initcalls() 啟動所有靜态編譯進核心的子產品
--->init/initramfs.c:populate_rootfs() 初始化rootfs
--->unpack_to_rootfs() 把initramfs或cpio-initrd解壓釋放到rootfs
--->如果是image-initrd則拷貝到/initrd.image
####################################### 傳統的image-initrd情形 ###########################################
--->rootfs中沒有/init檔案
--->do_mounts.c:prepare_namespace() 加載image-initrd,并運作它的/linuxrc檔案,以挂載實際的檔案系統
--->do_mounts_initrd.c:initrd_load() 把image-initrd資料加載到預設裝置/dev/ram0中
--->do_mounts_rd.c:rd_load_image() 加載image-initrd映像
--->identify_ramdisk_image() 識别initrd,确定是romfs、squashfs、minix,還是ext2
--->crd_load() 解壓并為ramdisk配置設定空間,計算循環備援校驗碼
--->lib/inflate.c:gunzip() 對gzip格式的ramdisk進行解壓
--->do_mounts_initrd.c:handle_initrd() 指定的根裝置不是/dev/ram0,由initrd來挂載真正的根檔案系統
--->mount_block_root("/dev/root.old",...) 将initrd挂載到rootfs的/root下
--->arch/x86/kernel/process.c:kernel_thread(do_linuxrc, "/linuxrc",...) 啟動一個核心線程來運作do_linuxrc函數
--->do_mounts_initrd.c:do_linuxrc()
--->arch/x86/kernel/sys_i386_32.c:kernel_execve() 運作image-initrd中的/linuxrc
--->将initrd移動到rootfs的/old下
--->若在linuxrc中根裝置重新設成Root_RAM0,則傳回,說明image-initrd直接作為最終的根檔案系統
--->do_mounts.c:mount_root() 否則将真正的根檔案系統挂載到rootfs的/root下,并切換到這個目錄下
--->mount_block_root()
--->do_mount_root()
--->fs/namespace.c:sys_mount() 挂載到"/root"
--->解除安裝initrd,并釋放它的記憶體
--->do_mounts.c:mount_root() 沒有指定另外的根裝置,則initrd直接作為真正的根檔案系統而被挂載
--->fs/namespace.c:sys_mount(".", "/",...) 根檔案挂載成功,移動到根目錄"/"
########################################################################################################
--->init/main.c:init_post() 啟動使用者空間的init程序
--->run_init_process(ramdisk_execute_command) 若加載了initramfs或cpio-initrd,則運作它的/init
--->run_init_process("/sbin/init") 否則直接運作使用者空間的/sbin/init
--->arch/x86/kernel/sys_i386_32.c:kernel_execve() 運作使用者空間的/sbin/init程式,并配置設定pid為1
--->run_init_process("/bin/sh") 當運作init沒成功時,可用此Shell來代替,以便恢複機器
/init cpio-initrd(或initramfs)中的初始化腳本,挂載真正的根檔案系統,啟動使用者空間的init程序
--->export PATH=/sbin:/bin:/usr/sbin:/usr/bin 設定cpio-initrd的環境變量$PATH
--->挂載procfs、sysfs
--->解析指令行參數
--->udevd --daemon --resolve-names=never 啟動udev
--->/initqueue/*.sh 執行/initqueue下的腳本完成對應初始化工作(現在該目錄下為空)
--->/initqueue-settled/*.sh 執行/initqueue-settled下的腳本(現在該目錄下為空)
--->/mount/*.sh 挂載真正的根檔案系統
--->/mount/99mount-root.sh 根據/etc/fstab中的選項挂載根檔案系統
--->/lib/dracut-lib.sh 一系列通用函數
--->把根檔案系統挂載到$NEWROOT下
--->尋找真正的根檔案系統中的init程式并存放在$INIT中 /sbin/init, /etc/init, /bin/init, 或/bin/sh
--->從/proc/cmdline中擷取啟動init的參數并存放在$initargs中
--->switch_root "$NEWROOT" "$INIT" $initargs 切換到根分區,并啟動其中的init程序
注意kernel_evecve調用的是與具體體系平台相關的實作,但它是一個通用的系統調用,在linux/syscalls.h中聲明,這個頭檔案中聲明了與體系結構無關的所有系統調用接口。隻不過kernel_evecve在實作時是與體系結構相關的,每種體系結構都要提供它的實作。
從以上分析可以看出,如果使用新的cpio-initrd(或initramfs),kernel_init隻負責核心初始化(包括加載核心子產品、建立基于記憶體的rootfs以及加載cpio-initrd)。後續根檔案系統的挂載、init程序的啟動工作都交給cpio-initrd來完成。cpio-initrd相對于image-initrd承擔了更多的初始化責任,這種變化也可以看作是核心代碼的使用者層化的一種展現,實際上精簡核心代碼,将部分功能移植到使用者層必然是linux核心發展的一個趨勢。如果是使用傳統的image-initrd的話,根檔案系統的挂載也會放在kernel_init()中,其中prepare_namespace完成挂載根檔案系統,init_post()完成運作/sbin/init,顯然這樣核心的代碼不夠精簡。
5、init程序
init是第一個調用的使用标準C庫編譯的程式。在此之前,還沒有執行任何标準的C應用程式。在桌面Linux系統上,第一個啟動的程式通常是/sbin/init,它的程序号為1。init程序是所有程序的發起者和控制者,它有兩個作用:
(1)扮演終結父程序的角色:所有的孤兒程序都會被init程序接管。
(2)系統初始化工作:如設定鍵盤、字型,裝載子產品,設定網絡等。
在完成系統初始化工作之後,init程序将在控制台上運作getty(登入程式)等任務,我們熟悉的登入界面就出現了!
init程式的運作流程需要分專門的一節來讨論,因為它有不同的實作方式。傳統的實作是基于UNIX System V init程序的,程式包為sysvinit(以前的RedHat/Fedora用的就是這個)。目前已經有多種sysvinit的替代産品了,這其中包括initng,它已經可以用于Debian了,并且在Ubuntu上也能工作。在同一位置上,Solaris使用SMF(Service Management Facility),而Mac OS則使用 launchd。現在廣泛使用的是upstart init初始化程序,目前在Ubuntu和Fedora,還有其他系統中已經取代了sysvinit。
傳統的Sysvinit daemon是一個基于運作級别的初始化程式,它使用了運作級别(如單使用者、多使用者等)并通過從/etc/rcX.d目錄到/etc/init.d目錄的初始化腳本的連結來啟動與終止系統服務。Sysvinit無法很好地處理現代硬體,如熱插拔裝置、USB硬碟、網絡檔案系統等。upstart系統則是事件驅動的,事件可能被硬體改動觸發,也可被啟動或關機或任務所觸發,或者也可能被系統上的任何其他程序所觸發。事件用于觸發任務或服務,統稱為作業。比如連接配接到一個USB驅動器可能導緻udev服務發送一個block-device-added事件,這可能引起一個預定任務檢查/etc/fstab和挂載驅動器(如果需要的話)。再如,一個Apache web伺服器可能隻有當網絡和所需的檔案系統都可用時才能啟動。
Upstart作業在/etc/init目錄及其子目錄下被定義。upstart系統相容sysvinit,它也會處理/etc/inittab和System V init腳本(如果有的話)。在諸如近來的Fedora版本的系統上,/etc/inittab可能隻含有initdefault操作的id項。目前Ubuntu系統預設沒有/etc/inittab,如果您想要指定一個預設運作級别的話,您可以建立一個。Upstart也使用initctl指令來支援與upstart init守護程序的互動。這時您可以啟動或終止作業、清單作業、以及擷取作業的狀态、發出事件、重新開機init程序,等等。
總的來說,x86架構的Linux核心啟動過程分為6大步,分别為:
(1)實模式的入口函數_start():在header.S中,這裡會進入衆所周知的main函數,它拷貝bootloader的各個參數,執行基本硬體設定,解析指令行參數。
(2)保護模式的入口函數startup_32():在compressed/header_32.S中,這裡會解壓bzImage核心映像,加載vmlinux核心檔案。
(3)核心入口函數startup_32():在kernel/header_32.S中,這就是所謂的程序0,它會進入體系結構無關的start_kernel()函數,即衆所周知的Linux核心啟動函數。start_kernel()會做大量的核心初始化操作,解析核心啟動的指令行參數,并啟動一個核心線程來完成核心子產品初始化的過程,然後進入空閑循環。
(4)核心子產品初始化的入口函數kernel_init():在init/main.c中,這裡會啟動核心子產品、建立基于記憶體的rootfs、加載initramfs檔案或cpio-initrd,并啟動一個核心線程來運作其中的/init腳本,完成真正根檔案系統的挂載。
(5)根檔案系統挂載腳本/init:這裡會挂載根檔案系統、運作/sbin/init,進而啟動衆所周知的程序1。
(6)init程序的系統初始化過程:執行相關腳本,以完成系統初始化,如設定鍵盤、字型,裝載子產品,設定網絡等,最後運作登入程式,出現登入界面。
如果從體系結構無關的視角來看,start_kernel()可以看作時體系結構無關的Linux main函數,它是體系結構無關的代碼的統一入口函數,這也是為什麼檔案會命名為init/main.c的原因。這個main.c粘合劑把各種體系結構的代碼“粘合”到一個統一的入口處。
整個核心啟動過程如下圖:
圖1 Linux核心啟動過程