initramfs的作用
1. 作為啟動跳板
kernel掛載initramfs,運行init程序,該程序會探測硬件,加載驅動,最后掛載真正的文件系統,執行文件系統上的init程序,進而切換到用戶空間,
真正的文件系統掛載后,initramfs使命完成,釋放其占用空間。
2. 作為最終文件系統
ramfs也可以作為最終文件系統,優點是速度快,重啟后文件復原,缺點是文件在ram和rom同時存在。
為什么要initramfs
1. 讓內核兼容不同的存儲介質
內核被bootloader加載到內存后,內核需要從rom中讀取程序或驅動,而若要操作rom,內核必須具有對應驅動(如從磁盤數據,則內核必須編譯進了磁盤驅動),
但是存儲介質種類繁多,內核若要兼容所有的存儲介質,則要將對應的所有驅動都編譯進內核,這會導致內核冗余巨大。
所以開發者設計了initramfs,initramfs是基於內存的文件系統,所以所有環境下都兼容,那么讓內核先掛載initramfs,內核根據當前硬件環境加載對應的
驅動,然后再掛載對於硬件的文件系統。
2. 文件系統掛載前可能需要復雜的工作
如掛載網絡文件系統前,需要配置網絡,此外某些文件系統還需要解壓,解密等操作,為保證內核的穩定,我們希望將這些操作實現為應用程序,
但是啟動應用程序需要內核能訪問存儲介質,可此時沒有尚未掛載文件系統,
所以可以先掛在initramfs,完成文件系統掛載前的工作,再掛載新文件系統。
ramfs 和 initrd
ramfs 是 initrd 的替代品
1. initrd
initrd是基於 ramdisk 的技術,是基於內存的 快設備。因此 initrd 具有快設備的一切屬性。
- initrd 容量固定,一旦創建無法動態調整
- initrd 需要按照一定的文件系統格式進行組織,因此制作時需要mke2fs這樣的工具格式化initrd,訪問initrd時需要文件系統驅動
- initrd 是偽塊設備,從內核角度,與真實塊設備無區別,所以內核訪問 initrd 也使用緩存機制。但這是多此一舉,因為initrd本就在內存中。
2. ramfs
Linus Torvalds 將cache當作一個文件系統直接掛載使用。
ramfs 是基於緩存的文件系統。所以ramfs去除了塊設備的一些限制
- ramfs 根據其中包含的文件大小可以自由伸縮:增加文件時自動分配內存,刪除文件時,自動釋放內存。
- ramfs 是基於已有的緩存機制,因此不必像ramdisk 那樣需要緩存之間進行多余的復制
3. initramfs 的原理
3.1 initramfs 的工作流程
內核首先掛載一個名為rootfs的文件系統,並將rootfs的根作為虛擬文件系統目錄樹的總根。
掛載rootfs后,內核將bootloader加載到內存中的initramfs中打包的文件解壓到rootfs中,而這些文件中包含了驅動以及掛載真正的根文件系統的工具,內核通過加載這些驅動,使用這些工具,實現了掛載真正的根文件系統。
此后rootfs完成使命,被真正的根文件系統覆蓋(overmount),但是rootfs作為虛擬文件系統目錄樹的總根,並不能被釋放。但這沒有關系,因為rootfs基於ramfs,刪除其中的文件即可釋放其占用的空間。
上面提到的 rootfs 就是一個 ramfs,由於其為基於緩存的文件系統,所以不需要設備驅動,有因為ramfs的空間是動態伸縮的,所以可以增刪文件,將bootloader 加載的壓縮文件 解壓,到 rootfs ,就完成了 rootfs 的構建。
3.2 什么是掛載
3.2.1 文件系統的組織結構
以ExtX為例,
(1) 超級塊(super block)
描述整個文件系統信息,包括inode總數,空閑的inode數量,塊大小,掛載次數等。
(2)塊組描述符(Group Descriptors)
包括所有塊組的描述
(3)塊位圖(Block Bitmap)
描述塊組中哪些塊已使用,哪些空閑
(4)索引節點位圖(Inode Bitmap)
和塊位圖類似,索引節點用於表示哪些inode已用,哪些inode空閑。
(5)索引節點表(Inode Table)
inode存儲文件描述信息,包括,文件類型,權限,文件大小,創建/修改/訪問時間,數據塊的索引,等。
所有的inode組成一個inode數組。
(6)數據塊(Data Block)
數據塊存儲文件數據,但是不同文件類型的文件,的數據塊的存儲內容是不同的,
- 常規文件類型:數據塊中存儲的是文件的數據
- 目錄類型:數據塊中存儲的是目錄下所有的文件名和子目錄名
對於 設備文件,socket文件等特殊文件,不需要數據塊,只需要將相關信息保存到inode中。
以上信息,在格式化存儲介質時,在存儲介質上構造。
虛擬文件系統需要模擬上面的對象。
-
樹形文件系統
文件系統都是以樹形組織,新增的樹可以掛載到原有的樹的任何節點。
虛擬文件系統是第一顆樹,上面只有一個節點,第一個添加的文件系統掛載到虛擬文件系統的根節點。 -
mount結構,描述樹之間的掛載關系
mount->mnt_mountpoint指向 被掛載的文件系統的掛載點,
mount->mnt_parent 指向掛載點所在文件系統的mount
mount->mnt_root 指向 掛載的文件系統的根
對於 基於ramfs 的 rootfs 掛載 虛擬文件系統的情況如下:
首先為 rootfs 創建一個 mount,由於 rootfs整個虛擬文件的第一個文件系統,
mnt_parent ,指向 rootfs的mount自己
mnt_mountpoint 指向rootfs自己的根mnt_root -
超級塊
訪問文件系統需要文件系統的超級塊,
所以載入rom的超級塊,並在內存進行實例化。
對於ramfs不存在超級塊,所以模擬一個 -
inode
每個文件都用一個inode表示,對於ramfs將模擬一個根節點inode -
dentry
dentry 表示父文件節點和子文件節點的關系,
通過構建dentry,實現將文件加入虛擬文件系統樹。
dentry 在rom中沒有實體,之存在於ram,內核會緩存最近訪問的dentry。
掛載完基於ramfs的rootfs后,有如下結構
整個虛擬文件系統樹對用戶進程不可見,進程看到的只是一個分支,
也就是namespace的概念,每個進程有自己的文件系統空間,通常多數進程的namespace是相同的,
通過掛在rootfs,虛擬文件系統的根目錄建立起了,根目錄下可以擴充文件,
內核解壓initramfs的內容到虛擬文件系統的根,並利用initramfs的內容掛載並切換到真正的文件系統。
3.3 內核如何獲得 initramfs, 如何使用initramfs
6 obj-y += noinitramfs.o
7 obj-$(CONFIG_BLK_DEV_INITRD) += initramfs.o
如果 CONFIG_BLK_DEV_INITRD ,則使用 initramfs
否則使用 默認則 initramfs
initramfs.c如下
636 static int __init populate_rootfs(void)
637 {
638 char *err;
639
...
645 // 先解壓 內嵌到內核的 initramfs
646 err = unpack_to_rootfs(__initramfs_start, __initramfs_size);
647 if (err)
648 panic("%s", err); /* Failed to decompress INTERNAL initramfs */
649 // 若有相關 啟動參數,則從外部加載initramfs 並解壓
if (initrd_start) {
650 #ifdef CONFIG_BLK_DEV_RAM
682 #else
683 printk(KERN_INFO "Unpacking initramfs...\n");
684 err = unpack_to_rootfs((char *)initrd_start,
685 initrd_end - initrd_start);
689 #endif
690 /*
691 * Try loading default modules from initramfs. This gives
692 * us a chance to load before device_initcalls.
693 */
694 load_default_modules();
695 }
696 return 0;
697 }
// populate_rootfs加入初始化數組
716 rootfs_initcall(populate_rootfs);
__initramfs_start 和 __initramfs_size
在 vmlinux.lds中定義
896 .init.data : {
897 *(.init.data) *(.meminit.data) . = ALIGN(8); __start_mcount_loc = .; *(__mcount_loc) __stop_mcount_loc = .; *(.init.rodata) . = ALIGN(8); __start_ftrace_events = .; *(_ftrace_events) __stop_ftrace_events = .; __start_ftrace_enum_maps = .; *(_ftrace_enum_map) __stop_ ftrace_enum_maps = .; *(.meminit.rodata) . = ALIGN(8); __clk_of_table = .; *(__clk_of_table) *(__clk_of_table_end) . = ALIGN(8); __reservedmem_of_table = .; *(__reservedmem_of_table) *(__reservedmem_of_table_end) . = ALIGN(8); __clksrc_of_table = .; *(__clksrc_of_tabl e) *(__clksrc_of_table_end) . = ALIGN(8); __iommu_of_table = .; *(__iommu_of_table) *(__iommu_of_table_end) . = ALIGN(8); __cpu_method_of_table = .; *(__cpu_method_of_table) *(__cpu_method_of_table_end) . = ALIGN(8); __cpuidle_method_of_table = .; *(__cpuidle_method_o f_table) *(__cpuidle_method_of_table_end) . = ALIGN(32); __dtb_start = .; *(.dtb.init.rodata) __dtb_end = .; . = ALIGN(8); __irqchip_of_table = .; *(__irqchip_of_table) *(__irqchip_of_table_end) . = ALIGN(32); __earlycon_table = .; *(__earlycon_table) *(__earlycon_tab le_end) . = ALIGN(8); __earlycon_of_table = .; *(__earlycon_of_table) *(__earlycon_of_table_end)
898 . = ALIGN(16); __setup_start = .; *(.init.setup) __setup_end = .;
899 __initcall_start = .; *(.initcallearly.init) __initcall0_start = .; *(.initcall0.init) *(.initcall0s.init) __initcall1_start = .; *(.initcall1.init) *(.initcall1s.init) __initcall2_start = .; *(.initcall2.init) *(.initcall2s.init) __initcall3_start = .; *(.initcall3 .init) *(.initcall3s.init) __initcall4_start = .; *(.initcall4.init) *(.initcall4s.init) __initcall5_start = .; *(.initcall5.init) *(.initcall5s.init) __initcallrootfs_start = .; *(.initcallrootfs.init) *(.initcallrootfss.init) __initcall6_start = .; *(.initcall6.init ) *(.initcall6s.init) __initcall7_start = .; *(.initcall7.init) *(.initcall7s.init) __initcall_end = .;
900 __con_initcall_start = .; *(.con_initcall.init) __con_initcall_end = .;
901 __security_initcall_start = .; *(.security_initcall.init) __security_initcall_end = .;
902 . = ALIGN(4); __initramfs_start = .; *(.init.ramfs) . = ALIGN(8); *(.init.ramfs.info)
903 }
可見 initramfs 會被鏈接到 init_begin 和 init_end之間,所以在內核初始化完成后 cpio 壓縮文件會從內存中釋放。
而此時 cpio 解壓獲得的 initramfs 已經拷貝到了 ramfs 的 根文件系統的 根目錄下。
如果用戶沒有使用 initramfs,則會用默認的 initramfs,也就是 noinitramfs.o
33 int __init default_rootfs(void)
34 {
35 int err;
36
37 err = sys_mkdir((const char __user __force *) "/dev", 0755);
38 if (err < 0)
39 goto out;
40
41 err = sys_mknod((const char __user __force *) "/dev/console",
42 S_IFCHR | S_IRUSR | S_IWUSR,
43 new_encode_dev(MKDEV(5, 1)));
44 if (err < 0)
45 goto out;
46
47 err = sys_mkdir((const char __user __force *) "/root", 0700);
48 if (err < 0)
49 goto out;
50
51 return 0;
52
53 out:
54 printk(KERN_WARNING "Failed to create a rootfs\n");
55 return err;
56 }
57 #if !IS_BUILTIN(CONFIG_BLK_DEV_INITRD)
58 rootfs_initcall(default_rootfs);
可見內核會保證最小的文件系統,避免 由於第一個進程打不開控制台導致 panic.
4. 掛載新的文件系統
假設新的文件系統掛載到 initramfs 的 root 目錄
mount2->mnt_parent 指向父文件系統
mount2->mnt_mountpoint 指向掛載點,這里是 root目錄
mount2->mnt_root 指向自己的根目錄
此時進程未切換文件系統空間,所以使用 initramfs的根目錄,為進程的文件系統根
mount2 和 mount 會被加入hash表,
當訪問某個dentry時,若其被標記為被掛載狀態,則求hash得到對應的mount,在對應mount下訪問文件。
真實情況,我們將mount2掛載到mount1的根目錄下,然后切換進程的namespace。
對於mount1的文件需要刪除,由於mount1是 ramfs類型,所以刪除文件就自動釋放對應內存,
由於mount1是整個虛擬文件系統的根,所以mount1不能卸載。
5. 基於ramfs構建基礎文件系統
5.1 配置內核使用initramfs
開啟 Initial RAM filesystem and RAM disk (initramfs/initrd) support
可以配置Initramfs source file(s) ,若配置,則使用嵌入內核的方式,initramfs會被壓縮鏈接到.init.ramfs段,
不配置則使用 外部加載方式,若使用外部加載方式,需要bootloader傳遞啟動參數。
5.2 devtmpfs
FHS規定在/dev下創建設備節點,以前是靜態創建,但是硬件環境不同,靜態創建不太適合,於是設計了udev,
udev為應用程序,負責創建節點和權限控制,udev會根據內核檢查到的硬件信息自動創建設備節點。
由於 設備節點不需要持久記錄,所以 推薦在 /dev目錄掛載 ramfs 文件系統,
后來開發者在 專門 實現了 tmpfs,tmpfs就是 基於緩存的 文件系統,
linux從 2.6開始使用udev,/dev目錄使用基於內存的文件系統哦 tmpfs
后來開發者又推出了 devtmpfs,在內核引導時,devtmpfs創建所有注冊的設備的設備文件,
在進入 用戶空間前,將 devtmpfs 掛載到 /dev目錄,也就是說在 udev啟動前,devtmpfs已經建立的初步的設備文件。
這樣的好處是:由於進入應用空間前設備文件已創建,所以應用程序不需要等待udev。
而 devtmpfs 的本質是,若內核支持 tmpfs,則為tmpfs,否則為 ramfs
devtmpfs是內核實現所以要配置內核,
udev為應用程序
新編譯的內核啟動后,/dev目錄下只有一個console,這是 initramfs創建的,而 devtmpfs創建的設備節點在 devtmpfs文件系統里,
所以需要掛載 devtmpfs 到 /dev目錄
# -n : mount 會在 /etc/mtab 文件維護一個已掛載文件系統列表,由於我們沒有 /etc/mtab文件,所以不需要維護
# udev : devtmpfs是基於內存的,沒有對應設備,所以名稱可以隨便取,命名為udev,是為了說明下面文件為udev創建
mount -n -t devtmpfs udev /dev
掛載成功后,/dev目錄下有很多設備文件。
5.3 掛載系統文件
這兩個文件系統和 devtmpfs 一樣都是基於內存的,所以名稱也能隨便取。
有了這兩個文件,一些命令就可以用了,如 modprobe
mount -n -t proc proc /proc
mount -n -t sysfs sysfs /sys