1. 程式人生 > >Linux核心啟動過程分析

Linux核心啟動過程分析

    1、Linux核心啟動協議
    閱讀文件\linux-2.6.35\Documentation\x86\boot.txt
    傳統支援Image和zImage核心的啟動裝載記憶體佈局(2.4以前的核心裝載就是這樣的佈局):
    |             |
0A0000    +------------------------+
    |  Reserved for BIOS     |    Do not use.  Reserved for BIOS EBDA.
09A000    +------------------------+
    |  Command line         |
    |  Stack/heap         |    For use by the kernel real-mode code.
098000    +------------------------+    
    |  Kernel setup         |    The kernel real-mode code.
090200    +------------------------+
    |  Kernel boot sector     |    The kernel legacy boot sector.
090000    +------------------------+
    |  Protected-mode kernel |    The bulk of the kernel image.
010000    +------------------------+
    |  Boot loader         |    <- Boot sector entry point 0000:7C00
001000    +------------------------+
    |  Reserved for MBR/BIOS |
000800    +------------------------+
    |  Typically used by MBR |
000600    +------------------------+
    |  BIOS use only     |
000000    +------------------------+
    當使用bzImage時,保護模式的核心會被重定位到0x1000000(高階記憶體),核心真實模式的程式碼(boot sector,setup和stack/heap)會被編譯成可重定位到0x100000與低端記憶體底端之間的任何地址處。不幸的是,在2.00和2.01版的引導協議中,0x90000+的記憶體區域仍然被使用在核心的內部。2.02版的引導協議解決了這個問題。boot loader應該使BIOS的12h中斷呼叫來檢查低端記憶體中還有多少記憶體可用。
    人們都希望“記憶體上限”,即boot loader觸及的低端記憶體最高處的指標,儘可能地低,因為一些新的BIOS開始分配一些相當大的記憶體,所謂的擴充套件BIOS資料域,幾乎快接近低端記憶體的最高處了。
    不幸的是,如果BIOS 12h中斷報告說記憶體的數量太小了,則boot loader除了報告一個錯誤給使用者外,什麼也不會做。因此,boot loader應該被設計成佔用儘可能少的低端記憶體。對zImage和以前的bzImage,這要求資料能被寫到x090000段,boot loader應該確保不會使用0x9A000指標以上的記憶體;很多BIOS在這個指標以上會終止。
    對一個引導協議>=2.02的現代bzImage核心,其記憶體佈局使用以下格式:
        |  Protected-mode kernel |
100000  +------------------------+
    |  I/O memory hole     |
0A0000    +------------------------+
    |  Reserved for BIOS     |    Leave as much as possible unused
    ~                        ~
    |  Command line         |    (Can also be below the X+10000 mark)
X+10000    +------------------------+
    |  Stack/heap         |    For use by the kernel real-mode code.
X+08000    +------------------------+    
    |  Kernel setup         |    The kernel real-mode code.
    |  Kernel boot sector     |    The kernel legacy boot sector.
X       +------------------------+
    |  Boot loader         |    <- Boot sector entry point 0000:7C00
001000    +------------------------+
    |  Reserved for MBR/BIOS |
000800    +------------------------+
    |  Typically used by MBR |
000600    +------------------------+
    |  BIOS use only     |
000000    +------------------------+
    這裡程式段地址是由grub的大小來決定的。地址X應該在bootloader所允許的範圍內儘可能地低。
    2、BIOS POST過程

    傳統意義上,由於CPU加電之後,CPU只能訪問ROM或者RAM裡的資料,而這個時候是沒有計算機作業系統的,所以需要有一段程式能夠完成載入儲存在非易失性儲存介質(比如硬碟)上的作業系統到RAM中的功能。這段程式儲存在ROM裡,BIOS就是這類程式中的一種。對於BIOS,主要由兩家制造商製造,駐留在主機板的ROM裡。有了BIOS,硬體製造商可以只需要關注硬體而不需要關注軟體。BIOS的服務程式,是通過呼叫中斷服務程式來實現的。BIOS載入bootloader程式,Bootloader也可以通過BIOS提供的中斷,向BIOS獲取系統的資訊。整個過程如下:
    (1)電源啟動時鐘發生器並在總線上產生一個#POWERGOOD的中斷。
    (2)產生CPU的RESET中斷(此時CPU處於8086工作模式)。
    (3)進入BIOS POST程式碼處:%ds=%es=%fs=%gs=%ss=0,%cs=0xFFFF0000,%eip = 0x0000FFF0 (ROM BIOS POST code,指令指標eip,資料段暫存器ds,程式碼段暫存器cs)。
    (4)在中斷無效狀態下執行所有POST檢查。
    (5)在地址0初始化中斷向量表IVT。
    (6)0x19中斷:以啟動裝置號為引數呼叫BIOS啟動裝載程式。這個程式從啟動裝置(硬碟)的0扇面1扇區讀取資料到記憶體實體地址0x7C00處開始裝載。這個0扇面1扇區稱為Boot sector(引導扇區),共512位元組,也稱為MBR。
    就是說,CPU 在  BIOS 的入口(CS:IP=FFFF:0000)處執行BIOS的彙編程式,BIOS程式功能有系統硬體的檢測,提供中斷訪問介面以訪問硬體。而後被BIOS程式通過中斷0x19呼叫磁碟MBR上的bootloader程式,將bootloader程式載入到ox7c00處,而後跳轉到0x7c00,這樣,位於 0x7c00處的bootloader程式,就可以執行了。
    從BIOS執行MBR中的bootloader程式開始,就是linux的程式碼在做的事情了。
    3、Bootloader過程

    bootloader程式是為計算機載入(load)計算機作業系統的。boot(引導)是bootstrap的簡寫,bootstrap是引導指令的意思。bootloader程式通常位於硬碟上,被BIOS呼叫,用於載入核心。在PC機上常見的bootloader主要有grub、lilo、syslinux等。
    GRUB(GRand Unified Bootloader)是當前linux諸多發行版本預設的載入程式。嵌入式系統上,最常見的bootloader是U-BOOT。這樣的bootloader一般位於MBR的最前部。在linux系統中,bootloader也可以寫入檔案系統所在分割槽中。比如,grub程式就非常強大。Gurb執行後,將初始化設定核心執行所需的環境。然後載入核心映象。
    grub磁碟引導全過程:
    (1)stage1: grub讀取磁碟第一個512位元組(硬碟的0道0面1扇區,被稱為MBR(主引導記錄),也稱為bootsect)。MBR由一部分bootloader的引導程式碼、分割槽表和魔數三部分組成。
    (2)stage1_5: 識別各種不同的檔案系統格式。這使得grub識別到檔案系統。
    (3)stage2: 載入系統引導選單(/boot/grub/menu.lst或grub.lst),載入核心vmlinuz和RAM磁碟initrd。
    4、核心啟動過程
    
    核心映像檔案vmlinuz:包含有linux核心的靜態連結的可執行檔案,傳統上,vmlinux被稱為可引導的核心映象。vmlinuz是vmlinux的壓縮檔案。其構成如下:
    (1)第一個512位元組(以前是在arch/i386/boot/bootsect.S);
    (2)第二個,一段程式碼,若干個不多於512位元組的段(以前是在arch/i386/boot/setup.S);
    (3)保護模式下的核心程式碼(在arch/x86/boot/main.c)。
    bzImage檔案:使用make bzImage命令編譯核心原始碼,可以得到採用zlib演算法壓縮的zImage檔案,即big zImage檔案。老的zImage解壓縮核心到低端記憶體,bzImage則解壓縮核心到高階記憶體(1M(0x100000)以上),在保護模式下執行。bzImage檔案一般包含有vmlinuz、bootsect.o、setup.o、解壓縮程式misc.o、以及其他一些相關檔案(如piggy.o)。注意,在Linux 2.6核心中,bootsect.S和setup.S被整合為header.S。
    initramfs(或initrd)檔案:initrd是initialized ram disk的意思。主要用於載入硬體驅動模組,輔助核心的啟動,掛載真正的根檔案系統。

    例如,我電腦上的grub啟動項如下(在/boot/grub/grub.lst中):

title Fedora (2.6.35.10-74.fc14.i686)
	root (hd0,0)
	kernel /vmlinuz-2.6.35.10-74.fc14.i686 ro root=/dev/mapper/VolGroup-lv_root rd_LVM_LV=VolGroup/lv_root rd_LVM_LV=VolGroup/lv_swap rd_NO_LUKS rd_NO_MD rd_NO_DM LANG=zh_CN.UTF-8 KEYBOARDTYPE=pc KEYTABLE=us rhgb quiet
	initrd /initramfs-2.6.35.10-74.fc14.i686.img
    核心的執行引數可以控制核心的行為,比如ro引數告訴核心,以只讀方式掛載根分割槽,而quiet則告訴核心,啟動的時候不要列印任何資訊。這些引數不光影響核心的執行,大多數的發行版也使用這些引數控制啟動完畢以後後續的動作。這些引數可以在任何時候從/proc/cmdline 這個檔案中獲得。現在,grub找到了核心(hd0,0)/boot/vmlinuz-2.6.35.10-74.fc14.i686,它將整個電腦的控制權交給了這個程式,核心開始進行各種初始化的動作,你可以將quiet引數去掉,以便看看核心都做了哪些事情,也可以在系統啟動成功以後,使用dmesg這個命令檢視核心啟動的時候,都列印了哪些東西。
    啟動過程是和體系結構相關的,對於2.6核心,x86體系結構,CPU在上電初始化時,指令暫存器CS:EIP總是被初始化為固定值,這就是CPU復位後的第一條指令的地址。對於32位地址匯流排的系統來說,4GB的物理空間至少被劃分為兩個部分,一部分是記憶體的地址空間,另外一部分地址空間用於對BIOS晶片儲存單元進行定址。x86復位後工作在真實模式下,該模式下CPU的定址空間為1MB。CS:IP的復位值是FFFF:0000,實體地址為FFFF0。主機板設計者必須保證把這個實體地址對映到BIOS晶片上,而不是RAM上。
    裝載Linux核心的第一步應該是載入真實模式程式碼(boot sector和setup程式碼),然後檢查偏移0x01f1處的頭部(header)中的各個引數值。真實模式的程式碼總共有32K,但是boot loader可以選擇只裝載前面的兩個扇區(1K),然後檢查bootup扇區的大小。
    header中各個域的格式如下:
Offset/Size		Proto		Name					Meaning

01F1/1			ALL(1		setup_sects			The size of the setup in sectors
01F2/2			ALL		root_flags	 		If set, the root is mounted readonly
01F4/4			2.04+		syssize				The size of the 32-bit code in 16-byte paras
01F8/2			ALL		ram_size			DO NOT USE - for bootsect.S use only
01FA/2			ALL		vid_mode			Video mode control
01FC/2			ALL		root_dev			Default root device number
01FE/2			ALL		boot_flag			0xAA55 magic number
0200/2			2.00+		jump				Jump instruction
0202/4			2.00+		header				Magic signature "HdrS"
0206/2			2.00+		version				Boot protocol version supported
0208/4			2.00+		realmode_swtch		Boot loader hook (see below)
020C/2			2.00+		start_sys_seg		The load-low segment (0x1000) (obsolete)
020E/2			2.00+		kernel_version		Pointer to kernel version string
0210/1			2.00+		type_of_loader		Boot loader identifier
0211/1			2.00+		loadflags			Boot protocol option flags
0212/2			2.00+		setup_move_size		Move to high memory size (used with hooks)
0214/4			2.00+		code32_start		Boot loader hook (see below)
0218/4			2.00+		ramdisk_image		initrd load address (set by boot loader)
021C/4			2.00+		ramdisk_size		initrd size (set by boot loader)
0220/4			2.00+		bootsect_kludge		DO NOT USE - for bootsect.S use only
0224/2			2.01+		heap_end_ptr		Free memory after setup end
0226/1			2.02+	 	ext_loader_ver		Extended boot loader version
0227/1			2.02+		ext_loader_type		Extended boot loader ID
0228/4			2.02+		cmd_line_ptr		32-bit pointer to the kernel command line
022C/4			2.03+		ramdisk_max			Highest legal initrd address
0230/4			2.05+		kernel_alignment 		Physical addr alignment required for kernel
0234/1			2.05+		relocatable_kernel 		Whether kernel is relocatable or not
0235/1			2.10+		min_alignment			Minimum alignment, as a power of two
0236/2			N/A		pad3			        Unused
0238/4			2.06+		cmdline_size			Maximum size of the kernel command line
023C/4			2.07+		hardware_subarch 		Hardware subarchitecture
0240/8			2.07+		hardware_subarch_data 		Subarchitecture-specific data
0248/4			2.08+		payload_offset			Offset of kernel payload
024C/4			2.08+		payload_length			Length of kernel payload
0250/8			2.09+		setup_data			64-bit physical pointer to linked list of struct setup_data
0258/8			2.10+		pref_address			Preferred loading address
0260/4			2.10+		init_size			Linear memory required during initialization
    每個域的具體細節可參考boot.txt文件。
    BIOS把Boot Loader載入到0x7C00的地方並跳轉到這裡繼續執行之後,BootLoader就會把真實模式程式碼setup載入到0x07C00之上的某個地址上,其中setup的前512個位元組是boot sector(引導扇區),現在這個引導扇區的作用並不是用來引導系統,而是為了相容及傳遞一些引數。之後Boot Loader跳轉到setup的入口點,入口點為_start例程(根據arch/x86/boot/setup.ld可知)。
    注意,bzImage由setup和vmlinux兩部分組成,setup是真實模式下的程式碼,vmlinux是保護模式下的程式碼。
    真實模式設定(setup)階段用於體系結構相關的硬體初始化工作,涉及的檔案有arch/x86/boot/header.S、連結指令碼setup.ld、arch/x86/boot/main.c。header.S第一部分定義了bstext、.bsdata、.header這3個節,共同構成了vmlinuz的第一個512位元組(即引導扇區的內容)。常量BOOTSEG和SYSSEG定義了引導扇區和核心的載入地址。下面是header.S的程式碼:
BOOTSEG		= 0x07C0		/* 引導扇區的原始地址 */
SYSSEG		= 0x1000		/* 歷史的載入地址>>4 */

#ifndef SVGA_MODE
#define SVGA_MODE ASK_VGA
#endif

#ifndef RAMDISK
#define RAMDISK 0
#endif

#ifndef ROOT_RDONLY
#define ROOT_RDONLY 1
#endif

	.code16
	.section ".bstext", "ax"

	.global bootsect_start
bootsect_start:

	# 使開始地址正常化
	ljmp	$BOOTSEG, $start2

start2:
	movw	%cs, %ax
	movw	%ax, %ds
	movw	%ax, %es
	movw	%ax, %ss
	xorw	%sp, %sp
	sti
	cld

	movw	$bugger_off_msg, %si

msg_loop:
	lodsb
	andb	%al, %al
	jz	bs_die
	movb	$0xe, %ah
	movw	$7, %bx
	int	$0x10
	jmp	msg_loop

bs_die:
	# 允許使用者按一個鍵,然後重啟
	xorw	%ax, %ax
	int	$0x16
	int	$0x19

	# 0x19中斷絕不會返回,無論它做什麼
	# 呼叫BIOS復位程式碼,便CPU工作在真實模式下
	ljmp	$0xf000,$0xfff0

	.section ".bsdata", "a"
bugger_off_msg:
	.ascii	"Direct booting from floppy is no longer supported.\r\n"
	.ascii	"Please use a boot loader program instead.\r\n"
	.ascii	"\n"
	.ascii	"Remove disk and press any key to reboot . . .\r\n"
	.byte	0


	# 下面設定核心的一些屬性,setup需要。這是header的第一部分,來自以前的boot sector

	.section ".header", "a"
	.globl	hdr
hdr:
setup_sects:	.byte 0			/* 被build.c填充 */
root_flags:	.word ROOT_RDONLY
syssize:	.long 0			/* 被build.c填充 */
ram_size:	.word 0			/* 已過時 */
vid_mode:	.word SVGA_MODE
root_dev:	.word 0			/* 被build.c填充 */
boot_flag:	.word 0xAA55

	# 偏移512處,setup的入口點

	.globl	_start
_start:
		# Explicitly enter this as bytes, or the assembler
		# tries to generate a 3-byte jump here, which causes
		# everything else to push off to the wrong offset.
		.byte	0xeb		# short (2-byte) jump
		.byte	start_of_setup-1f
1:

	# header的第二部分,來自以前的setup.S:設定頭部header,包括大量的bootloader引數,如header版本、核心版本字串指標、bootloader型別、
	# 核心裝載時的很多標誌、堆疊尾部地址指標、核心命令列地址指標和大小、32位保護模式入口地址、ramdisk地址和大小等
	
code32_start:				# 這裡對32位的程式碼,裝載器可以設定可設定一個不同的入口地址
		.long	0x100000	# 0x100000 = 為大核心的預設入口地址(保護模式)
	
	# ............ (省略)

# End of setup header #####################################################

	.section ".entrytext", "ax"
start_of_setup:
#ifdef SAFE_RESET_DISK_CONTROLLER
# 重置磁碟控制器
	movw	$0x0000, %ax		# 重置磁碟控制器
	movb	$0x80, %dl		# 所有的的磁碟控制器All disks
	int	$0x13
#endif

# ............(省略)

	# 讓%ss無效,建立一個新的棧
	movw	$_end, %dx
	testb	$CAN_USE_HEAP, loadflags
	jz	1f
	movw	heap_end_ptr, %dx
1:	addw	$STACK_SIZE, %dx
	jnc	2f
	xorw	%dx, %dx	# Prevent wraparound

2:	# 現在%dx應該指向我們棧空間的尾部
	andw	$~3, %dx	# dword對齊
	jnz	3f
	movw	$0xfffc, %dx	# 確保不是0
3:	movw	%ax, %ss
	movzwl	%dx, %esp	# 清除%esp的上半部分
	sti			# 現在我們應該有一個工作空間

# 我們將進入%cs=%ds+0x20,設定好%cs
	pushw	%ds
	pushw	$6f
	lretw
6:

# 在setup終止時檢查簽名
	cmpl	$0x5a5aaa55, setup_sig
	jne	setup_bad

# 對BSS(Block Started by Symbol)清零
	movw	$__bss_start, %di
	movw	$_end+3, %cx
	xorl	%eax, %eax
	subw	%di, %cx
	shrw	$2, %cx
	rep; stosl

# 跳轉到C程式碼(不會返回)
	calll	main

# ............(省略)
    由setup.ld中的ENTRY(_start)可知,_start彙編例程是bzImage核心映像開始執行的入口點,即引導扇區之後的開始處(偏移512位元組處),它會準備大量的bootloader引數。最後的call main跳轉到arch/x86/boot/main.c:main()函式處執行,這就是眾所周知的main函式,它們都工作在真實模式下。main函式先呼叫copy_boot_params函式把位於第一個扇區的引數複製到boot_params變數中,boot_params位於setup的資料段,然後呼叫鏈為arch/x86/boot/pm.c:go_to_protected_mode(void) --->arch/x86/boot/pmjump.S:protected_mode_jump()。
    真實模式的protected_mode_jump執行後,跳出了bzImage的第一部分,BootLoader預設把第二部分放在0x100000處,這個入口處是startup_32,先執行arch/x86/boot/compressed/head_32.S中的startup_32(保護模式下的入口函式),然後執行arch/x86/kernel/head_32.S中的startup_32(32位核心的入口函式),這裡會拷貝boot_params以及boot_command_line, 初始化頁表,開啟分頁機制。
    startup_32()函式會呼叫head32.c:i386_start_kernel()函式,它會呼叫init/main.c:start_kernel()函式,這是Linux核心的啟動函式。init/main.c檔案是整個Linux核心的中央聯結點。每種體系結構都會執行一些底層設定函式,然後執行名為start_kernel的函式(在init/main.c中可以找到這個函式)。可以認為main.c是核心的“粘合劑(glue)”,之前執行的程式碼都是各種體系結構相關的程式碼,一旦到達start_kernel(),就與體系結構無關了。
    start_kernel()會呼叫一系列初始化函式來設定中斷,執行進一步的記憶體配置,解析核心命令列引數。然後呼叫fs/dcache.c:vfs_caches_init()--->fs/namespace.c:mnt_init()建立基於記憶體的rootfs檔案系統(是一個虛擬的記憶體檔案系統,稱為VFS),這是系統初始化時的根結點,即"/"結點,後面VFS會指向真實的檔案系統。注意在Linux系統中,目錄結構與Windows上有較大的不同。系統中只有一個根目錄,路徑是“/”,而其它的分割槽只是掛載在根目錄中的一個資料夾內,如“/proc”和“/sys”等,這裡的“/”就是Linux中的根目錄。
    下面是mnt_init()的程式碼:
void __init mnt_init(void)
{
	unsigned u;
	int err;

	init_rwsem(&namespace_sem);

	mnt_cache = kmem_cache_create("mnt_cache", sizeof(struct vfsmount),
			0, SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL);

	mount_hashtable = (struct list_head *)__get_free_page(GFP_ATOMIC);

	if (!mount_hashtable)
		panic("Failed to allocate mount hash table\n");

	printk("Mount-cache hash table entries: %lu\n", HASH_SIZE);

	for (u = 0; u < HASH_SIZE; u++)
		INIT_LIST_HEAD(&mount_hashtable[u]);

	err = sysfs_init();
	if (err)
		printk(KERN_WARNING "%s: sysfs_init error: %d\n",
			__func__, err);
	fs_kobj = kobject_create_and_add("fs", NULL);
	if (!fs_kobj)
		printk(KERN_WARNING "%s: kobj create error\n", __func__);
	init_rootfs();
	init_mount_tree();
}
    這裡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檔案系統配置為根檔案系統。
    為什麼不直接把真實的檔案系統配置為根檔案系統?答案很簡單,核心中沒有真實根檔案系統裝置(如硬碟,USB)的驅動,而且即便你將根檔案系統的裝置驅動編譯到核心中,此時它們還尚未載入,實際上所有核心中的驅動是由後面的kernel_init執行緒進行載入。另外,我們的root裝置都是以裝置檔案的方式指定的,如果沒有根檔案系統,裝置檔案怎麼可能存在呢?
    注意根據呼叫鏈do_kern_mount()--->vfs_kern_mount(type)--->type->get_sb()--->fs/ramfs/inode.c:rootfs_get_sb()--->ramfs_fill_super()--->fs/dcache.c:d_alloc_root(),函式d_alloc_root分配最終的根結點,程式碼如下:
struct dentry * d_alloc_root(struct inode * root_inode)
{
	struct dentry *res = NULL;

	if (root_inode) {
		static const struct qstr name = { .name = "/", .len = 1 };

		res = d_alloc(NULL, &name);
		if (res) {
			res->d_sb = root_inode->i_sb;
			res->d_parent = res;
			d_instantiate(res, root_inode);
		}
	}
	return res;
}
    從上面的程式碼中的可以看出,這個rootfs的dentry物件的名字為"/",這就是我們看到的根目錄"/"。
    start_kernel()在最後會呼叫rest_init(),這個函式會啟動一個核心執行緒來執行kernel_init(),自己則呼叫cpu_idle()進入空閒迴圈,讓排程器接管控制權。搶佔式的排程器就可以週期性地接管控制權,從而提供多工處理能力。
    kernel_init()用於完成初始化rootfs、載入核心模組、掛載真正的根檔案系統。根據Documentation/early-userspace/README的描述,目前2.6的kernel支援三方式來掛載最終的根檔案系統:    
    (1)所有需要的裝置和檔案系統驅動被編譯進核心,沒有initrd。通過“root="引數指定的根裝置,init/main.c:kernel_init()將呼叫prepare_namespace()直接在指定的根裝置上掛載最終的根檔案系統。通過可選的"init="選項,還可以執行使用者指定的init程式。
    (2)一些裝置和檔案驅動作為模組來構建並存放的initrd中。initrd被稱為ramdisk,是一個獨立的小型檔案系統。它需要包含/linuxrc程式(或指令碼),用於載入這些驅動模組,並掛載最終的根檔案系統(結合使用pivot_root系統呼叫),然後initrd被解除安裝。initrd由prepare_namespace()掛載和執行。核心必須要使用CONFIG_BLK_DEV_RAM(支援ramdisk)和CONFIG_BLK_DEV_INITRD(支援initrd)選項進行編譯才能支援initrd。
    initrd檔案通過在grub引導時用initrd命令指定。它有兩種格式,一種是類似於linux2.4核心使用的傳統格式的檔案系統映象,稱之為image-initrd,它的製作方法同Linux2.4核心的initrd一樣,其核心檔案就是 /linuxrc。另外一種格式的initrd是cpio格式的,這種格式的initrd從linux 2.5起開始引入,使用cpio工具生成,其核心檔案不再是/linuxrc,而是/init,這種 initrd稱為cpio-initrd。為了向後相容,linux2.6核心對cpio-initrd和image-initrd這兩種格式的initrd 均支援,但對其處理流程有著顯著的區別。cpio-initrd的處理與initramfs類似,會直接跳過prepare_namespace(),image-initrd的處理則由prepare_namespace()進行。
    (3)使用initramfs。prepare_namespace()呼叫會被跳過。這意味著必須有一個程式來完成這些工作。這個程式是通過修改usr/gen_init_cpio.c的方式,或通過新的initrd格式(一個cpio歸檔檔案)存放在initramfs中的,它必須是"/init"。這個程式負責prepare_namespace()所做的所有工作。為了保持向後相容,在現在的核心中,/init程式只有是來自cpio歸檔的情況才會被執行。如果不是來自cpio歸檔,init/main.c:kernel_init()將執行prepare_namespace()來掛載最終的根檔案系統,並執行一個預先定義的init程式(或者是使用者通過init=指定的,或者是/sbin/init,/etc/init,/bin/init)。
    initramfs是從2.5 kernel開始引入的一種新的實現機制。顧名思義,initramfs只是一種RAM filesystem而不是disk。initramfs實際是一個包含在核心映像內部的cpio歸檔,啟動所需的使用者程式和驅動模組被歸檔成一個檔案。因此,不需要cache,也不需要檔案系統。 編譯2.6版本的linux核心時,編譯系統總會建立initramfs,然後通過連線指令碼arch\x86\kernel\vmlinux.lds.S把它與編譯好的核心連線成一個檔案,它被連結到地址__initramfs_start~__initramfs_end處。核心原始碼樹中的usr目錄就是專門用於構建核心中的initramfs的。預設情況下,initramfs是空的,X86架構下的檔案大小是134個位元組。實際上它的含義就是:在核心映象中附加一個cpio包,這個cpio包中包含了一個小型的檔案系統,當核心啟動時,核心將這個cpio包解開,並且將其中包含的檔案系統釋放到rootfs中,核心中的一部分初始化程式碼會放到這個檔案系統中,作為使用者層程序來執行。這樣帶來的明顯的好處是精簡了核心的初始化程式碼,而且使得核心的初始化過程更容易定製。
    注意initramfs和initrd都可以是cpio包,可以壓縮也可以不壓縮。但initramfs是包含在核心映像中的,作為核心的一部分存在,因此它不會由bootloader(如grub)單獨地載入,而initrd是另外單獨編譯生成的,是一個獨立的檔案,會由bootloader單獨載入到RAM中核心空間以外的地址處。目前initramfs只支援cpio包格式,它會被populate_rootfs--->unpack_to_rootfs(&__initramfs_start, &__initramfs_end - &__initramfs_start, 0)函式解壓、解析並拷貝到根目錄。initramfs被解析處理後原始的cpio包(壓縮或非壓縮)所佔的空間(&__initramfs_start - &__initramfs_end)是作為系統的一部分直接保留在系統中,不會被釋放掉。而對於initrd映象檔案,如果沒有在命令列中設定"keepinitd"命令,那麼initrd映象檔案被處理後其原始檔案所佔的空間(initrd_end - initrd_start)將被釋放掉。    
    下面看kernel_init的程式碼:
static int __init kernel_init(void * unused)
{
	/* ......(省略) */

	do_basic_setup();

	/* Open the /dev/console on the rootfs, this should never fail */
	if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
		printk(KERN_WARNING "Warning: unable to open an initial console.\n");

	(void) sys_dup(0);
	(void) sys_dup(0);
	/*
	 * check if there is an early userspace init.  If yes, let it do all
	 * the work
	 */

	if (!ramdisk_execute_command)
		ramdisk_execute_command = "/init";

	if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
		ramdisk_execute_command = NULL;
		prepare_namespace();
	}

	/*
	 * Ok, we have completed the initial bootup, and
	 * we're essentially up and running. Get rid of the
	 * initmem segments and start the user-mode stuff..
	 */

	init_post();
	return 0;
}
    kernel_init會先呼叫do_basic_setup,這是一個很關鍵的函式。在此之前CPU子系統執行起來了,記憶體管理和程序管理也啟動了,到do_basic_setup才開始做真正實際的工作。所有直接編譯在kernel中的模組都是由它啟動的。程式碼如下:

static void __init do_basic_setup(void)
{
    init_workqueues();
    cpuset_init_smp();
    usermodehelper_init();
    init_tmpfs();
    driver_init();
    init_irq_proc();
    do_ctors();
    do_initcalls();
}
    do_initcalls()用來啟動所有在__initcall_start和__initcall_end段之間的函式,而靜態編譯進核心的模組會將其初始化函式放置在這段區間裡。其中與rootfs相關的初始化函式都會由rootfs_initcall()所引用。在init/initramfs.c中就有rootfs_initcall(populate_rootfs)的引用,這是用來初始化rootfs的,因此do_initcall()最終會呼叫到populate_rootfs()。需要特別指出的是initramfs.c模組的入口函式populate_rootfs()是否執行取決於Kernel的編譯選項,參考init/Makefile,核心編譯時必須配置CONFIG_BLK_DEV_INITRD選項才會執行這個函式。程式碼如下:

static int __init populate_rootfs(void)
{
	char *err = unpack_to_rootfs(__initramfs_start,
			 __initramfs_end - __initramfs_start);
	if (err)
		panic(err);	/* Failed to decompress INTERNAL initramfs */
	if (initrd_start) {
#ifdef CONFIG_BLK_DEV_RAM
		int fd;
		printk(KERN_INFO "Trying to unpack rootfs image as initramfs...\n");
		err = unpack_to_rootfs((char *)initrd_start,
			initrd_end - initrd_start);
		if (!err) {
			free_initrd();
			return 0;
		} else {
			clean_rootfs();
			unpack_to_rootfs(__initramfs_start,
				 __initramfs_end - __initramfs_start);
		}
		printk(KERN_INFO "rootfs image is not initramfs (%s)"
				"; looks like an initrd\n", err);
		fd = sys_open("/initrd.image", O_WRONLY|O_CREAT, 0700);
		if (fd >= 0) {
			sys_write(fd, (char *)initrd_start,
					initrd_end - initrd_start);
			sys_close(fd);
			free_initrd();
		}
#else
		printk(KERN_INFO "Unpacking initramfs...\n");
		err = unpack_to_rootfs((char *)initrd_start,
			initrd_end - initrd_start);
		if (err)
			printk(KERN_EMERG "Initramfs unpacking failed: %s\n", err);
		free_initrd();
#endif
	}
	return 0;
}
    (1)第一行的upack_to_rootfs()用來把核心映像中的initramfs釋放到rootfs。它實際上有兩個功能,一個是檢測是否是屬於cpio包,另外一個就是解壓並釋放cpio包。注意如果__initramfs_start和__initramfs_end的值相等,則initramfs長度為零,unpack_to_rootfs()不會做任何處理,直接返回。
    (2)if(initrd_start)判斷是否載入了initrd。無論哪種格式的initrd,都會被boot loader載入到地址initrd_start處。當然,如果是initramfs的情況下,該值肯定為空了。
    (3)第二個unpack_to_rootfs()把cpio-initrd映象釋放到rootfs,以此作為initramfs。這其中有/init指令碼程式。
    (4)如果不是cpio-initrd,則認為是一個image-initrd,將其內容儲存到/initrd.image中。image-initrd由prepare_namespace()函式來處理。傳統的image-initrd中使用/linuxrc指令碼程式進行初始化。
    回到kernel_init,接下來的工作是開啟控制檯裝置/dev/console並設為標準輸入,有了這個裝置,啟動資訊才能顯示到終端上。後續的兩個sys_dup(0)是複製標準輸入為標準輸出和標準錯誤輸出。然後,如果rootfs中存在init檔案(使用者通過rdinit=指定,或者預設的/init,儲存在ramdisk_execute_command中),說明是載入了initramfs(包括cpio-initrd的情形),直接跳過prepare_namespace(),轉向init_post(),它會呼叫run_init_process(ramdisk_execute_command)執行這個/init檔案,替換當前程序,這樣核心的工作全部結束,後續的初始化和掛載真正根檔案系統的工作都交給/init程式。讀者可能會問如果載入了cpio-initrd, 那麼真實檔案系統中的init程序不是沒有機會運行了嗎?確實,如果載入了cpio-initrd,那麼核心就不負責執行使用者空間的init程序了,而是將這個執行任務交給了cpio-initrd的init程序。
    如果rootfs中沒有init檔案,說明是image-initrd的情形,就會轉入到prepare_namespace(),這個函式載入image-initrd,並執行它的/linuxrc檔案。prepare_namespace()的程式碼如下:
void __init prepare_namespace(void)
{
	int is_floppy;

	if (root_delay) {
		printk(KERN_INFO "Waiting %dsec before mounting root device...\n",
		       root_delay);
		ssleep(root_delay);
	}

	/*
	 * wait for the known devices to complete their probing
	 *
	 * Note: this is a potential source of long boot delays.
	 * For example, it is not atypical to wait 5 seconds here
	 * for the touchpad of a laptop to initialize.
	 */
	wait_for_device_probe();

	md_run_setup();

	if (saved_root_name[0]) {
		root_device_name = saved_root_name;
		if (!strncmp(root_device_name, "mtd", 3) ||
		    !strncmp(root_device_name, "ubi", 3)) {
			mount_block_root(root_device_name, root_mountflags);
			goto out;
		}
		ROOT_DEV = name_to_dev_t(root_device_name);
		if (strncmp(root_device_name, "/dev/", 5) == 0)
			root_device_name += 5;
	}

	if (initrd_load())
		goto out;

	/* wait for any asynchronous scanning to complete */
	if ((ROOT_DEV == 0) && root_wait) {
		printk(KERN_INFO "Waiting for root device %s...\n",
			saved_root_name);
		while (driver_probe_done() != 0 ||
			(ROOT_DEV = name_to_dev_t(saved_root_name)) == 0)
			msleep(100);
		async_synchronize_full();
	}

	is_floppy = MAJOR(ROOT_DEV) == FLOPPY_MAJOR;

	if (is_floppy && rd_doload && rd_load_disk(0))
		ROOT_DEV = Root_RAM0;

	mount_root();
out:
	devtmpfs_mount("dev");
	sys_mount(".", "/", NULL, MS_MOVE, NULL);
	sys_chroot(".");
}
    (1)對於將根檔案系統存放到USB或者SCSI裝置上的情況,Kernel需要等待這些耗費時間比較久的裝置驅動載入完畢,所以這裡存在一個Delay。
    (2)wait_for_device_probe(),從字面的意思來看,這裡也是來等待根檔案系統所在的裝置探測函式的完成。
    (3)使用者通過“root=”指定的根裝置名會被儲存在saved_root_name中,如果使用者指定了以mtd開始的字串做為它的根裝置。就會直接呼叫mount_block_root()去掛載它並goto到out。這個檔案是mtdblock的裝置檔案。否則將裝置結點檔案轉換為ROOT_DEV即裝置節點號。然後,轉向initrd_load(),去載入image-initrd,執行其中的/linuxrc,掛載最終和根檔案系統。
    (4)initrd_load()會把/dev/ram0作為預設的根裝置並把image-initrd載入到這裡。如果使用者通過root=指定了實際根裝置(不是/dev/ram0),則說明image-initrd只是作為臨時的檔案系統而存在,轉向handle_initrd(),對image-initrd進行具體的處理。它執行其中的/linuxrc,掛載最終的根檔案系統。
    (5)如果使用者沒有指定根裝置(或指定為預設的/dev/ram0),說明直接把image-initrd作為最終的真實檔案系統(在無盤工作站和很多嵌入式Linux系統中,initrd通常作為永久的根檔案系統而存在),prepare_namespace()會設定好ROOT_DEV為/dev/ram0,並呼叫mount_root()掛載這個image-initrd,作為最終的檔案系統而存在。
    (6)掛載完真正的根檔案系統後,goto到out,將掛載點從當前目錄移到"/",並把"/"作為系統的根目錄,至此虛擬檔案系統切換到了實際的根檔案系統。
    initrd_load()的程式碼如下:
int __init initrd_load(void)
{
	if (mount_initrd) {
		create_dev("/dev/ram", Root_RAM0);
		/*
		 * Load the initrd data into /dev/ram0. Execute it as initrd
		 * unless /dev/ram0 is supposed to be our actual root device,
		 * in that case the ram disk is just set up here, and gets
		 * mounted in the normal path.
		 */
		if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) {
			sys_unlink("/initrd.image");
			handle_initrd();
			return 1;
		}
	}
	sys_unlink("/initrd.image");
	return 0;
}
    (1)mount_initrd表示是否使用了image-initrd。可以通過kernel的引數“noinitrd“來配置mount_initrd的值,預設為1。很少看到有專案區配置該值,所以一般情況下,mount_initrd的值應該為1。
    (2)建立一個Root_RAM0的裝置節點/dev/ram,呼叫rd_load_image將image-initrd的資料載入到/dev/ram0。rd_load_image會開啟/dev/ram0,先是用identify_ramdisk_image()識別image-initrd的檔案系統型別,確定是romfs、squashfs、minix,還是ext2。然後用crd_load()為image-initrd分配空間、計算迴圈冗餘校驗碼(CRC)、解壓,並將其載入到記憶體中。
    (3)判斷ROOT_DEV!=Root_RAM0的含義是,如果你在grub或者lilo裡配置的root=不指定為/dev/ram0,則轉向handle_initrd(),由它來掛載實際的檔案系統。例如我電腦上的Fedora啟動指定root=/dev/mapper/VolGroup-lv_root,肯定就不是Root_RAM0了。如果沒有指定根裝置(或指定為預設的/dev/ram0),則會跳過handle_initrd(),直接返回到prepare_namespace()。
    下面是handle_initrd()的程式碼:
static void __init handle_initrd(void)
{
	int error;
	int pid;

	real_root_dev = new_encode_dev(ROOT_DEV);
	create_dev("/dev/root.old", Root_RAM0);
	/* mount initrd on rootfs' /root */
	mount_block_root("/dev/root.old", root_mountflags & ~MS_RDONLY);
	sys_mkdir("/old", 0700);
	root_fd = sys_open("/", 0, 0);
	old_fd = sys_open("/old", 0, 0);
	/* move initrd over / and chdir/chroot in initrd root */
	sys_chdir("/root");
	sys_mount(".", "/", NULL, MS_MOVE, NULL);
	sys_chroot(".");

	/*
	 * In case that a resume from disk is carried out by linuxrc or one of
	 * its children, we need to tell the freezer not to wait for us.
	 */
	current->flags |= PF_FREEZER_SKIP;

	pid = kernel_thread(do_linuxrc, "/linuxrc", SIGCHLD);
	if (pid > 0)
		while (pid != sys_wait4(-1, NULL, 0, NULL))
			yield();

	current->flags &= ~PF_FREEZER_SKIP;

	/* move initrd to rootfs' /old */
	sys_fchdir(old_fd);
	sys_mount("/", ".", NULL, MS_MOVE, NULL);
	/* switch root and cwd back to / of rootfs */
	sys_fchdir(root_fd);
	sys_chroot(".");
	sys_close(old_fd);
	sys_close(root_fd);

	if (new_decode_dev(real_root_dev) == Root_RAM0) {
		sys_chdir("/old");
		return;
	}

	ROOT_DEV = new_decode_dev(real_root_dev);
	mount_root();

	printk(KERN_NOTICE "Trying to move old root to /initrd ... ");
	error = sys_mount("/old", "/root/initrd", NULL, MS_MOVE, NULL);
	if (!error)
		printk("okay\n");
	else {
		int fd = sys_open("/dev/root.old", O_RDWR, 0);
		if (error == -ENOENT)
			printk("/initrd does not exist. Ignored.\n");
		else
			printk("failed\n");
		printk(KERN_NOTICE "Unmounting old root\n");
		sys_umount("/old", MNT_DETACH);
		printk(KERN_NOTICE "Trying to free ramdisk memory ... ");
		if (fd < 0) {
			error = fd;
		} else {
			error = sys_ioctl(fd, BLKFLSBUF, 0);
			sys_close(fd);
		}
		printk(!error ? "okay\n" : "failed\n");
	}
}
    (1)real_root_dev為一個全域性變數,用來儲存放使用者指定的根裝置號。
    (2)呼叫mount_block_root將initrd掛載到rootfs的/root下,裝置節點為/dev/root.old。提取rootfs的根目錄描述符並將其儲存到root_fd。它的作用就是為了在進入到initrd檔案系統並處理完initrd之後,還能夠返回rootfs。
    (3)進入到/root中的initrd檔案系統,呼叫kernel_thread(do_linuxrc, "/linuxrc", SIGCHLD)啟動一個核心執行緒來執行/linuxrc檔案,等待它完成的後續的初始化工作。
    (4)把initrd檔案系統移動到rootfs的/old下。然後通過root_fd重新進入到rootfs,如果real_root_dev在linuxrc中重新設成Root_RAM0,說明直接把image-initrd直接作為真正的根檔案系統,initrd_load()返回1,而後prepare_namespace()直接goto到out,改變當前目錄到initrd中,不作後續處理直接返回。
    (5)如果使用使用者指定的根裝置,則呼叫mount_root將真正的檔案系統掛載到VFS的/root目錄下。通過呼叫鏈mount_root()--->mount_block_root()--->do_mount_root()--->sys_mount(name,"/root")可知,指定的根裝置用裝置節點/dev/root表示,掛載點為VFS的/root,並將當前目錄切換到了這個掛載點下。
    (6)如果真實檔案系統中有/initrd目錄,那麼會把/old中的initrd移動到真實檔案系統的/initrd下。如果沒有/initrd目錄,則用sys_umount()解除安裝initrd,並釋放它的記憶體。
    prepare_namspace執行完後,真正的檔案系統就掛載成功。轉入init_post(),它用來執行使用者空間的第一個程序,即眾所周知的init程序。程式碼如下:
static noinline int init_post(void)
	__releases(kernel_lock)
{
	/* ...... */

	if (ramdisk_execute_command) {
		run_init_process(ramdisk_execute_command);
		printk(KERN_WARNING "Failed to execute %s\n",
				ramdisk_execute_command);
	}

	if (execute_command) {
		run_init_process(execute_command);
		printk(KERN_WARNING "Failed to execute %s.  Attempting "
					"defaults...\n", execute_command);
	}
	run_init_process("/sbin/init");
	run_init_process("/etc/init");
	run_init_process("/bin/init");
	run_init_process("/bin/sh");

	panic("No init found.  Try passing init= option to kernel. "
	      "See Linux Documentation/init.txt for guidance.");
}
    注意run_init_process在呼叫相應程式執行的時候,用的是kernel_execve。也就是說呼叫程序會替換當前程序。只要上述任意一個檔案呼叫成功,就不會返回到這個函式。如果上面幾個檔案都無法執行。打印出沒有找到init檔案的錯誤。執行使用者空間中的init程序可能是以下幾種情況:
    (1)noinitrd方式,則直接執行使用者空間中的/sbin/init(或/etc/init,/bin/init),作為第一個使用者程序。
    (2)傳統的image-initrd方式。執行的第一個程式是/linuxrc指令碼,由它來啟動使用者空間中的init程序。
    (3)cpio-initrd和initramfs方式。執行的第一個程式是/init指令碼,由它來啟動使用者空間中的init程序。
    我電腦上Fedora的/boot目錄下有initramfs-2.6.35.10-74.fc14.i686.img,它就是啟動Fedora時指定的cpio-initrd(經過了壓縮,可以用file命令檢視其檔案型別)。先加上.gz字尾,用gunzip解壓,然後用cpio -i --make-directories < initramfs-2.6.35.10-74.fc14.i686.img命令匯出它的檔案。我們可以看到根目錄下有/init指令碼,./bin目錄中有一組很少但卻非常必要的應用程式,包括dash(一個指令碼直譯器,比bash體積小速度快,相容性高,以前的initrd用的是nash)、plymouth、sed等。./sbin下有dmraid、kpartx、loginit指令碼、lvm(邏輯卷管理器)、modprobe、switch_root、udevd等核心程式。
    /init設定$PATH環境變數,掛載procfs和sysfs、啟動udev(動態裝置管理程序,通過監視sysfs按照規則動態建立/dev目錄中的裝置,已經逐漸取代了hotplug和coldplug)、掛載真正的根檔案系統、用switch_root切換到根分割槽並執行/sbin/init。    

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

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核心啟動過程