Linux核心Ramdisk(initrd)機制
摘要:對於Linux使用者來說,Ramdisk並不陌生,可是為什麼需要它呢?本文對Ramdisk在核心啟動過程中的作用,以及它的內部機制進行深入介紹。
標題
initrd 和 initramfs在核心中的處理
臨時的根目錄rootfs的掛載
initrd的解壓縮
老式的initrd的處理
cpio格式的initrd的處理
initrd例項分析
在早期的Linux系統中,一般就只有軟盤或者硬碟被用來作為Linux的根檔案系統,因此很容易把這些裝置的驅動程式整合到核心中。但是現在根檔案系統可能儲存在各種儲存裝置上,包括SCSI, SATA, U盤等等。因此把這些裝置驅動程式全部編譯到核心中顯得不太方便。在Linux核心模組自動載入機制的介紹中,我們看到利用udevd可以實現實現核心模組的自動載入,因此我們希望根檔案系統的裝置驅動程式也能夠實現自動載入。但是這裡有一個矛盾,udevd是一個可執行檔案,在根檔案系統被掛載前,是不可能執行udevd的,但是如果udevd沒有啟動,那就無法自動載入根根據系統裝置的驅動程式,同時也無法在/dev目錄下建立相應的裝置節點。為了解決這個矛盾,於是出現了initrd(boot loader initialized RAM disk)。initrd是一個被壓縮過的小型根目錄,這個目錄中包含了啟動階段中必須的驅動模組,可執行檔案和啟動指令碼。包括上面提到的udevd,當系統啟動的時候,booload會把initrd檔案讀到記憶體中,然後把initrd的起始地址告訴核心。核心在執行過程中會解壓initrd,然後把 initrd掛載為根目錄,然後執行根目錄中的/initrc指令碼,您可以在這個指令碼中執行initrd中的udevd,讓它來自動載入裝置驅動程式以及在/dev目錄下建立必要的裝置節點。在udevd自動載入磁碟驅動程式之後,就可以mount真正的根目錄,並切換到這個根目錄中。
您可以通過下面的方法來製作一個initrd檔案。
# dd if=/dev/zero of=initrd.img bs=4k count=1024
# mkfs.ext2 -F initrd.img
# mount -o loop initrd.img /mnt
# cp -r miniroot/* /mnt
# umount /mnt
# gzip -9 initrd.img
通過上面的命令,我們製作了一個4M的initrd,其中miniroot就是一個根目錄。最後我們得到一個名為initrd.img.gz的壓縮檔案。
利用initrd核心在啟動階段可以順利的載入裝置驅動程式,然而initrd存在以下缺點:
initrd大小是固定的,例如上面的壓縮之前的initrd大小是4M(4k*1024),假設您的根目錄(上例中的miniroot/)總大小僅僅是 1M,它仍然要佔用4M的空間。如果您在dd階段指定大小為1M,後來發現不夠用的時候,必須按照上面的步驟重新來一次。
initrd是一個虛擬的塊裝置,在上面的例子中,您可是使用fdisk對這個虛擬塊裝置進行分割槽。在核心中,對塊裝置的讀寫還要經過緩衝區管理模組,也就是說,當核心讀取initrd中的檔案內容時,緩衝區管理層會認為下層的塊裝置速度比較慢,因此會啟用預讀和快取功能。這樣initrd本身就在記憶體中,同時塊裝置緩衝區管理層還會儲存一部分內容。為了避免上述缺點,於是出現了initramfs,它的作用和initrd類似,您可以使用下面的方法來製作一個initramfs:
# find miniroot/ | cpio -c -o > initrd.img
# gzip initrd.img 這樣得到的initrd.img大小是可變的,它取決於您的小型根目錄miniroot/的總大小,由於首選使用cpio把根目錄進行打包,因此這個initramfs又被稱為cpio initrd. 在系統啟動階段,bootload除了從磁碟上機制核心映象bzImage之外,還要載入initrd.img.gz,然後把initrd.img.gz 的起始地址傳遞給核心。能不能把這兩個檔案合二為一呢?答案是肯定的,在Linux 2.6的核心中,可以把initrd.img.gz連結到核心檔案(ELF格式)的一個特殊的資料段中,這個段的名字為.init.ramfs。其中全域性變數__initramfs_start和__initramfs_end分別指向這個資料段的起始地址和結束地址。核心啟動時會對.init.ramfs段中的資料進行解壓,然後使用它作為臨時的根檔案系統。別看這個過程複雜,您只需要在make menuconfig中配置以下選項就可以了:
General setup --->
[*] Initial RAM filesystem and RAM disk (initramfs/initrd) support (../miniroot/) Initramfs source file(s) 其中../miniroot/就是我們的小型根目錄。這樣就只需要一個核心映象檔案就可以了。 核心在啟動過程中,必須對以下幾種情況進行處理:
如果.init.ramfs資料段大小不為0(initramfs_end - initramfs_start != 0),就說明這是initrd整合在核心資料段中。並且是cpio的initrd.
initrd是由bootloader載入到記憶體中的,這時bootloader會把起始地址和結束地址傳遞給核心,核心中的全域性 initrd_start和initrd_end分別指向initrd的起始地址和結束地址。現在核心還需要判斷這個initrd是新式的cpio格式的 initrd還是舊的initrd.
initrd 和 initramfs在核心中的處理
臨時的根目錄rootfs的掛載
首選在核心啟動過程,會初始化rootfs檔案系統,rootfs和tmpfs都是記憶體中的檔案系統,其型別為ramfs. 然後會把這個rootf掛載到根目錄。 其程式碼如下:
[start_kernel() -> vfs_caches_init() -> mnt_init()]
void __init mnt_init(void)
{ ......
init_rootfs();
init_mount_tree();
}init_rootfs()註冊rootfs檔案系統,程式碼如下:
static struct file_system_type rootfs_fs_type = {
.name = "rootfs",
.get_sb = rootfs_get_sb,
.kill_sb = kill_litter_super,};
int __init init_rootfs(void){
err = register_filesystem(&rootfs_fs_type);
...... return err;}
init_mount_tree會把rootfs掛載到/目錄,程式碼如下:
static void __init init_mount_tree(void)
{
struct vfsmount *mnt;
struct mnt_namespace *ns;
mnt = do_kern_mount("rootfs", 0, "rootfs", NULL); ......
set_fs_pwd(current->fs, ns->root, ns->root->mnt_root);
set_fs_root(current->fs, ns->root, ns->root->mnt_root);}
do_kern_mount()會呼叫前面註冊的rootfs檔案系統物件的rootfs_get_sb()函式,
[rootfs_get_sb() -> ramfs_fill_super() -> 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物件的名字為"/",也就是根目錄了。
initrd的解壓縮
在start_kernel()的最後,呼叫rest_init(),rest_init()會建立一個新的核心程序,並在這個核心程序中執行 kernel_init()函式,kernel_init()會呼叫populate_rootfs()來探測和解壓initrd檔案。這個函式需要處理上面的幾種initrd的情況。
[kernel_init() -> populate_rootfs()]static int __init populate_rootfs(void){ /* 如果__initramfs_end - __initramfs_start不為0,就說明這是和核心檔案整合在一起的cpio的intrd。*/ char *err = unpack_to_rootfs(__initramfs_start, __initramfs_end - __initramfs_start, 0); if (err) panic(err);#ifdef CONFIG_BLK_DEV_INITRD /* 如果initrd_start不為0,說明這是由bootloader載入的initrd, * 那麼需要進一步判斷是cpio格式的initrd,還是老式塊裝置的initrd。 */ if (initrd_start) {#ifdef CONFIG_BLK_DEV_RAM int fd; /* 首先判斷是不是cpio格式的initrd,也就是這裡說的initramfs。*/ printk(KERN_INFO "checking if image is initramfs..."); /* 這裡unpack_to_rootfs()的最後一個引數為1,表示check only,不會執行解壓縮。*/ err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start, 1); if (!err) { /* 如果是cpio格式的initrd,把它解壓到前面掛載的根檔案系統上,然後釋放initrd佔用的記憶體。*/ printk(" it is/n"); unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start, 0); free_initrd(); return 0; } /* 如果執行到這裡,說明這是舊的塊裝置格式的initrd。 * 那麼首先在前面掛載的根目錄上建立一個initrd.image檔案, * 再把initrd_start到initrd_end的內容寫入到/initrd.image中, * 最後釋放initrd佔用的記憶體空間(它的副本已經儲存到/initrd.image中了。)。 */ printk("it isn't (%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(); } ...... return 0;}rootfs_initcall(populate_rootfs);經過populate_rootfs()函式的處理之後,如果是cpio格式的initrd,那麼unpack_to_rootfs()函式已經把目錄解壓縮到之前mount的根目錄上面了。但是如果是舊的塊裝置的initrd,unpack_to_rootfs()函式解壓縮後得到的是一個塊虛擬的裝置映象檔案/initrd.image,對於這種情況,還需要進一步處理才能使用。接下來,kernel_init()就要處理這種情況。
static int __init kernel_init(void * unused){ ...... do_basic_setup(); /* 核心啟動時,可以通過啟動引數 rdinit=xxx 來指定啟動的最後階段,需要執行initrd中的哪一個可執行檔案, * 如果指定了這個引數,那麼ramdisk_execute_command就會指向xxx這字串,新cpio格式的initrd預設執行/init。 * 因此,如果如果ramdisk_execute_command為NULL, 就把它設定為/init。 */ if (!ramdisk_execute_command) ramdisk_execute_command = "/init"; /* 現在,嘗試訪問ramdisk_execute_command,預設為/init,如果訪問失敗,說明根目錄上不存在這個檔案。 * 於是呼叫prepare_namespace(),進一步檢查是不是舊的塊裝置的initrd * (在這種情況下,還是一個塊裝置映象檔案/initrd.image,所以訪問/init檔案失敗。)。 */ if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) { ramdisk_execute_command = NULL; prepare_namespace(); } init_post(); return 0;}老式的initrd的處理
prepare_namespace()用於處理老式的initrd。
[kernel_init() -> prepare_namespace() -> initrd_load()]int __init initrd_load(void){ if (mount_initrd) { create_dev("/dev/ram", Root_RAM0); if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) { sys_unlink("/initrd.image"); handle_initrd(); return 1; } } sys_unlink("/initrd.image"); return 0;}initrd_load()執行以下步驟:
呼叫create_dev()建立裝置檔案節點/dev/ram,其實這也是一個ramfs檔案系統。
呼叫rd_load_image()把/initrd.image載入到/dev/ram中。
呼叫handle_initrd()把把塊裝置檔案/dev/ram掛載到/root。
其中handle_initrd()程式碼如下:
[kernel_init() -> prepare_namespace() -> initrd_load() -> handle_initrd()]static void __init handle_initrd(void){ ...... real_root_dev = new_encode_dev(ROOT_DEV); /* 建立/dev/root.old裝置檔案。*/ create_dev("/dev/root.old", Root_RAM0); /* 把/dev/root.old mount到/root目錄。*/ /* 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); /* chroot到/root目錄,好了,現在/root目錄成為當前的根目錄。*/ 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; /* 建立一個執行緒,執行/linuxrc,這是舊的initrd預設執行的檔案。*/ pid = kernel_thread(do_linuxrc, "/linuxrc", SIGCHLD); ......}cpio格式的initrd的處理
對於新的cpio格式的initrd不需要額外的處理,因此kernel_init()繼續執行:
[kernel_init() -> init_post()]static int noinline init_post(void){ ...... /* 開啟console,注意如果cpio格式的根目錄中不存在/dev/console檔案, * 在unpack_to_rootfs()函式也會建立這個裝置檔案。 */ if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0) printk(KERN_WARNING "Warning: unable to open an initial console./n"); /* 現在,標準輸入,標準輸出和標準錯誤全部都是/dev/console。*/ (void) sys_dup(0); (void) sys_dup(0); /* 執行ramdisk_execute_command指定的命令,預設為/init.*/ if (ramdisk_execute_command) { run_init_process(ramdisk_execute_command); printk(KERN_WARNING "Failed to execute %s/n", ramdisk_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.");}在呼叫run_init_process()執行/init之後,這個函式就不會返回了,一般的發行版本的Linux中,initrd中的/init指令碼會啟動udevd,載入必要的裝置驅動程式,然後掛載真正的根檔案系統,最後在執行真正的根檔案系統上的initrd,這樣就這個啟動過程就順利的交接了。
塊裝置的initrd不僅使用不方便,而且在核心中的處理過程也更加複雜,因此cpio的initrd肯定會取代它,推薦使用cpio格式的initrd.
initrd例項分析
如果您使用的是ubuntu,您可以執行以下的命令來看看它的initrd中的內容。
# mkdir /tmp/initrd# cp /boot/initrd.img-xxx /tmp/initrd/initrd.img.gz# cd /tmp/initrd# gunzip initrd.img.gz# cat initrd.img | cpio -ivmd 現在,可以來看看這個根目錄的init指令碼到底做了什麼。
# cat init#!/bin/sh# ubuntu使用者一定很熟悉這個訊息。echo "Loading, please wait..."......exec run-init
{init} "
{rootmnt}/dev/console >${rootmnt}/dev/console 2>&1這個init指令碼最後執行initrd中的run-init切換到真正的根檔案系統中。
您可以對這個指令碼進行修改,加入相關的列印資訊,然後使用本文開頭介紹的方法,重新制作一個cpio的initrd,然後使用這個initrd啟動核心,快看看試驗效果吧。
本文永久更新連結:http://embeddedlinux.org.cn/emb-linux/kernel-driver/201905/05-8654.html