天天看點

linux核心中啟動頁面,Linux核心啟動過程分析

下面給出核心映像完整的啟動過程:

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粘合劑把各種體系結構的代碼“粘合”到一個統一的入口處。

整個核心啟動過程如下圖:

linux核心中啟動頁面,Linux核心啟動過程分析

圖1 Linux核心啟動過程

linux核心中啟動頁面,Linux核心啟動過程分析