文件系統
昨天考完了minix源碼解讀,今天把我的複習資料整合以後加上我自己的理解發出來,因爲複習的時候參考了許多資料,所以文章中很多地方借鑑了其它朋友的東西,不能一一註明,還請見諒。
Minix文件系統的在磁盤中常駐的數據結構有引導塊,超級塊(部分),i節點位圖,區段位圖,i節點。而在只在內存中的數據結構有文件描述符,塊高速緩存,超級塊(部分)。
1. Minix文件系統的物理佈局
MINIX文件系統是一個邏輯的、自包含的實體,它含有i-節點、目錄和數據塊。MINIX文件系統可以存儲在任何塊設備中。MINIX的文件系統都有相同的佈局。如圖所示:
Minix文件系統在磁盤上的物理佈局
引導塊
引導塊中包含有可執行代碼。啓動計算機時,硬件從引導設備將引導塊讀入內存,轉而執行其代碼。引導塊代碼開始操作系統本身的加載過程。一旦系統啓動並加載成功之後,引導塊不再使用。
超級塊
超級塊主要定義一些文件系統的參數。如節點數,區段數,最大文件長度等。Minix文件系統完整的超級塊概念中除了在磁盤中的超級塊外,還有一些數據結構只在內存中。
struct minix_super_block {
__u16 s_ninodes; // i節點的總數
__u16 s_nzones; //16位數據塊總數
__u16 s_imap_blocks; //inode表位圖塊數
__u16 s_zmap_blocks; //數據塊位圖塊數
__u16 s_firstdatazone; //數據塊的起點
__u16 s_log_zone_size; //數據塊長
__u32 s_max_size; //最大文件字節長度
__u16 s_magic; //版本特徵值(魔數)
__u16 s_state; //安裝狀態
__u32 s_zones; //32位數據塊總數
unsigned shorts_block_size; // 塊大小(字節)
chars_disk_version; //文件系統格式子版本號
//下列各項只有載入內存時才使用
struct inode*s_isup; // 指向被掛載的文件系統根目錄的i節點指針
struct inode*s_imount; // 指向掛載到i節點的指針
unsigneds_inodes_per_block; //魔數的預先計算值
dev_ts_dev; // 超級塊所屬的設備
ints_rd_only; // 該位1表示只讀
ints_native; //該位1表示非字節交換
ints_version; // 文件系統版本號,0意味着魔數異常
ints_ndzones; // 直接區段/i節點
ints_nindirs; // 間接區段/間接塊
bit_ts_isearch; // i節點位圖中的第一個區段位
bit_ts_zsearch; // 區段位圖中的第一個空閒位
};
位圖
i節點位圖:i節點位圖來記錄空閒i-節點。
邏輯上,在創建文件時,文件系統必須在位圖塊中逐一地查找第一個空閒i-節點,把它分配給這個新創建的文件。然而,超級塊在內存的拷貝中有一個域指向第一個空閒i-節點,因此不必進行查找,在該空閒i-節點分配使用後,就需要修改指針,使它指向下一個空閒i-節點。這樣就實現了快速查找空閒的i節點。消除了很多順序查找位圖的麻煩。這種策略也使用與對區段位圖的檢索。
區段位圖:磁盤存儲區可以以區段爲單位進行分配,而每個區段可以包含1、2、4、8個,或一般情況下,2n個磁盤塊。區段位圖按區段,記錄空閒存儲區。
i節點
i節點:Minix的i節點幾乎與標準UNIX的節點一模一樣。UNIX的節點採用一級尋址,二級尋址,三級尋址混合的尋址方式。Minix也一樣,有7個直接尋址的指針,2個間接的尋址的指針。
Minix3的i-node訪問時間、修改時間都和標準的UNIX一樣,除了讀操作外,其餘的文件操作都會導致i-node的修改時間變化。
當一個文件被打開時,它的i-node將被找到同時裝入到內存中的i-node表中,並且一直保留到該文件被關閉。在內存中的i-node表中有一些字段是磁盤中的i-node沒有的。比如該i-node所在的設備。每個i-node還有一個計數器,如果同一個文件被多次打開,那麼在內存中只保存一個i-node的副本。當計數器減到0時,該i-node將從內存中刪除,如果在內存中曾被修改過,還要把它寫入磁盤。
i-node的主要作用是給出文件數據塊所在的位置。前7個區段號直接存在i-node中。在Minix發行版中,區段和塊的大小都爲1KB,所以小於7KB的文件可以不必使用間接塊號。當文件的大小超過7KB時,需要使用間接區段。具體方案如圖
在i-node中,Minix只用到了其中的一級間接塊和二級間接塊。如果塊的大小和區段的大小都是1KB,區段號位32位,則一級間接塊含有256項,可以表示256KB的存儲區。二級間接塊指向256個一級間接塊,因此可以訪問長達256個256KB長度的文件。如果塊的大小爲4KB,那麼二級間接塊將指向1024*1024個塊,文件最大長度位4GB。
i-node中含有模式信息,給出了文件的類型和保護標誌位、SETUID位、SETGID位,類型包括普通文件、目錄、快設備文件、字符設備或者管道。i-node中的link字段記錄了多少個目錄項正在指向這個i-node。
inode
EXTERN struct inode {
//磁盤和內存中都有的
mode_t i_mode; /* file type, protection, etc. */
nlink_t i_nlinks; /* how many links to this file */
uid_t i_uid; /*user id of the file's owner */
gid_t i_gid; /*group number */
off_t i_size; /*current file size in bytes */
time_t i_atime; /* time of last access (V2 only) */
time_t i_mtime; /* when was file data last changed */
time_t i_ctime; /* when was inode itself changed (V2 only)*/
zone_t i_zone[V2_NR_TZONES]; /* zone numbersfor direct, ind, and dbl ind */
//只有內存中有的
dev_t i_dev; /*which device is the inode on */
ino_t i_num; /*inode number on its (minor) device */
int i_count; /*# times inode used; 0 means slot is free */
unsigned int i_ndzones; /* # direct zones (Vx_NR_DZONES) */
unsigned int i_nindirs; /* # indirect zones per indirect block */
struct super_block *i_sp; /* pointer to super block for inode's device*/
char i_dirt; /*CLEAN or DIRTY */
zone_t i_zsearch; /* where to start search for new zones */
char i_mountpoint; /* true if mounted on */
char i_seek; /*set on LSEEK, cleared on READ/WRITE */
char i_update; /* the ATIME, CTIME, and MTIME bits are here */
LIST_ENTRY(inode) i_hash; /* hash list */
TAILQ_ENTRY(inode) i_unused; /* free and unused list */
}inode[NR_INODES];
2. Minix文件系統內存中的數據結構
塊高速緩存
MINIX使用塊高速緩存來改進文件系統性能。高速緩存用一個緩衝數組來實現,其中每個緩衝區由包含指針、計數器和標誌的頭以及用於存放磁盤塊的體組成。所有未使用的緩衝區均使用雙鏈表,按最近一次使用時間從近到遠的順序鏈接起來。形成一個LRU鏈。
爲了迅速判斷某一塊是否在內存中,我們使用了哈希表。所有緩衝區,如果它所包含塊的哈希代碼爲k,在哈希表中用第k項指向的單鏈錶鏈接在一起。哈希函數提取塊號低n位作爲哈希代碼,因此不同設備的塊可以出現在同一哈希鏈之中。每個緩衝區都在其中某個鏈中。
在MINIX啓動,初始化文件系統時,所有緩衝區均未使用,所以都在LRU雙向鏈表中,並且也由系統預設全部在哈希表第0項指向的單鏈表中。這時,哈希表的其他項均爲空指針。但是一旦系統啓動完成,系統需要空白緩存塊時,緩衝區將從0號鏈刪除,放到其他鏈中。系統一段時間後,差不多所有的塊都可能被使用過,並隨機地分散在不同的哈希鏈中。
但塊被返回到LRU鏈表中,此塊仍被放在原來的哈希鏈表上,方便系統再次訪問。
Minix文件系統的高速緩存
get_block:獲取一個塊;
put_block:返回之前用get_block請求的塊;
alloc_zone:分配一個新區段;
free_zone:釋放一個區段;
rw_block:在高速緩存和磁盤間傳輸一個塊;
invalidate:清除用於某設備的所有高速緩存塊;
flushall:刷新某設備上所有修改過的塊;
rw_scatterred:在設備上讀或寫分散的塊;
rm_lru:從LRU鏈表中刪除一個塊。
文件系統數據結構
vfs: fproc
EXTERN structfproc {
unsigned fp_flags;
mode_t fp_umask; /* mask set by umask system call */
struct vnode *fp_wd; 進程的工作目錄/* working directory; NULL during reboot */
struct vnode *fp_rd; 進程的根目錄/* root directory; NULL during reboot */
struct filp *fp_filp[OPEN_MAX]; filp是系統文件打開表表項的結構,這裏fp_filp數組保存的是指向該進程打開的文件在系統文件打開表中的指針,在找一個進程打開文件的inode的時候,第一步就是通過該進程的fproc找到filp中的相對位置/* the file descriptor table */
fd_set fp_filp_inuse; 該進程打開的文件描述符/* which fd's are in use? */
uid_t fp_realuid; /* real user id */
uid_t fp_effuid; /* effective user id */
gid_t fp_realgid; /* real group id */
gid_t fp_effgid; /* effective group id */
int fp_ngroups; /* number of supplemental groups */
gid_t fp_sgroups[NGROUPS_MAX];/* supplementalgroups */
dev_t fp_tty; /*major/minor of controlling tty */
int fp_block_fd; /* place to save fd if rd/wr can't finish */
int fp_block_callnr; /* blocked call if rd/wr can't finish */
char *fp_buffer; /* place to save buffer if rd/wr can't finish*/
int fp_nbytes; /* place tosave bytes if rd/wr can't finish */
int fp_cum_io_partial; /* partialbyte count if rd/wr can't finish */
int fp_revived; /* set to indicate process being revived */
endpoint_t fp_task; /* which task is proc suspended on */
int fp_blocked_on; /* what is it blocked on */
endpoint_t fp_ioproc; /* proc no. in suspended-on i/o message */
cp_grant_id_t fp_grant; /* revoke this grant on unsuspend if >-1 */
char fp_sesldr; /* true if proc is a session leader */
char fp_execced; /* true if proc has exec()ced after fork */
pid_t fp_pid; /*process id */
fd_set fp_cloexec_set; /* bit map for POSIX Table 6-2 FD_CLOEXEC */
endpoint_t fp_endpoint; /* kernel endpoint number of this process*/
}fproc[NR_PROCS];
vfs: filp
當一個文件被打開時,就會把一個文件描述符返回給用戶進程,用於後續的read和write調用。Minix採用filp共享表記錄整個系統所用的打開的文件描述符的部分相關信息。進程表中的文件描述符數組中包含了指向這個filp數組元素的指針。
filp是系統管理的所有文件打開表,這個結構中是每個表項中的內容,打開文件的進程不一樣需要佔有不同的表項
EXTERN structfilp {
mode_t filp_mode; 系統打開模式/* RW bits, telling how file is opened */
int filp_flags; /* flags from open and fcntl */
int filp_state; /* state for crash recovery */
int filp_count; 有多少文件描述符/* how many file descriptors share this slot?*/
/* struct inode *filp_ino;*/ /* pointer to the inode */
struct vnode *filp_vno; 指向擁有這個filp結構的打開文件的vnode結構
u64_t filp_pos; /* file position */
/* the following fields are for select() andare owned by the generic
* select() code (i.e., fd-type-specificselect() code can't touch these).
*/
int filp_selectors; /* select()ingprocesses blocking on this fd */
int filp_select_ops; /* interested in these SEL_* operations */
int filp_select_flags; /* Select flags for the filp */
/* following are for fd-type-specificselect() */
int filp_pipe_select_ops;
} filp[NR_FILPS];
vfs:vmnt
EXTERN struct vmnt {
intm_fs_e; 實際文件系統,例如mfs,pfs/*FS process' kernel endpoint */
dev_t m_dev; 子文件系統的設備號/*device number */
intm_flags; 一些標誌位/* mountflags */
struct vnode *m_mounted_on; 父文件系統被掛載的的vnode號/* vnode onwhich the partition is mounted */
struct vnode *m_root_node; 子文件系統的根目錄vnode號/* rootvnode */
char m_label[LABEL_MAX]; /*label of the file system process */
} vmnt[NR_MNTS]; //vmnt表
3. Minix文件系統的功能實現
服務器—客戶模型
Minix文件系統作爲一個服務器程序接受用戶進程或其它服務器進程發來的請求消息,如果用戶想要操作的數據塊就在高速緩存中,則文件系統之間向系統任務發消息請求進行數據操作即可,如圖所示。而如果數據不在高速緩存內,則文件系統先要想磁盤驅動程序發送消息請求調入高速緩存,當數據塊調入後,再想系統任務發送消息進行數據操作,如圖所示。
服務器主程序的實現
main.c
調用get_work函數檢查是否有以前阻塞的進程被喚醒,如果有,這些進程的優先級將高於收到的消息。此時,應該去運行被喚醒的程序。只有當沒有文件系統的內部操作時,get_work纔去調用receive函數去接受消息。獲取消息發送者的進程號和消息的消息號(who和call_nr)。
Main函數的總體框架:
PUBLIC int main()
{
fs_init();
while(TRUE){
get_work(); //get_work接收信息設置全局變量who爲調用者的進程表項號,把call_nr設置爲即將執行的系統調用的編號。
fp=&fproc[who];
//先處理一下特殊的控制消息
if(call_nr==SYS_SIG){
……………..
}else if(call_nr==SYN_ALARM)
……………..
}
…………………………..
//調用內部消息響應函數來處理消息
else{
………………….
error=(*call_vec[call_nr])();
//將消息處理結果發送給用戶程序
reply(who,error);
}
return(ok);
}
系統調用
1. read
以系統調用read爲例,我們先來看文件系統是如何一步一步將調用最終傳遞給底層系統的:
首先要知道的是在MINIX 3中
1.設備靠設備文件描述
2.設備文件是一種特設的i-node,他沒有數據區,也沒有子i-node。
3.在設備文件中一定要有該設備的兩個號:主編號和副編號(Major/Minor id)。主編號用於區分不同的設備,副編號用於向驅動傳遞參數。
調用過程:
user: read
_syscall函數
send_rec陷入內核,內核向VFS發送消息
vfs: main接收到消息
調用do_read()函數。
調用read_write(),首先調用get_filp(m_in.fd)獲得當前文件的文件描述符f,這個f是保存在fproc結構中記錄進程打開文件。在得到f之後,可以得到此文件對應的vnode* vp。vnode參見其數據結構。在這裏要分析傳遞進來的文件描述符所描述文件的種類:即塊設備或字符型設備描述文件還是普通文件。若爲:
管道文件:調用rw_pipe。
字符型設備文件:不使用BufferCache技術,直接使用dev_io調用驅動程序;
塊設備型文件:調用req_breadwrite;
普通文件:調用req_readwrite。
這些函數除了dev_io外,統統都要向具體的文件系統發送消息。向誰發呢,vp->v_bfs_e 中有可以處理這個文件的FS進程endpoint,這個值應當在打開文件時-即調用open時填充。對普通文件而言這個值就是mfs的進程號。
mfs: main接收消息
調用fs_readwrite()。這裏我們的第一項任務就是要通過傳入的inode編號找到具體的inode結構。在mfs中inode結構被保存在了一個哈希表中。接下來,我們根據文件中讀取偏移position,讀取字節數量nrbytes和數據塊的大小blocksize來找到一個個chunk,注意,這個chunk可能是一個block的數據,也可能是比一個block小的數據,但不可能大於一個block。我們先說如何實現的,再說爲什麼要這麼實現。minix中這部分的代碼是這樣的:
off= ((unsigned int) position) % block_size; //讀取位置在block中的offset。注意文件是以塊爲單位存放的。
chunk= min(nrbytes, block_size - off); //每個chunk的大小是讀取字節數和塊大小-讀取位置的offset中小的一個,想想這是爲什麼
if(rw_flag == READING) {
bytes_left= f_size - position; //f_size是文件大小,bytes_left是文件中還有多少沒有讀的字節
if(position >= f_size) break; //讀取偏移超過文件大小
if(chunk > (unsigned int) bytes_left) chunk = bytes_left; //這裏是防止讀的字節超過了文件中剩餘的字節
}
...
nrbytes-= chunk; //還沒讀完的字節數
cum_io+= chunk; //已經讀完的字節數
position+= (off_t) chunk; //讀取位置偏移
調用rw_chunk函數。此函讀取一個Chunk大小的數據到buffer中。MINIX 3 使用了buffercache的技術,即按LRU的方式對數據進行緩存。
調用read_map,確定該文件的目前偏移在哪個block
調用rahead
調用get_block函數:
此函數首先嚐試從Buffer中讀取
如果沒有再調用rw_block從磁盤讀取(同時讀到BufferCache中)。
rw_block調用block_dev_io對磁盤進行I/O操作。此函數根據dev編號查找數組driver_endpoint:這是一個按主設備編號排序的存儲相應驅動程序進程號的,他應當在mount 一個系統時就應該確定好。在得到驅動程序的endpoint編號後向其發送請求讀消息。
2. 路徑名解析
vfs:eat_path()
advance()
lookup():
req_lookup()//如果沒有mount,一次解析到底。如果有mount,文件系統會在發生mount的目錄的inode中發現這個掛載標誌位被置上了(if (rip->i_mountpoint) return(EENTERMOUNT);),於是它停止繼續解析。而是到掛載在這個目錄的子文件系統的根目錄inode上繼續尋找,它怎麼找到子文件系統的根目錄inode呢。
我們首先進入下面的循環,這是在有mount的情況下一次解析不成功纔會進入的循環
while(r == EENTERMOUNT || r == ELEAVEMOUNT || r == ESYMLINK) {
//下面的for循環,實際上是搜索vmnt表,如果發現表中有一項父文件系統被掛載的目錄的inode等於我們上次解析停止的地方的那個目錄的res.inode_nr,我們就認爲這個vmnt表項就是這個掛載的記錄,然後我們取出vmnt表項中的另一個指針:指向子文件系統的根目錄的m_root_node指針,然後停止搜索。
for(vmp = &vmnt[0]; vmp != &vmnt[NR_MNTS]; ++vmp) {
if(vmp->m_dev != NO_DEV && vmp->m_mounted_on) {
if(vmp->m_mounted_on->v_inode_nr == res.inode_nr &&
vmp->m_mounted_on->v_fs_e== res.fs_e) {
dir_vp= vmp->m_root_node;
break;
}
}
//下述代碼的意思是,我們設置子文件系統的對應的實際文件系統的endpoint和根目錄的inode號,以便接下來在子文件系統中進行解析
fs_e= dir_vp->v_fs_e;
dir_ino= dir_vp->v_inode_nr;
req_lookup()//繼續解析子文件系統的路徑名
}
}
fs_sendrec()//vfs實際上沒有進行路徑名解析,它只是解決的mount節點要跳轉的問題,實際的路徑名解析是在mfs中進行的
mfs:fs_lookup
sys_safecopyfrom() //複製路徑名,設置調用者id
parse_path() //查找inode
這部分見課件minix5P34
advance()
search_dir()//字符串匹配,搜索解析目錄得到的字符串對應的文件是否在當前目錄下
read_map()查找當前目錄的物理塊號
get_block()獲得當前目錄的緩存塊
//在緩存塊中搜索匹配的目錄項,找到的匹配目錄項的inode存放在numb中
put_block()//釋放緩存塊
get_inode()//如果找到了就獲得指向這個文件mfs中inode的指針
3. OPEN
do_open()(vfs/open.c)
fetch_name() //獲得路徑名
//檢查路徑名長度
//通過消息或調用sys_datacopy()把路徑名寫入user_fullpath
common_open()
首先是調用get_fd獲取新的文件描述符;
查看fproc中文件描述符信息,是否有空餘文件描述符
獲得空的filp slot
如果OPEN調用可以是以CREATE模式打開,即O_CREATE位被置上,那麼我們要使用new_node()創建一個文件
調用last_dir()查看目錄正確性
解析user_fullpath中的目錄
調用advance()將路徑解析爲vnode
查看權限,調用forbidden()
調用req_create()創建inode
創建文件
2.否則使用eat_path函數進行路徑解析。在filp這個描述系統打開文件的數組裏加入這個文件的描述符,返回fd.
4.Mount
首先從理論上了解一下mount的機理
當我們鍵入mount /dev/c0d1p2/usr的時候,存放硬盤1的第2個分區上的文件系統將被掛載到根文件系統下的/usr/目錄下。
掛載完成後,/usr目錄的在內存中的inode結構中會置上一個標誌位,表明/usr已經被掛載了。mount會在vmnt中添加一個表項記錄本次mount。這個表項裏有兩個指針,分別指向父文件系統被掛載的的vnode號和子文件系統的根目錄vnode號。
vfs: do_mount()
mount_fs()
req_mountpoint()
fs_sendrec()
mfs: fs_mountpoint() //如果可以mount,那麼被mount的inode結構中i_mountpoint被置爲真