1. 程式人生 > >linux文件系統 - 初始化(二)

linux文件系統 - 初始化(二)

軟鏈接 復制代碼 模式 文件的 操作 pop 臨時 console mini

一、目的

本文主要講述linux3.10文件系統初始化過程的第二階段:加載initrd。

initrd是一個臨時文件系統,由bootload負責加載到內存中,裏面包含了基本的可執行程序和驅動程序。在linux初始化的初級階段,它提供了一個基本的運行環境。當成功加載磁盤文件系統後,系統將切換到磁盤文件系統並卸載initrd。

如果是嵌入式設備,那麽最終的文件系統就是initrd。

二、cpio文件格式

initrd常用的的文件格式是cpio,cpio格式記錄了文件系統的結構和內容。

cpio格式具體定義如圖1所示:

cpio格式的文件由段組成,最後一個段比較特殊,文件名為”TRAILER!!!”。

每個段都由文件頭、文件名和文件體組成;文件名和文件體的長度由文件頭中的name_len和body_len指定,並且文件名和文件體需要按指定字節對齊,所以尾部包含padding。

文件頭共110個字節,頭6個字節固定為070701,剩下字節的含義分別為:索引節點號、文件模式、用戶id、組id、鏈接數、時間戳、文件體長度、主設備號、次設備號、設備號、文件名長度、保留字段。

其他詳細情況請參見init/initramfs.c文件,這裏不再描述。

技術分享圖片

圖1

三、initrd文件實例

為了更直觀的理解cpio格式的initrd文件,下面看一個實例。

ubuntu環境中,boot目錄下存放著經過壓縮的cpio格式文件initrd。

boot目錄下的initrd文件拷貝到任意目錄下,重名為為initrd.gz,並且使用gunzip解壓。

這樣我們就得到了一個cpio格式的initrd文件,使用vi查看文件內容如圖2所示(由於文件太大,只展示了部分內容):

簡單分析後顯示該文件包含了script/nfs-top目錄、script/nfs-top/ORDER文件、script/nfs-top/udev文件、run目錄、標誌cpio結束的TRAILER!!!文件。

技術分享圖片

圖2

四、解壓initrd文件

initrd經過gunzip解壓後,可以使用cpio工具解壓cpio格式的文件。命令如下:

  1. root: cpio-idmv < initrd

解壓成功後,使用ls命令查看initrd文件內容如圖3所示:

bin和sbin目錄下包含基本的可執行程序;conf和etc目錄下是配置文件;lib目錄下是可執行程序使用的動態庫;scripts目錄下是腳本程序;init程序。initrd必須提供一個init程序,linux在加載完initrd後,會跳轉到init程序,由init程序負責後面的初始化工作。

技術分享圖片

圖3

五、總結

本文詳細介紹了cpio格式的initrd文件,以及解壓後各個目錄的含義。initrd文件系統提供了init程序,在linux初始化階段的後期會跳轉到init程序,由該程序負責加載驅動程序和掛載磁盤文件系統以及其他的初始化工作。

加載initrd(中)

一、目的

上文詳細介紹了CPIO格式的initrd文件,本文從源代碼角度分析加載並解析initrd文件的過程。

initrd文件和linux內核一般存儲在磁盤空間中,在系統啟動階段由bootload負責把磁盤上的內核和initrd加載到指定的內存空間中;然後,再由內核讀取和解析initrd文件,在VFS(目前只有rootfs的根目錄)中新建目錄、常規文件、符號鏈接文件以及特殊文件;這樣VFS就從根目錄"/"成長為一棵枝繁葉茂的大樹了。

二、函數調用過程

initrd詳細的加載過程在init/initramfs.c中實現的,為了更好的理解加載過程,我們給出了關鍵函數的調用關系圖1。這裏需要註意下,由於使用roofs_initcall()宏在initcallroofs段中註冊了populate_rootfs()函數,因此在執行do_initcalls()函數時會隱示調用populate_rootfs()。

技術分享圖片

圖1

三、initcall簡介

linux在代碼段中定義了一個特殊的段initcall,該段中存放的都是函數指針;linux初始化階段調用do_initcalls()依次執行該段的函數。關於該段的詳細信息可以參見vmlinux.lds.S鏈接腳本。

用戶可以調用以下一組宏在initcall段中註冊函數指針;initcall段分為initcall0-initcall7這8個等級,initcall0段的優先級最高,initcall7段的優先級最低,優先級高的段最先被執行;initcallrootfs段優先級介於5和6之間。

#define __define_initcall(fn, id) 179     static initcall_t __initcall_##fn##id __used 180     __attribute__((__section__(".initcall" #id ".init"))) = fn
技術分享圖片
187 #define early_initcall(fn)          __define_initcall(fn, early)
      
196 #define pure_initcall(fn)           __define_initcall(fn, 0)
        
198 #define core_initcall(fn)           __define_initcall(fn, 1)
199 #define core_initcall_sync(fn)      __define_initcall(fn, 1s)
200 #define postcore_initcall(fn)       __define_initcall(fn, 2)
201 #define postcore_initcall_sync(fn)  __define_initcall(fn, 2s)
202 #define arch_initcall(fn)           __define_initcall(fn, 3)
203 #define arch_initcall_sync(fn)      __define_initcall(fn, 3s)
204 #define subsys_initcall(fn)         __define_initcall(fn, 4)
205 #define subsys_initcall_sync(fn)    __define_initcall(fn, 4s)
206 #define fs_initcall(fn)             __define_initcall(fn, 5)
207 #define fs_initcall_sync(fn)        __define_initcall(fn, 5s)
208 #define rootfs_initcall(fn)         __define_initcall(fn, rootfs)
209 #define device_initcall(fn)         __define_initcall(fn, 6)
210 #define device_initcall_sync(fn)    __define_initcall(fn, 6s)
211 #define late_initcall(fn)           __define_initcall(fn, 7)
212 #define late_initcall_sync(fn)      __define_initcall(fn, 7s)
技術分享圖片

用戶使用不同優先級的initcall宏可以很方便的在linux代碼中註冊函數指針;將這些函數指針存儲在相應的initcall段中;最終,由do_initcalls()按照優先級依次執行段中的函數,具體的代碼實現如下:

技術分享圖片
715 static initcall_t *initcall_levels[] __initdata = {
716     __initcall0_start,
717     __initcall1_start,
718     __initcall2_start,
719     __initcall3_start,
720     __initcall4_start,
721     __initcall5_start,
722     __initcall6_start,
723     __initcall7_start,
724     __initcall_end,
725 };

678 int __init_or_module do_one_initcall(initcall_t fn)
679 {
681     int ret;
686     ret = fn();
    }

739 static void __init do_initcall_level(int level)
740 {
742     initcall_t *fn;
            ... 
751     for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
752         do_one_initcall(*fn);
753 }
754 
755 static void __init do_initcalls(void)
756 {
757     int level;
758 
759     for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
760         do_initcall_level(level);
761 }
技術分享圖片

回到加載initrd這個話題中,在init/initram.c的最後使用rootfs_initcall宏註冊了populate_rootfs()函數;基於以上分析,我們知道這裏就是加載initrd文件的入口,下面就開始分析該函數的功能。

627 rootfs_initcall(populate_rootfs);

四、加載initrd文件

系統啟動階段,bootload將initrd加載到內存起始地址為initrd_start,結束地址為initrd_end的內存中。

populate_rootfs()調用unpack_to_rootfs()從內存中讀取並解析initrd文件;根據CPIO的格式我們知道initrd文件是由很多個段組成,且段中又是由文件頭、文件名和文件體組成,因此該解析程序可以使用了狀態機原理處理initrd文件。

解析程序定義了以下8種狀態:Start(初始狀態)、Collect(獲取符號鏈接文件信息狀態)、GotHeader(獲取文件頭信息狀態)、SkipIt(跳過該段狀態)、GotName(獲取文件名並新建文件狀態)、CopyFile(寫文件狀態)、GotSymlink(新建符號鏈接文件狀態)、Reset(終止狀態)。

技術分享圖片
376 static __initdata int (*actions[])(void) = {
377     [Start]     = do_start,
378     [Collect]   = do_collect,
379     [GotHeader] = do_header,
380     [SkipIt]    = do_skip,
381     [GotName]   = do_name,
382     [CopyFile]  = do_copy,
383     [GotSymlink]    = do_symlink,
384     [Reset]     = do_reset,
385 };
技術分享圖片

為了直觀理解initrd文件的解析過程,下面給出狀態機跳轉圖2。

從圖中可以看出將文件分為符號鏈接和非符號鏈接兩種情況處理,這是因為符號鏈接文件是一種特殊的文件,只有第一個符號鏈接文件的inode存儲的是真實數據,而其他符號鏈接文件inode中存儲的是第一個符號鏈接文件的路徑名,因此需要把第一個符號鏈接文件的路徑名緩存起來,緩存的數據結構是hash表,所以在處理符號鏈接文件時多了一些hash表的操作,因此分為了符號鏈接文件和非符號鏈接文件這兩種情況來處理。

initrd文件的詳細解析過程如下:

1、S0:初始狀態,初始化一些全局變量;

2、S1:獲取符號鏈接文件的文件頭和文件體;

3、S2:根據CPIO格式的定義,獲取文件頭信息;

4、S3:跳過當前CPIO格式的段,繼續處理下一個段;

5、S4:獲取文件名,並在VFS中新建文件;

6、S5:將文件內容寫入到新建文件中;

7、S6:新建符號鏈接文件;

8、S7:處理完當前CPIO格式的段,繼續一個段的處理。

從圖中還可以看出,由於目錄文件和特殊文件沒有文件內容,因此跳過了S5狀態,直接進入S3狀態。

技術分享圖片

圖2

五、總結

通過以上分析,程序就可以成功解析initrd文件,並使用sys_dir()、sys_open()、sys_mknod()、sys_symlink()等系統調用新建目錄、常規文件、特殊文件和符號鏈接文件了。此時,VFS從只有根目錄"/"成長為了一棵內容豐富的大樹。

加載initrd(下)

一、目的

linux把文件分為常規文件、目錄文件、軟鏈接文件、硬鏈接文件、特殊文件(設備文件、管道文件、socket文件等)幾種類型,分別對應不同的新建函數sys_open()、sys_mkdir()、sys_symlink()、sys_link()、sys_mknod()。

系統初始化階段成功加載initrd後,調用這些接口函數創建各種文件,因此這些函數在linux文件系統初始化過程中起到了重要作用,本文將詳細描述這些接口函數的實現過程。

這些接口函數主要在fs/namei.c、fs/open.c文件中實現,可以在這兩個文件中找到對應的源代碼。

二、快速路徑查找

在以上系統調用的實現中,需要解決一個關鍵問題:如何根據待新建文件的路徑名,快速找到父目錄的位置。得到父目錄的位置後,才能創建待新建文件的目錄,以及分配inode節點。

例如:新建/tmp/test.txt文件,首先需要根據”/tmp/test.txt”路徑名查找到test.txt父目錄tmp在VFS中的位置,然後基於tmp的位置才能為test.txt文件新建目錄及分配inode節點。

由於快速路徑查找使用比較頻繁,所以對查找效率要求較高,否則會影響系統性能。do_path_lookup(intdfs, const char *name, unsigned int flags, struct nameidata*nd)函數負責實現該功能,其中name參數是路徑名,nd參數是structnameidata結構體,nd的path成員記錄了父目錄的位置,last成員記錄了待新建文件的文件名。例如:path記錄了tmp在VFS中的位置,last記錄”test.txt”文件名。

該函數的實現比較復雜,不方便對照源代碼講述,因此下面重點描述了該函數的主要操作。但是為了讀者方便閱讀源代碼,給出如下關鍵函數的調用關系:do_path_lookup()->path_lookupat()->path_init()->link_path_walk()->walk_component()->lookup_fast()->__d_look_rcu()->__follow_mount_rcu()

do_path_lookup()在實現過程中,按照以下三種情形來操作:

1、路徑名在單文件系統中:從根目錄或當前目錄開始,根據目錄拓撲結構,遞歸查找父目錄;

2、路徑名在多文件系統中:在遞歸查找時,需要從當前文件系統的掛載點切換到最終文件系統的目錄拓撲結構中,然後繼續查找;

3、路徑名在掛載點重復掛載多文件系統的情況:在遞歸查找時,需要從當前文件系統的掛載點切換到最終文件系統的目錄拓撲結構中,然後繼續查找;

為了理解以上三種情形的具體差異,舉例說明:待新建文件路徑名為”/usr/tmp/log/new.txt”。

情形1:

如下圖所示,路徑名只在單個文件系統ext3中存在,所以根據目錄拓撲結構,遞歸查找父目錄即可;查找完成後,使用nd.path記錄父目錄log的位置,nd.last記錄了字符串常量”new.txt”。

技術分享圖片

情形2:

在情形2中,路徑名橫跨了兩個文件系統,minix掛載在ext3的tmp目錄上;當成功掛載minix後,設置掛載點tmp為已掛載狀態,並且將tmp指向minix文件系統。

路徑查找程序根據路徑名”/usr/tmp/log/new.txt”查找到tmp目錄時,發現tmp為已掛載狀態,所以從掛載點tmp切換到minix根目錄;然後,確認minix根目錄不是已掛載狀態後,在minix文件系統中繼續查找剩下的路徑名”log/new.txt”;查找完成後,使用nd記錄查找結果。

技術分享圖片

情形3:

在情形3中,ext3的tmp目錄重復掛載了兩個文件系統minix和nfs;當成功掛載minix後,設置掛載點tmp為已掛載狀態;當在同一掛載點tmp掛載nfs時,發現tmp已經是掛載狀態,所以從掛載點tmp切換到minix根目錄,在該根目錄掛載nfs文件系統,並且將minix根目錄設置為已掛載狀態。linux支持在同一掛載點掛載多個文件系統的操作,但是只有最後被掛載的文件系統才是可見的,所以使用ls命令只能看到nfs文件系統的內容。

路徑查找程序根據路徑名”/usr/tmp/log/new.txt”查找到tmp目錄時,發現tmp為已掛載狀態,所以從掛載點tmp切換到minix根目錄;然後發現minix根目錄也是已掛載狀態,所以繼續切換到nfs根目錄;最後,確認nfs根目錄不是已掛載狀態後,在nfs文件系統中繼續查找剩下的路徑名”log/new.txt”;查找完成後,使用nd記錄查找結果。

技術分享圖片

在以上操作中,由__follow_mount_rcu()函數負責目錄掛載狀態檢測和切換操作。

三、新建文件系統調用

do_path_lookup()函數返回的structnameidata *nd數據結構,記錄了待新建文件父目錄的位置,所以根據nd記錄的信息,在VFS樹中新建文件就變得相對簡單了。

由於文件系統的操作比較復雜,因此不對源代碼進行詳解,主要介紹系統調用的主要功能(圖中綠色部分),但是給出了關鍵函數調用路徑,便於讀者查閱細節內容。

3.1、新建常規文件系統調用sys_open()

註:這裏重點介紹sys_open()的新建功能,忽略打開功能(打開功能比較復雜也與主題不符),所以讀者不能片面認為sys_open()的功能只是新建文件。

關鍵函數調用路徑如下:

技術分享圖片

主要功能總結:

1)get_unused_fd_flags()新建文件描述符;

2)do_filp_open()創建常規文件的file結構體、目錄項、inode節點,並將三者關聯起來;

2.1)get_empty_filp()創建常規文件的file結構體;

2.2)lookup_open()調用lookup_dcache()創建常規文件的目錄項目;

2.3)lookup_open()調用vfs_create()創建常規文件的inode節點;

3)fd_install()將文件描述符指向file結構體。

具體操作流程如下圖所示:

技術分享圖片

3.2、新建硬鏈接文件系統調用sys_link()

關鍵函數調用路徑如下:

技術分享圖片

主要功能總結:

1)user_path_at()返回硬鏈接目標文件的目錄項位置;

2)user_path_create()創建硬鏈接文件的目錄項;

2.1)do_path_lookup()返回硬鏈接文件的父目錄位置

2.2)lookup_hash()根據父目錄位置,創建硬鏈接文件的目錄項;

3)vfs_link()將硬鏈接文件指向硬鏈接目標文件的inode節點。

具體操作流程如下圖所示:

技術分享圖片

3.3、新建目錄文件系統調用sys_mkdir()

關鍵函數調用路徑如下:

技術分享圖片

主要功能總結:

1)user_path_create()創建目錄文件的目錄項

1.1)do_path_lookup()返回目錄文件的父目錄位置;

1.2)lookup_hash()根據父目錄位置,創建目錄文件的目錄項;

2)vfs_mkdir()創建目錄文件的inode節點;

具體操作流程如下圖所示:

技術分享圖片

3.4、新建軟鏈接文件系統調用sys_symlink()

關鍵函數調用路徑:

技術分享圖片

主要功能總結:

1)user_path_create()創建軟鏈接文件的父目錄位置;

1.1)do_path_lookup()返回軟鏈接文件的父目錄位置;

1.2)lookup_hash()根據父目錄位置,創建軟鏈接文件的目錄項;

2)vfs_symlink()創建軟鏈接文件的inode節點,並且inode節點記錄了軟鏈接目標文件的位置;簡單來說,軟鏈接文件的內容就是軟鏈接目標文件的路徑。

具體操作流程如下圖所示:

技術分享圖片

3.5、新建特殊文件系統調用sys_mknod()

關鍵函數調用路徑:

技術分享圖片

主要功能總結:

1)user_path_create()創建特殊文件的父目錄位置;

1.1)do_path_lookup()返回特殊文件的父目錄位置;

1.2)lookup_hash()根據父目錄位置,創建特殊文件的目錄項;

2)vfs_mknod()創建特殊文件的inode節點,初始化inode的i_rdev成員,並且將i_fop成員指向默認文件操作;

註:當用戶使用sys_open()打開特殊文件時,會調用默認文件操作的open成員,把設備文件掛接到inode的設備鏈表中,並且將i_fop成員重新指向該設備的文件操作(即設備驅動)。

具體操作流程如下圖所示:

技術分享圖片

通過以上介紹,可以看出快速路徑查找函數do_path_lookup()在新建文件系統調用中起到了基礎性的作用,因此有必要掌握該函數的用法。

四、VFS全景圖

到目前為止,initrd的加載過程就全部結束了;最後,為了更清晰的理解VFS此時的全貌,給出如下VFS全景圖。

從圖中看出以下特點:

1、sysfs文件系統目前還沒有掛載到rootfs的某個掛載點上,後續init程序會把sysfs掛載到rootfs的sys掛載點上;

2、系統打開了/dev/console設備,說明系統已經可以使用該設備,打印信息也可以正常輸出;

3、rootfs文件系統的根目錄下已經準備好了init程序,內核後續會啟動該程序完成剩下的初始化操作。

技術分享圖片

五、總結

目前為止,linux成功加載了initrd文件後,在內核中構建了一個基於內存的文件系統rootfs;VFS再也不是只有根目錄、結構簡單的小樹苗了;現在,它已經成長為擁有目錄、常規文件、鏈接文件、設備文件等多種文件類型、結構復雜的參天大樹了。

linux文件系統 - 初始化(二)