從檔案系統的資料結構看 Linux 核心設計
作者簡介
趙晨雨:西安郵電大學2018級陳莉君教授研究生,天真無邪小白一枚,已經愛上linux核心而不能自拔,正在成長為核心狂熱愛好者 :japanese_ogre:
跟隨陳老師學習linux核心兩個月了,對linux核心產生了極大的興趣,最近學習檔案系統,有一些自己的看法,很榮幸能在linux核心之旅進行分享^_^

本篇文章使用盡量 通俗 的語言來說明linux核心檔案系統中各個資料結構之間的關係,這是一個很複雜的結構關係,在學習這裡的時候一定要和原始碼一一對應起來,否則的話就會陷入迷茫之中。由於linux核心足夠複雜,就會有多種解釋方式,我認為所有關於linux核心的書籍,都是不同作者對核心的不同的看法,說不定這些看法對於linus本人來說都是很巧妙的,所以我在這裡也大膽地提出自己對linux核心設計方式的一些看法。
話不多說,我們上圖:
我使用這張結構圖來進行說明,一共大概有10個結構體,我把它分成三條線來看,在圖中也標記好了,在看每一條線時,我們把它從整體結構中隔離出來看。
第一條線( 綠色 )
這一條線是程序部分,也就是以程序的眼光來看檔案系統。task_struct是一個非常複雜的結構體,我們在這裡只看與檔案系統相關的欄位。從現在開始,把自己當成一個核心設計者,接下來將要介紹的資料結構都是為了給程序提供完好的服務,使得程序可以正常執行。(這裡也可以體會到程序真的是OS的核心)
首先,程序在執行的時候,總歸會使用到檔案,那麼就會用open()和close()兩個函式,此時,就會產生圖中的第1個結構體file,另外程序使用到的檔案是很多的,所以會有很多個file(這裡說明一下,file還有一個很好的作用是併發訪問,不同的使用者在開啟相同檔案的時候都會產生file,這樣就可以實現互不干擾)產生,那麼這個時候就需要對這些file結構體進行管理,按照核心的標準套路,就是使用唯一標號,我們叫它檔案描述符,那麼自然而然地,我們可以想到,這麼多檔案描述符我們怎麼管理呢?再按照核心的標準套路,我們把它又封裝成一個結構體,這個結構體最主要的任務就是把這麼多file結構體進行很有條理的管理,從而方便程序的使用。這個結構體就是圖中第2個files_struct結構體了,看圖中的紅線,一個二級指標指向fd_arry陣列,數組裡存放著一個又一個的file結構體的地址,最重要的我們要讓PCB能夠使用到這個結構體,就放入一個欄位files指向files_struct。
(file結構體之後的dentry結構體部分先保留。)
第二條線( 紅紫色 )
這一條線是純正的檔案系統線,也就是我們現在是檔案系統,我現在需要正確的進入核心。
首先,每一種檔案系統都需要符合VFS的規定和核心的標準套路,我是一個檔案系統,那麼我的條條框框就很多,也就是欄位特別多,為了融入核心,我需要通過圖中第3個結構體file_system_type來進行註冊,換句話說,核心中有多少個檔案系統,就有多少個file_system_type結構體。之後,我是檔案系統,我自然而然就需要使用磁碟來存放檔案了,那麼這個時候有會有很多很多的細節,也就是存放在塊裝置上的管理資訊,這些資訊又有很多,那麼按照核心的設計套路,再次封裝成一個結構體,也就是圖中第4個結構體super_block結構體。我在這裡有一個疑問,就是一個file_system_type和一個super_block,這兩個有什麼區別又有什麼關係呢?因為我覺得只需要一個就夠了。在閱讀大量書籍後,我自己給出的答案是file_system_type是描述這個檔案系統的,而super_block是用來實際管理檔案系統的,二者是不同的作用,就好比註冊完以後,那張登錄檔還有別的作用嗎?
super_block也是一個極其龐大的結構體,它既然處於管理的地位,所以具體的一個個檔案就需要和它進行關聯,按照核心標準套路,一個個具體的檔案在核心中還是抽象成一個結構體,就是圖中的第5個結構體inode結構體,(多說一句,這兩個結構體之間是互聯的,各自有指標指向對方,而且檔案系統這裡的指標真的很精彩!),那麼inode代表了一個個實際的檔案,這時候,自然而然就需要目錄了,核心把目錄也當做一個檔案來處理,同樣抽象成一個結構體,也就是圖中的第6個結構體dentry結構體。
一二條線的交叉部分
這裡的交叉部分很巧妙,我學習檔案系統的時候,是從super_block開始學習的,所以順著下來是inode結構體,但是當時就在 想 , 為什麼不先是dentry目錄,然後目錄下再存放inode呢? 我自己的 答案 是:
-
站在第二條線來想,所謂目錄,是對檔案的劃分,所以得先有檔案,然後才能有目錄。
-
站在第一條線來想,我要開啟一個檔案,這個檔案已經存在了,那麼我就需要從目錄中去找,然後再往下找到inode
綜合兩條線來看,正是因為核心的這種設計方式,我們才會有在開啟一個檔案時先找目錄的習慣。
再次來體會圖中的交叉部分,dentry是很巧妙的。
第三條線( 藍紫色 )
我們直接來看圖中的第7個結構體,vfsmount結構體,這裡先說一下為什麼會有這麼個結構,我們在第二條線中說過了檔案系統註冊時的file_struct_type結構體了,但這個資訊還不夠,我們要正確使用一種檔案系統,就必須mount到根檔案系統的某個目錄上去,記得要root許可權哦,這個時候還是為了管理上的方便,又抽象出來一個結構體vfsmount,它存放的就是檔案安裝的相關資訊,它和PCB之間還需要建立連線,這裡又通過第8個結構體來建立連線,它在2.6原始碼中名字是namespace(2.4中是mnt_namespace)。
從檔案系統來看核心
這裡假設大家已經細讀了核心原始碼,我們可以發現,核心設計的標準套路,就是 抽象、管理、操作 , 抽象 是分為兩種情況,一種是外部檔案的抽象,一種是內部資訊複雜而進行的抽象。那麼是怎麼進行 管理 的呢?舉個例子來說,只要核心中有許多獨立的結構體存在時,我們就通過雙向連結串列,單項鍊表,雜湊表這些方式進行管理。這裡可以體會一下資料結構的精華所在。 操作 的物件就是我們所抽象的一個個資料結構。我們在上面僅僅介紹了這幾個結構的關係,它們還只是資料結構而已,我們還需要操作,就是呼叫核心的API了,這些API的工作就是傳遞資料,從這個結構體中拿出資料放到另外的一個結構體中,所以所謂核心的執行,就是我們抽象出來的資料結構中資料的流動,在我們的腦海裡可以形成一個很壯觀的動態的場面。並且我始終堅信,核心所有解決問題的策略,都可以在我們現實生活中找到影子,畢竟,核心是人寫出來的嘛!
學習核心的方法
這裡推薦一下將核心劃分的學習方法(這種方法在高劍林老師的書中有詳細的介紹)。也就是將核心程式碼分成基礎部分和應用部分(注意這裡是將核心再劃分哦)。我們在學習核心程式碼時,應用部分佔用了大多數,基礎部分的規模並不大,而且各個版本之間改動幅度很小,並且相當的短小精悍。我們可以這樣來看,在使用者臺下,我們的程式要執行,需要呼叫核心的系統函式,那麼我們可以類似的把核心再分成“使用者態”和“核心態”,“使用者態”就是應用部分,“核心態”就是基礎部分,應用部分想要執行,就需要呼叫基礎部分。
按照上面的介紹,基礎部分也是一個小核心,那麼它就需要給外界提供服務,換句話說,就是它的內部也具有自己的資料結構和函式:
-
資料結構:
-
雙向連結串列
-
hash連結串列
-
單向連結串列
-
紅黑樹
-
radix樹
-
服務:
-
核心中使用記憶體
-
核心中的任務排程
-
軟中斷和tasklet
-
工作佇列
-
自旋鎖
-
核心訊號量
-
原子變數
這種方法主要對應前面介紹的核心設計套路中的管理環節,我們可以通過這種方法打破對核心的恐懼感,因為核心無非就是使用這些基礎部分來管理資料,而資料的組織方式在核心中是最複雜的,例如說檔案系統這裡存在大量的全域性連結串列,拿inode_in_use和inode_in_unused來說,我們就會很奇怪,為什麼要有這兩個連結串列?那麼運用這種方法我們就可以這樣來想,核心設計者在設計的時候,遇到了一個實際問題,這個問題一般可以從連結串列的名字看出來,這裡就是遇到了區分inode有沒有使用的問題,那麼自然而然就可以想到,使用基礎部分的各種連結串列來進行管理,因為這些連結串列在核心中就是負責處理這種問題的。所以,我們在學習核心的時候,心中有這些基本部分的概念,再來看核心就是另一種角度了。
由於自己接觸linux核心時間不長,才疏學淺,班門弄斧了,如果有錯誤的地方歡迎大家指正,小趙萬分感謝:-D