對文件系統的理解


目錄

如果沒有文件系統

         如何讀寫文件

         提煉上述過程中我們需要知道的信息

文件系統的實現

         需要在硬盤上保存的信息

         代碼上實現的邏輯

                  設備號

                  分區信息

                  file結構體

                  inode保存的信息

如果有文件系統

         讀寫接口

         讀寫流程

         TASK_FS


如果沒有文件系統

  如果我們不在硬盤本身建立文件系統,我們直接面對硬盤的扇區。

如何讀寫文件  

先看看對於操作普通文件來說,意味着什么。

  我們要拿着一個小本本,上面記着,文件名,文件所在扇區以及文件大小。每次要讀寫文件,我們要人工查詢這個賬本,知道我們要的文件在哪里。如果文件A所在的扇區M已經寫滿了,隨后的一個扇區M+1被文件B占用了,我們還想接着寫文件A,怎么辦呢?只能從其他地方找一個空閑扇區N,然后在賬本上把N記錄到文件A占用的扇區項中。

  我們如何知道硬盤上還有哪些空間可以用呢?難道每次都從前往后把扇區使用情況計算一遍嗎嗎?可能還需要另起一個賬本記錄扇區使用情況,刪除文件,我們把對應的扇區標記為空閑,如果創建文件,把對應的扇區標記為不能使用。

對於操作系統而言呢?我覺得,沒有文件系統就不會有操作系統,這樣的操作系統充其量就是一個硬盤驅動。為什么?可以設想一下創建文件的過程:

  1. 用戶告訴這樣的操作系統,說要創建一個文件A
  2. 計算機輸出,請你自己記錄好文件名,並告訴我要在哪個扇區創建。並且記錄好這個文件你占用了哪些扇區

我不能忍受。。。

提煉上述過程中我們需要的信息

將變化的放在一起,將不變的放在一起。統一才有美感。

dir_entry

  對於文件使用情況的賬本而言,看起來要表述一個文件在硬盤上的信息,我們需要知道它占用了哪些扇區,它的名字,文件大小這樣的信息。那么這些信息應該放在哪里呢?當然可以隨機存放,但是存放完了,計算機如何在下次使用的時候找到這個文件呢?還是需要一份記錄來索引這些信息,還不如把這些文件信息按照統一格式存放在一起,這就是目錄結構(dir_entry)的由來。按照樹狀目錄能得到任何文件信息。

sector_map

對於硬盤使用情況的賬本而言,要記錄好哪些扇區空閑,哪些已經被使用了。這就是sector_map的由來。

super_block

那么這些賬本本身是存在於硬盤的某些地方,還需要一個總賬本來記錄這些管理塊的信息,這個總賬本就是super_block。

inode

那么inode的由來呢?為什么文件名和inode分開存放呢?

  試想一下,如果文件名和文件的屬性信息一起存放的話,一個文件目錄項會占用很大的空間,一個扇區也許只能存幾個文件的信息,而系統在查找文件的時候,可能要讀很多次扇區才能找到需要的文件,這樣大大影響系統的效率。畢竟我們在找文件的時候,不需要文件的信息,不需要知道文件大小、所在扇區等等信息全部與查找無關,為什么要這些信息來影響我們的速度呢?我們只要文件名來判斷這是不是我們要找的文件。所以將文件的其余信息剝離出來概括為inode結構體。

inode_array

  inode單獨列出來了,存放在哪里呢?如何通過dir_entry找到inode呢?當然可以存放於任何扇區上,只不過dir_entry可能要加上inode所在扇區和在扇區中的偏移兩個字段了,隨之而來問題就是存放inode的扇區只能用來存放其他inode而不能用來存放文件數據了,因為我們給文件分配空間是按照扇區為單位的,難道一個扇區分給文件時候,還要記上一筆在偏移offset處是inode占用的,讀寫的時候請跳過,這樣的邏輯恐怕沒人會去用代碼實現它吧。另外,由於“存放inode的扇區只能用來存放其他inode而不能用來存放文件數據”這樣的原因,設計者就折中了一下,把indoe占用的扇區都提到一個單獨空間,以后所有的inode都放到這個空間里,這個空間就是inode_array。

  當然也會出現問題,可能inode_array滿了,而硬盤空間還要很大剩余;或者硬盤空間嘛呢,inode_array還有很多剩余。這是很極端的情況,總要有不盡人意的地方,那就把這個不足最小化吧。

  在存放inode的時候,怎么知道inode_array中哪個下標可以用呢?這就是又需要一份記錄,來記錄inode_array中哪些是空閑的,哪些是已經使用,這個記錄就是inode_map。而inode在inode_array中的下標就是inode_num。dir_entry中記錄了這個inode_num就可以在inode_array中找到對應的文件信息了。這個過程銜接的太美妙了。


文件系統的實現

文件系統需要的結構體大概都知道了,剩下的僅僅是需要規划處具體的結構體了。我們來看看。

需要在硬盤上保存的信息

    超級塊

    Inode-map

    Sector-map

    Inode-array

    上面幾個結構體作者在書中都列舉出來了,都是很好理解的。我不啰嗦再搬運過來了。

代碼上實現的邏輯

設備號

  正是在作者的講解下,我算是真正的了解到設備號的意義。以前總是看書上說主設備號代表設備歸屬於哪個驅動,子設備號真正表明是哪個具體的設備。我雖然能順着設備號找到驅動,能從驅動中看到子設備號對流程的分用作用,但是感覺總是欠缺點什么。我就好奇為什么linux 0.12中將0x300就能代表第一塊硬盤,難道不能是0x400嗎?為什么0代表整個硬盤,1代表第一個分區?分區編號要按照物理分區順序嗎?如果是0x400會產生什么影響呢?

  跟着作者一起學着規划硬盤空間,才漸漸明白,這些編號可以隨意編,跟硬盤上的分區順序不存在某種必然的聯系,只是最后落實到保存硬盤信息的結構體上的時候,不會出現偏差就可以了。

  對於操作系統而言,每個分區都被當做一個獨立的設備對待。看看書中所描述的硬盤信息結構體。

struct part_info {
    u32    base;    /* # of start sector (NOT byte offset, but SECTOR) */
    u32    size;    /* how many sectors in this partition */
};

/* main drive struct, one entry per drive */
struct hd_info
{
    int            open_cnt;
    struct part_info    primary[NR_PRIM_PER_DRIVE];//計算后NR_PRIM_PER_DRIVE = 5
    struct part_info    logical[NR_SUB_PER_DRIVE];// 計算后NR_SUB_PER_DRIVE = 64
};

  由結構體可以看出來,硬盤上存在的每個分區都會被記錄下來。

  書中根設備編號是0x322,可以知道子設備號是0x22,一開始很困惑,這么大的子設備號,難道要分0x22個分區?或者說系統怎么就知道0x22表示的是根分區呢?

  還得再看一段代碼:

logidx = (p->DEVICE - MINOR_hd1a) % NR_SUB_PER_DRIVE;
sect_nr += p->DEVICE < MAX_PRIM ?
        hd_info[drive].primary[p->DEVICE].base :
        hd_info[drive].logical[logidx].base;

  先將設備號減去第一個邏輯設備的編號得到設備號在logical數組的下標。當然,可能這個設備號不是邏輯設備,而是主分區。沒關系,下一步判斷p->DEVICE 是不是小於MAX_PRIM,如果小於,說明是主分區,直接用p->DEVICE在primary數組中取值就可以了。

  原來是這樣,你想怎么樣編號就怎么樣編號,只要你自己能找到映射關系就可以了。

分區信息

  硬盤的管理結構體已經設計好了,那么如何獲取硬盤的分區信息呢?見硬盤驅動那篇總結。

文件描述符

  內存中的文件如何和硬盤中的文件聯系起來?當我們打開一個文件后,后續的操作,如何來標示我們操作的是一個文件而不是一段莫名其妙的內存呢?

  首先,我們會想到將inode讀到內存就好了,我們就知道文件的所有信息了。那文件名呢?好像文件名除了查找匹配能貢獻一份力量,其它地方用不着啊,難道也一些讀進來嗎?僅僅是做個標識而已,用一個數不是更好、更簡單嗎?這就是文件描述符的作用。那文件描述符放在哪里呢?由於每個進程打開的文件不同,打開同一個文件的次序不同,那么文件描述符一般情況下也就不能作為進程共享的資源了(當然,域套接字是可以的,內核社區的人員一次又一次地刷新人們的理解力)。既然如此,文件描述符最好是進程私有的了,就只能放在進程表(也就是進程控制塊)里面了。此外,機器資源有限,總不能讓一個進程無限制的打開文件,最好大家都沒內存了,只能歇菜了。所以,一個進程打開的文件數是有限制的,目前我們只給20個就好了。

file結構體

  好像有了文件描述符就可以直接和inode關聯起來了,沒必要中間再加一層file結構體啊。我想是因為要以比較節約的方式共享文件吧,節約什么呢,除了內存還能有誰能讓那些設計師精益求精呢?  

  1. 我們當然可以在每個進程控制塊里面分配20個存放文件信息的結構體,存放讀寫偏移指針、打開的權限、inode指針等等信息。但是能保證進程會長時間打開20個文件嗎?如果不能保證,那不就浪費了。如果以后允許打開100個文件呢?難道進程控制塊也要隨之而增大嗎?
  2. 關於共享文件,父子進程通過一個放在描述符數組里面的指針共享一個file結構體,而不用在單獨維護一個file時候還要考慮同步。設想這樣一個情形:父子進程都對一個文件進行寫操作,父進程寫了10個字符,按照需求該子進程接着寫10個字符了,如果是父子進程單獨維護file結構體,那么實際上只有子進程寫了10個字符,父進程寫的10個字符被覆蓋了。如果共享呢?file中的pos每次操作對於兩個進程而言都是同步的(當然這個例子不太嚴謹,它本身就存在同步問題,但是僅僅用來說明一點問題還是可以的)。

inode保存的信息

  為什么不用inode本身當做系統或者進程操作文件的接口呢?這個問題比較好考慮。多個進程操作同一個文件,那讀寫指針的值肯定不一樣,讀寫方式也不一樣,其實這些不一樣的地方提煉出來就是file結構體啦,file結構體的內容也不是隨意產生的。

  將變化的放在一起,將不變的放在一起。


如果有文件系統

再接下來考慮一下如果有文件系統能給我們帶來什么好處呢?

讀寫接口

不過,首先還是要實現讀寫接口的,就套用linux慣用的讀寫接口就好了。

int read(int fd, void *buf, int count);

只不過linux是通過中斷調用來和內核交互,咱們是通過給TASK_FS發送消息並同步等待來實現的。

讀寫流程

  1. 那么如果一個用戶進程A請求讀寫一個文件X,那么A會向TASK_FS進程發送消息,告訴FS文件名和讀寫模式。
  2. 功能完備的文件系統還要考慮很多因素,諸如做下判斷,看看文件路徑是相對於當前目錄還是根目錄。我們比較簡單,全部按照根目錄實現,而且不支持多級目錄,所有文件都放在根目錄中。
  3. TASK_FS會給TASK_HD發消息,把目錄區讀給我。然后逐一比較有沒有相同的文件名,假設有同名的,根據dir_entry中記錄的inode_num算一下文件indoe所在的扇區是多少,然后再給TASK_HD發消息,把inode所在扇區讀進來,文件具體的信息就有了。
  4. 后續操作這個文件,TASK_HD根據進程控制塊中的信息來計算和決定該怎么操作文件數據。比如說根據文件描述找到file結構體,里面有讀寫指針知道下一步要操作的位置是哪里,通過file結構體找到inode,這樣就知道文件數據在哪個扇區了。

通過上面簡單的敘述,也可以窺見現代文件系統問什么加入了dentry這個成員,目錄項在查找的時候也是經常用到的,還不如緩存在內存中,加快讀寫速度。

TASK_FS

  TASK_FS在微內核的設計中,被設計為一個進程了,它不斷地循環讀取其它進程發給它的讀寫請求,但是一次只能處理一個請求,如果這個請求沒有完成,那么其它進程只能掛接在TASK_FS的等待隊列上等待了。不過沒關系,過早的優化是萬惡之源。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM