from: https://www.ibm.com/developerworks/cn/linux/l-k26initrd/html
簡介: Linux 的 initrd 技術是一個很是廣泛使用的機制,linux2.6 內核的 initrd 的文件格式由原來的文件系統鏡像文件轉變成了 cpio 格式,變化不只反映在文件格式上, linux 內核對這兩種格式的 initrd 的處理有着截然的不一樣。本文首先介紹了什麼是 initrd 技術,而後分別介紹了 Linux2.4 內核和 2.6 內核的 initrd 的處理流程。最後經過對 Linux2.6 內核的 initrd 處理部分代碼的分析,使讀者能夠對 initrd 技術有一個全面的認識。爲了更好的閱讀本文,要求讀者對 Linux 的 VFS 以及 initrd 有一個初步的瞭解。linux
initrd 的英文含義是 boot loader initialized RAM disk,就是由 boot loader 初始化的內存盤。在 linux內核啓動前, boot loader 會將存儲介質中的 initrd 文件加載到內存,內核啓動時會在訪問真正的根文件系統前先訪問該內存中的 initrd 文件系統。在 boot loader 配置了 initrd 的狀況下,內核啓動被分紅了兩個階段,第一階段先執行 initrd 文件系統中的"某個文件",完成加載驅動模塊等任務,第二階段纔會執行真正的根文件系統中的 /sbin/init 進程。這裏提到的"某個文件",Linux2.6 內核會同之前版本內核的不一樣,因此這裏暫時使用了"某個文件"這個稱呼,後面會詳細講到。第一階段啓動的目的是爲第二階段的啓動掃清一切障愛,最主要的是加載根文件系統存儲介質的驅動模塊。咱們知道根文件系統能夠存儲在包括IDE、SCSI、USB在內的多種介質上,若是將這些設備的驅動都編譯進內核,能夠想象內核會多麼龐大、臃腫。架構
Initrd 的用途主要有如下四種:函數
1. linux 發行版的必備部件工具
linux 發行版必須適應各類不一樣的硬件架構,將全部的驅動編譯進內核是不現實的,initrd 技術是解決該問題的關鍵技術。Linux 發行版在內核中只編譯了基本的硬件驅動,在安裝過程當中經過檢測系統硬件,生成包含安裝系統硬件驅動的 initrd,無非是一種便可行又靈活的解決方案。oop
2. livecd 的必備部件post
同 linux 發行版相比,livecd 可能會面對更加複雜的硬件環境,因此也必須使用 initrd。spa
3. 製做 Linux usb 啓動盤必須使用 initrd.net
usb 設備是啓動比較慢的設備,從驅動加載到設備真正可用大概須要幾秒鐘時間。若是將 usb 驅動編譯進內核,內核一般不能成功訪問 usb 設備中的文件系統。由於在內核訪問 usb 設備時, usb 設備一般沒有初始化完畢。因此常規的作法是,在 initrd 中加載 usb 驅動,而後休眠幾秒中,等待 usb設備初始化完畢後再掛載 usb 設備中的文件系統。
4. 在 linuxrc 腳本中能夠很方便地啓用個性化 bootsplash。
爲了使讀者清晰的瞭解Linux2.6內核initrd機制的變化,在重點介紹Linux2.6內核initrd以前,先對linux2.4內核的initrd進行一個簡單的介紹。Linux2.4內核的initrd的格式是文件系統鏡像文件,本文將其稱爲image-initrd,以區別後面介紹的linux2.6內核的cpio格式的initrd。 linux2.4內核對initrd的處理流程以下:
1. boot loader把內核以及/dev/initrd的內容加載到內存,/dev/initrd是由boot loader初始化的設備,存儲着initrd。
2. 在內核初始化過程當中,內核把 /dev/initrd 設備的內容解壓縮並拷貝到 /dev/ram0 設備上。
3. 內核以可讀寫的方式把 /dev/ram0 設備掛載爲原始的根文件系統。
4. 若是 /dev/ram0 被指定爲真正的根文件系統,那麼內核跳至最後一步正常啓動。
5. 執行 initrd 上的 /linuxrc 文件,linuxrc 一般是一個腳本文件,負責加載內核訪問根文件系統必須的驅動, 以及加載根文件系統。
6. /linuxrc 執行完畢,真正的根文件系統被掛載。
7. 若是真正的根文件系統存在 /initrd 目錄,那麼 /dev/ram0 將從 / 移動到 /initrd。不然若是 /initrd 目錄不存在, /dev/ram0 將被卸載。
8. 在真正的根文件系統上進行正常啓動過程 ,執行 /sbin/init。 linux2.4 內核的 initrd 的執行是做爲內核啓動的一箇中間階段,也就是說 initrd 的 /linuxrc 執行之後,內核會繼續執行初始化代碼,咱們後面會看到這是 linux2.4 內核同 2.6 內核的 initrd 處理流程的一個顯著區別。
linux2.6 內核支持兩種格式的 initrd,一種是前面第 3 部分介紹的 linux2.4 內核那種傳統格式的文件系統鏡像-image-initrd,它的製做方法同 Linux2.4 內核的 initrd 同樣,其核心文件就是 /linuxrc。另一種格式的 initrd 是 cpio 格式的,這種格式的 initrd 從 linux2.5 起開始引入,使用 cpio 工具生成,其核心文件再也不是 /linuxrc,而是 /init,本文將這種 initrd 稱爲 cpio-initrd。儘管 linux2.6 內核對 cpio-initrd和 image-initrd 這兩種格式的 initrd 均支持,但對其處理流程有着顯著的區別,下面分別介紹 linux2.6 內核對這兩種 initrd 的處理流程。
1. boot loader 把內核以及 initrd 文件加載到內存的特定位置。
2. 內核判斷initrd的文件格式,若是是cpio格式。
3. 將initrd的內容釋放到rootfs中。
4. 執行initrd中的/init文件,執行到這一點,內核的工做所有結束,徹底交給/init文件處理。
1. boot loader把內核以及initrd文件加載到內存的特定位置。
2. 內核判斷initrd的文件格式,若是不是cpio格式,將其做爲image-initrd處理。
3. 內核將initrd的內容保存在rootfs下的/initrd.image文件中。
4. 內核將/initrd.image的內容讀入/dev/ram0設備中,也就是讀入了一個內存盤中。
5. 接着內核以可讀寫的方式把/dev/ram0設備掛載爲原始的根文件系統。
6. .若是/dev/ram0被指定爲真正的根文件系統,那麼內核跳至最後一步正常啓動。
7. 執行initrd上的/linuxrc文件,linuxrc一般是一個腳本文件,負責加載內核訪問根文件系統必須的驅動, 以及加載根文件系統。
8. /linuxrc執行完畢,常規根文件系統被掛載
9. 若是常規根文件系統存在/initrd目錄,那麼/dev/ram0將從/移動到/initrd。不然若是/initrd目錄不存在, /dev/ram0將被卸載。
10. 在常規根文件系統上進行正常啓動過程 ,執行/sbin/init。
經過上面的流程介紹可知,Linux2.6內核對image-initrd的處理流程同linux2.4內核相比並無顯著的變化, cpio-initrd的處理流程相比於image-initrd的處理流程卻有很大的區別,流程很是簡單,在後面的源代碼分析中,讀者更能體會處處理的簡捷。
4.cpio-initrd同image-initrd的區別與優點
沒有找到正式的關於cpio-initrd同image-initrd對比的文獻,根據筆者的使用體驗以及內核代碼的分析,總結出以下三方面的區別,這些區別也正是cpio-initrd的優點所在:
cpio-initrd的製做很是簡單,經過兩個命令就能夠完成整個製做過程
#假設當前目錄位於準備好的initrd文件系統的根目錄下 bash# find . | cpio -c -o > ../initrd.img bash# gzip ../initrd.img |
而傳統initrd的製做過程比較繁瑣,須要以下六個步驟
#假設當前目錄位於準備好的initrd文件系統的根目錄下 bash# dd if=/dev/zero of=../initrd.img bs=512k count=5 bash# mkfs.ext2 -F -m0 ../initrd.img bash# mount -t ext2 -o loop ../initrd.img /mnt bash# cp -r * /mnt bash# umount /mnt bash# gzip -9 ../initrd.img |
本文不對上面命令的含義做細節的解釋,由於本文主要介紹的是linux內核對initrd的處理,對上面命令不理解的讀者能夠參考相關文檔。
經過上面initrd處理流程的介紹,cpio-initrd的處理流程顯得格外簡單,經過對比可知cpio-initrd的處理流程在以下兩個方面獲得了簡化:
1. cpio-initrd並無使用額外的ramdisk,而是將其內容輸入到rootfs中,其實rootfs自己也是一個基於內存的文件系統。這樣就省掉了ramdisk的掛載、卸載等步驟。
2. cpio-initrd啓動完/init進程,內核的任務就結束了,剩下的工做徹底交給/init處理;而對於image-initrd,內核在執行完/linuxrc進程後,還要進行一些收尾工做,而且要負責執行真正的根文件系統的/sbin/init。經過圖1能夠更加清晰的看出處理流程的區別:
圖1內核對cpio-initrd和image-initrd處理流程示意圖
如圖1所示,cpio-initrd再也不象image-initrd那樣做爲linux內核啓動的一箇中間步驟,而是做爲內核啓動的終點,內核將控制權交給cpio-initrd的/init文件後,內核的任務就結束了,因此在/init文件中,咱們能夠作更多的工做,而不比擔憂同內核後續處理的銜接問題。固然目前linux發行版的cpio-initrd的/init文件的內容尚未本質的改變,可是相信initrd職責的增長必定是一個趨勢。
上面簡要介紹了Linux2.4內核和2.6內核的initrd的處理流程,爲了使讀者對於Linux2.6內核的initrd的處理有一個更加深刻的認識,下面將對Linuxe2.6內核初始化部分同initrd密切相關的代碼給予一個比較細緻的分析,爲了講述方便,進一步明確幾個代碼分析中使用的概念:
rootfs: 一個基於內存的文件系統,是linux在初始化時加載的第一個文件系統,關於它的進一步介紹能夠參考文獻[4]。
initramfs: initramfs同本文的主題關係不是很大,可是代碼中涉及到了initramfs,爲了更好的理解代碼,這裏對其進行簡單的介紹。Initramfs是在 kernel 2.5中引入的技術,實際上它的含義就是:在內核鏡像中附加一個cpio包,這個cpio包中包含了一個小型的文件系統,當內核啓動時,內核將這個cpio包解開,而且將其中包含的文件系統釋放到rootfs中,內核中的一部分初始化代碼會放到這個文件系統中,做爲用戶層進程來執行。這樣帶來的明顯的好處是精簡了內核的初始化代碼,並且使得內核的初始化過程更容易定製。Linux 2.6.12內核的 initramfs尚未什麼實質性的東西,一個包含完整功能的initramfs的實現可能還須要一個緩慢的過程。對於initramfs的進一步瞭解能夠參考文獻[1][2][3]。
cpio-initrd: 前面已經定義過,指linux2.6內核使用的cpio格式的initrd。
image-initrd: 前面已經定義過,專指傳統的文件鏡像格式的initrd。
realfs: 用戶最終使用的真正的文件系統。
內核的初始化代碼位於 init/main.c 中的 static int init(void * unused)函數中。同initrd的處理相關部分函數調用層次以下圖,筆者按照這個層次對每個函數都給予了比較詳細的分析,爲了更好的說明,下面列出的代碼中刪除了同本文主題不相關的部分:
init函數是內核全部初始化代碼的入口,代碼以下,其中只保留了同initrd相關部分的代碼。
static int init(void * unused){ [1] populate_rootfs(); [2] if (sys_access((const char __user *) "/init", 0) == 0) execute_command = "/init"; else prepare_namespace(); [3] 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); [4] if (execute_command) run_init_process(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."); } |
代碼[1]:populate_rootfs函數負責加載initramfs和cpio-initrd,對於populate_rootfs函數的細節後面會講到。
代碼[2]:若是rootfs的根目錄下中包含/init進程,則賦予execute_command,在init函數的末尾會被執行。不然執行prepare_namespace函數,initrd是在該函數中被加載的。
代碼[3]:將控制檯設置爲標準輸入,後續的兩個sys_dup(0),則複製標準輸入爲標準輸出和標準錯誤輸出。
代碼[4]:若是rootfs中存在init進程,就將後續的處理工做交給該init進程。其實這段代碼的含義是若是加載了cpio-initrd則交給cpio-initrd中的/init處理,不然會執行realfs中的init。讀者可能會問:若是加載了cpio-initrd, 那麼realfs中的init進程不是沒有機會運行了嗎?確實,若是加載了cpio-initrd,那麼內核就不負責執行realfs的init進程了,而是將這個執行任務交給了cpio-initrd的init進程。解開fedora core4的initrd文件,會發現根目錄的下的init文件是一個腳本,在該腳本的最後一行有這樣一段代碼:
……….. switchroot --movedev /sysroot |
就是switchroot語句負責加載realfs,以及執行realfs的init進程。
對cpio-initrd的處理位於populate_rootfs函數中。
void __init populate_rootfs(void){ [1] char *err = unpack_to_rootfs(__initramfs_start, __initramfs_end - __initramfs_start, 0); [2] if (initrd_start) { [3] err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start, 1); [4] if (!err) { printk(" it is\n"); unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start, 0); free_initrd_mem(initrd_start, initrd_end); return; } [5] fd = sys_open("/initrd.image", O_WRONLY|O_CREAT, 700); if (fd >= 0) { sys_write(fd, (char *)initrd_start, initrd_end - initrd_start); sys_close(fd); free_initrd_mem(initrd_start, initrd_end); } } |
代碼[1]:加載initramfs, initramfs位於地址__initramfs_start處,是內核在編譯過程當中生成的,initramfs的是做爲內核的一部分而存在的,不是 boot loader加載的。前面提到了如今initramfs沒有任何實質內容。
代碼[2]:判斷是否加載了initrd。不管哪一種格式的initrd,都會被boot loader加載到地址initrd_start處。
代碼[3]:判斷加載的是否是cpio-initrd。實際上 unpack_to_rootfs有兩個功能一個是釋放cpio包,另外一個就是判斷是否是cpio包, 這是經過最後一個參數來區分的, 0:釋放 1:查看。
代碼[4]:若是是cpio-initrd則將其內容釋放出來到rootfs中。
代碼[5]:若是不是cpio-initrd,則認爲是一個image-initrd,將其內容保存到/initrd.image中。在後面的image-initrd的處理代碼中會讀取/initrd.image。
對image-initrd的處理 在prepare_namespace函數裏,包含了對image-initrd進行處理的代碼,相關代碼以下:
void __init prepare_namespace(void){ [1] if (initrd_load()) goto out; out: umount_devfs("/dev"); [2] sys_mount(".", "/", NULL, MS_MOVE, NULL); sys_chroot("."); security_sb_post_mountroot(); mount_devfs_fs (); } |
代碼[1]:執行initrd_load函數,將initrd載入,若是載入成功的話initrd_load函數會將realfs的根設置爲當前目錄。
代碼[2]:將當前目錄即realfs的根mount爲Linux VFS的根。initrd_load函數執行完後,將真正的文件系統的根設置爲當前目錄。
initrd_load函數負責載入image-initrd,代碼以下:
int __init initrd_load(void) { [1] if (mount_initrd) { create_dev("/dev/ram", Root_RAM0, NULL); [2] 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]:若是加載initrd則創建一個ram0設備 /dev/ram。
代碼[2]:/initrd.image文件保存的就是image-initrd,rd_load_image函數執行具體的加載操做,將image-nitrd的文件內容釋放到ram0裏。判斷ROOT_DEV!=Root_RAM0的含義是,若是你在grub或者lilo裏配置了 root=/dev/ram0 ,則實際上真正的根設備就是initrd了,因此就不把它做爲initrd處理 ,而是做爲realfs處理。
handle_initrd()函數負責對initrd進行具體的處理,代碼以下:
static void __init handle_initrd(void){ [1] real_root_dev = new_encode_dev(ROOT_DEV); [2] create_dev("/dev/root.old", Root_RAM0, NULL); mount_block_root("/dev/root.old", root_mountflags & ~MS_RDONLY); [3] 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 */ [4] sys_chdir("/root"); sys_mount(".", "/", NULL, MS_MOVE, NULL); sys_chroot("."); mount_devfs_fs (); [5] pid = kernel_thread(do_linuxrc, "/linuxrc", SIGCHLD); if (pid > 0) { while (pid != sys_wait4(-1, &i, 0, NULL)) yield(); } /* move initrd to rootfs' /old */ sys_fchdir(old_fd); sys_mount("/", ".", NULL, MS_MOVE, NULL); /* switch root and cwd back to / of rootfs */ [6] sys_fchdir(root_fd); sys_chroot("."); sys_close(old_fd); sys_close(root_fd); umount_devfs("/old/dev"); [7] if (new_decode_dev(real_root_dev) == Root_RAM0) { sys_chdir("/old"); return; } [8] ROOT_DEV = new_decode_dev(real_root_dev); mount_root(); [9] 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); 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"); } |
handle_initrd函數的主要功能是執行initrd的linuxrc文件,而且將realfs的根目錄設置爲當前目錄。
代碼[1]:real_root_dev,是一個全局變量保存的是realfs的設備號。
代碼[2]:調用mount_block_root函數將initrd文件系統掛載到了VFS的/root下。
代碼[3]:提取rootfs的根的文件描述符並將其保存到root_fd。它的做用就是爲了在chroot到initrd的文件系統,處理完initrd以後要,還可以返回rootfs。返回的代碼參考代碼[7]。
代碼[4]:chroot進入initrd的文件系統。前面initrd已掛載到了rootfs的/root目錄。
代碼[5]:執行initrd的linuxrc文件,等待其結束。
代碼[6]:initrd處理完以後,從新chroot進入rootfs。
代碼[7]:若是real_root_dev在 linuxrc中從新設成Root_RAM0,則initrd就是最終的realfs了,改變當前目錄到initrd中,不做後續處理直接返回。
代碼[8]:在linuxrc執行完後,realfs設備已經肯定,調用mount_root函數將realfs掛載到root_fs的 /root目錄下,並將當前目錄設置爲/root。
代碼[9]:後面的代碼主要是作一些收尾的工做,將initrd的內存盤釋放。
到此代碼分析完畢。
經過本文前半部分對cpio-initrd和imag-initrd的闡述與對比以及後半部分的代碼分析,我相信讀者對Linux 2.6內核的initrd技術有了一個較爲全面的瞭解。在本文的最後,給出兩點最重要的結論:
1. 儘管Linux2.6既支持cpio-initrd,也支持image-initrd,可是cpio-initrd有着更大的優點,在使用中咱們應該優先考慮使用cpio格式的initrd。
2. cpio-initrd相對於image-initrd承擔了更多的初始化責任,這種變化也能夠看做是內核代碼的用戶層化的一種體現,咱們在其它的諸如FUSE等項目中也看到了將內核功能擴展到用戶層實現的嘗試。精簡內核代碼,將部分功能移植到用戶層必然是linux內核發展的一個趨勢。
從下面三篇文章中,能夠得到更多的關於initramfs的知識:
[1]http://tree.celinuxforum.org/pubwiki/moin.cgi/EarlyUserSpace
[2]http://lwn.net/Articles/14776/
[3]http://www.ussg.iu.edu/hypermail/linux/kernel/0211.0/0341.html
從下面這篇文章中讀者能夠了解到關於linux VSF、rootfs的相關知識:
[4] http://www.ibm.com/developerworks/cn/linux/l-vfs/
下面是一些initrd的參考資料: