目錄
如果沒有文件系統
如何讀寫文件
提煉上述過程中我們需要知道的信息
文件系統的實現
需要在硬盤上保存的信息
代碼上實現的邏輯
設備號
分區信息
file結構體
inode保存的信息
如果有文件系統
讀寫接口
讀寫流程
TASK_FS
如果沒有文件系統
如果我們不在硬盤本身建立文件系統,我們直接面對硬盤的扇區。
如何讀寫文件
先看看對於操作普通文件來說,意味着什么。
我們要拿着一個小本本,上面記着,文件名,文件所在扇區以及文件大小。每次要讀寫文件,我們要人工查詢這個賬本,知道我們要的文件在哪里。如果文件A所在的扇區M已經寫滿了,隨后的一個扇區M+1被文件B占用了,我們還想接着寫文件A,怎么辦呢?只能從其他地方找一個空閑扇區N,然后在賬本上把N記錄到文件A占用的扇區項中。
我們如何知道硬盤上還有哪些空間可以用呢?難道每次都從前往后把扇區使用情況計算一遍嗎嗎?可能還需要另起一個賬本記錄扇區使用情況,刪除文件,我們把對應的扇區標記為空閑,如果創建文件,把對應的扇區標記為不能使用。
對於操作系統而言呢?我覺得,沒有文件系統就不會有操作系統,這樣的操作系統充其量就是一個硬盤驅動。為什么?可以設想一下創建文件的過程:
- 用戶告訴這樣的操作系統,說要創建一個文件A
- 計算機輸出,請你自己記錄好文件名,並告訴我要在哪個扇區創建。並且記錄好這個文件你占用了哪些扇區
我不能忍受。。。
提煉上述過程中我們需要的信息
將變化的放在一起,將不變的放在一起。統一才有美感。
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結構體啊。我想是因為要以比較節約的方式共享文件吧,節約什么呢,除了內存還能有誰能讓那些設計師精益求精呢?
- 我們當然可以在每個進程控制塊里面分配20個存放文件信息的結構體,存放讀寫偏移指針、打開的權限、inode指針等等信息。但是能保證進程會長時間打開20個文件嗎?如果不能保證,那不就浪費了。如果以后允許打開100個文件呢?難道進程控制塊也要隨之而增大嗎?
- 關於共享文件,父子進程通過一個放在描述符數組里面的指針共享一個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發送消息並同步等待來實現的。
讀寫流程
- 那么如果一個用戶進程A請求讀寫一個文件X,那么A會向TASK_FS進程發送消息,告訴FS文件名和讀寫模式。
- 功能完備的文件系統還要考慮很多因素,諸如做下判斷,看看文件路徑是相對於當前目錄還是根目錄。我們比較簡單,全部按照根目錄實現,而且不支持多級目錄,所有文件都放在根目錄中。
- TASK_FS會給TASK_HD發消息,把目錄區讀給我。然后逐一比較有沒有相同的文件名,假設有同名的,根據dir_entry中記錄的inode_num算一下文件indoe所在的扇區是多少,然后再給TASK_HD發消息,把inode所在扇區讀進來,文件具體的信息就有了。
- 后續操作這個文件,TASK_HD根據進程控制塊中的信息來計算和決定該怎么操作文件數據。比如說根據文件描述找到file結構體,里面有讀寫指針知道下一步要操作的位置是哪里,通過file結構體找到inode,這樣就知道文件數據在哪個扇區了。
通過上面簡單的敘述,也可以窺見現代文件系統問什么加入了dentry這個成員,目錄項在查找的時候也是經常用到的,還不如緩存在內存中,加快讀寫速度。
TASK_FS
TASK_FS在微內核的設計中,被設計為一個進程了,它不斷地循環讀取其它進程發給它的讀寫請求,但是一次只能處理一個請求,如果這個請求沒有完成,那么其它進程只能掛接在TASK_FS的等待隊列上等待了。不過沒關系,過早的優化是萬惡之源。