XV6操做系統代碼閱讀心得(五):文件系統

Unix文件系統

當今的Unix文件系統(Unix File System, UFS)起源於Berkeley Fast File System。和全部的文件系統同樣,Unix文件系統是以塊(Block)爲單位對磁盤進行讀寫的。通常而言,一個塊的大小爲512Byte或者4KB。文件系統的全部數據結構都以塊爲單位存儲在硬盤上,一些典型的數據塊包括:superblock, inode, data block, directory block and indirection block。node

Superblock包含了關於整個文件系統的元信息(metadata),好比文件系統的類型、大小、狀態和關於其餘文件系統數據結構的信息。Superblock對文件系統是很是重要的,所以Unix文件系統的實現會保存多個Superblock的副本。數組

inode是Unix文件系統中用於表示文件的抽象數據結構。inode不只是指抽象了一組硬盤上的數據的"文件」,目錄和外部IO設備等也會用inode數據結構來表示。inode包含了一個文件的元信息,好比擁有者、訪問權限、文件類型等等。對於一個文件系統裏的全部文件,文件系統會維護一個inode列表,這個列表可能會佔據一個或者多個磁盤塊。緩存

Data block用於存儲實際的文件數據。一些文件系統中可能會存在用於存放目錄的Directory Block和Indirection Block,可是在Unix文件系統中這些文件塊都被視爲數據,上層文件系統經過inode對其加以操做,他們惟一的區別是inode裏記錄的屬性有所不一樣。數據結構

Xv6中的文件系統設計思想與Unix大抵相同,可是實現細節多有簡化。在底層實現上,Xv6採用與Linux相似的分層實現思路,層層向上逐級封裝,以便能支持多種多樣的設備和IO方式。Xv6的文件系統包含了磁盤IO層、Log層、Inode層、File層和系統調用層,下面會依次介紹其實現,併發

Xv6中的磁盤IO

Xv6中的磁盤IO在ide.c中,這是一個基於Programmed IO的面向IDE磁盤的簡單實現。一個Xv6中的磁盤讀寫請求用以下的數據結構表示ide

struct buf {
  int flags;
  uint dev;
  uint blockno;
  struct sleeplock lock;
  uint refcnt;
  struct buf *prev; // LRU cache list
  struct buf *next;
  struct buf *qnext; // disk queue
  uchar data[BSIZE];
};

其中,對IDE磁盤而言,須要關心的域是flags(DIRTY, VALID),dev(設備),blockno(磁盤塊編號)和next(指向隊列的下一個成員的指針).函數

磁盤讀寫實現的思路是這樣的:Xv6會維護一個進程請求磁盤操做的隊列(idequeue)。當進程請求磁盤讀寫時,請求會被加入隊列,進程會進入睡眠狀態(iderw())。任什麼時候候,隊列的開頭表示當前正在進行的磁盤讀寫請求。當一個磁盤讀寫操做完成時,會觸發一箇中斷,中斷處理程序(ideintr())會移除隊列開頭的請求,喚醒隊列開頭請求所對應的進程。若是還有後續的請求,就會將其移到隊列開頭,開始處理下一個磁盤請求。佈局

磁盤請求隊列的聲明以下,固然對其訪問是必須加鎖的。ui

static struct spinlock idelock;
static struct buf *idequeue;

ide.c中函數及其對應功能以下操作系統

函數名 功能
idewait() 等待磁盤進入空閒狀態
ideinit() 初始化IDE磁盤IO
idestart() 開始一個磁盤讀寫請求
iderw() 上層文件系統調用的磁盤IO接口
ideintr() 當磁盤請求完成後中斷處理程序會調用的函數

操做系統啓動時,main()函數會調用ideinit()ide磁盤進行初始化,初始化函數中會初始化ide鎖,設定磁盤中斷控制,並檢查是否存在第二個磁盤。

iderw()函數提供了面向頂層文件系統模塊的接口。iderw()既可用於讀,也可用於寫,只需經過判斷buf->flag裏的DIRTY位和VALID位就能判斷出請求是讀仍是寫。若是請求隊列爲空,證實當前磁盤不是工做狀態,那麼就須要調用idestart()函數初始化磁盤請求隊列,並設置中斷。若是請求是個寫請求,那麼idestart()中會向磁盤發出寫出數據的指令。以後,iderw()會將調用者陷入睡眠狀態。

當磁盤讀取或者寫操做完畢時,會觸發中斷進入trap.c中的trap()函數,trap()函數會調用ideintr()函數處理磁盤相關的中斷。在ideintr()函數中,若是當前請求是讀請求,就讀取目前已經在磁盤緩衝區中準備好的數據。最後,ideintr()會喚醒正在睡眠等待當前請求的進程,若是隊列裏還有請求,就調用idestart()來處理新的請求。

Buffer Cache的功能與實現

在文件系統中,Buffer Cache擔任了一個磁盤與內存文件系統交互的中間層。因爲對磁盤的讀取是很是緩慢的,所以將最近常常訪問的磁盤塊緩存在內存裏是頗有益處的。

Xv6中Buffer Cache的實如今bio.c中,Buffer Cache的數據結構以下(rev11版本)

struct {
  struct spinlock lock;
  struct buf buf[NBUF];
  // Linked list of all buffers, through prev/next.
  // head.next is most recently used.
  struct buf head;
} bcache;

此數據結構在固定長度的數組上維護了一個由struct buf組成的雙向鏈表,而且用一個鎖來保護對Buffer Cache鏈表結構的訪問。值得注意的是,對鏈表結構的訪問和對一個struct buf結構的訪問須要的是不一樣的鎖。

在緩存初始化時,系統調用binit()對緩存進行初始化。binit()函數對緩存內每一個元素初始化了睡眠鎖,並從後往前鏈接成一個雙向鏈表。一開始的時候,緩存內全部的塊都是空的。

上層文件系統使用bread()bwrite()對緩存中的磁盤塊進行讀寫。關於緩存的所有操做是在bread()bwrite()中自動完成的,不須要上層文件系統的參與。

bread()會首先調用bget()函數,bget()函數會檢查請求的磁盤塊是否在緩存中。若是在緩存中,那麼直接返回緩存中對應的磁盤塊便可。若是不在緩存中,那麼須要先使用最底層的iderw()函數先將此磁盤塊從磁盤加載進緩存中,再返回此磁盤塊。

bget()函數的實現有一些Tricky。搜索緩存塊的代碼很是直接,可是在其中必須仔細考慮多進程同時訪問磁盤塊時的同步機制。在Xv6 rev7版本中因爲沒有實現睡眠鎖,爲了不等待的緩衝區在等待的過程當中改變了內容,必須在從鎖中醒來時從新掃描磁盤緩衝區尋找合適的磁盤塊,可是在rev11版本中因爲實現了睡眠鎖,在找到對應的緩存塊時,只需釋放對Buffer Cache的鎖並拿到與當前緩存塊有關的睡眠鎖便可。

bwrite()函數直接將緩存中的數據寫入磁盤。Buffer Cache層不會嘗試執行任何延遲寫入的操做,什麼時候調用bwrite()寫入磁盤是由上層的文件系統控制的。

上層文件系統調用brelse()函數來釋放一塊再也不使用的衝區。brelse()函數中主要涉及的是對雙向鏈表的操做,在此再也不贅述。

Log層的功能與實現

在文件系統中添加Log層是爲了可以使得文件系統可以處理諸如系統斷電之類的異常狀況,避免磁盤上的文件系統出現Inconsistency。Log層的實現思路是這樣的,對於上層文件系統的所有磁盤操做,將其分割爲一個個transaction,每一個transaction都會首先將數據和其對應磁盤號寫入磁盤上的Log區域,而且只有在Log區域寫入所有完成後,再將Log區域的數據寫入真正存儲的數據區域。經過這種設計,若是在寫入Log的時候斷電,那麼文件系統會當作這些寫入不存在,若是在寫入真正區域的時候斷電,那麼Log區域的數據能夠用於恢復文件系統。如此,就能夠避免文件系統中文件的損壞。

在Xv6 rev7的文件系統實現中,不容許多個進程併發地向Log層執行transaction,然而rev11的實現有所不一樣,容許多個進程併發地向Log層執行transaction。如下對實現細節的討論基於rev11版本。

上層文件系統在使用log層時,必須首先調用begin_op()函數。begin_op()函數會記錄一個新的transaction信息。在使用完log層後,上層系統必須調用end_op()函數。只有當沒有transaction在執行時,log纔會執行真正的磁盤寫入。真正的磁盤寫入操做在commit()函數中,能夠看到commit()函數只有在end_op()結束,log.outstanding==0時纔會被調用(以及開機的時刻)。commit()函數會先調用write_log()函數將緩存裏的磁盤塊寫到磁盤上的Log區域裏,並將Log Header寫入到磁盤區域。只有當磁盤裏存在Log Header的區域數據更新了,這一次Log更新纔算完成。在Log區域更新後,commit()函數調用install_trans()完成真正的磁盤寫入步驟,在這以後調用write_head()函數清空當前的Log數據。

XV6 文件系統的硬盤佈局

在Xv6操做系統的硬盤中,依次存放了以下幾個硬盤塊。對這些硬盤塊的索引是直接使用一個整數來進行的,

[boot block | super block | log | inode blocks | free bit map | data blocks]

第一個硬盤塊boot block會在開機的時候被加載進內存,磁盤塊編號是0。第二個superblock佔據了一個硬盤塊,編號是1,在Xv6中的聲明以下

struct superblock {
  uint size;         // Size of file system image (blocks)
  uint nblocks;      // Number of data blocks
  uint ninodes;      // Number of inodes.
  uint nlog;         // Number of log blocks
  uint logstart;     // Block number of first log block
  uint inodestart;   // Block number of first inode block
  uint bmapstart;    // Block number of first free map block
};

Superblock中存儲了文件系統有關的元信息。操做系統必須先讀入Super Block才知道剩下的log塊,inode塊,bitmap塊和datablock塊的大小和位置。在Superblock以後順序存儲了多個log塊、多個inode塊、多個bitmap塊。磁盤剩餘的部分存儲了data block塊。

XV6中的文件

Xv6中的文件(包括目錄)所有用inode數據結構加以表示,全部文件的inode都會被存儲在磁盤上。系統和進程須要使用某個inode時,這個inode會被加載到inode緩存裏。存儲在內存裏的inode會比存儲在磁盤上的inode多一些運行時信息。內存裏的inode數據結構聲明以下。

// in-memory copy of an inode
struct inode {
  uint dev;           // Device number
  uint inum;          // Inode number
  int ref;            // Reference count
  struct sleeplock lock; // protects everything below here
  int valid;          // inode has been read from disk?

  short type;         // copy of disk inode
  short major;
  short minor;
  short nlink;
  uint size;
  uint addrs[NDIRECT+1];
};

其中,inode.type指明瞭這個文件的類型。Xv6中,這個類型能夠是普通文件,目錄,或者是特殊文件。

內核會在內存中維護一個inode緩存,緩存的數據結構聲明以下

struct {
  struct spinlock lock;
  struct inode inode[NINODE];
} icache;

對於Inode節點的基本操做以下

函數名 功能
iinit() 讀取Superblock,初始化inode相關的鎖
ialloc() 在磁盤上分配一個inode
iupdate() 將內存裏的一個inode寫入磁盤
iget() 獲取指定inode,更新緩存
iput() 對內存內一個Inode引用減1,引用爲0則釋放inode
ilock() 獲取指定inode的鎖
iunlock() 釋放指定inode的鎖
readi() 往inode讀數據
writei() 往inode寫數據
bmap() 返回inode的第n個數據塊的磁盤地址

一個Inode有12(NDIRECT)個直接映射的磁盤塊,有128個間接映射的磁盤塊,這些合計起來,Xv6系統支持的最大文件大小爲140*512B=70KB。

Xv6系統中的文件描述符

Unix系統一個著名的設計哲學就是"Everything is a file",這句話更準確地說是"Everything is a file descriptor"。上文所提的inode數據結構用於抽象文件系統中的文件和目錄,而文件描述符除了抽象文件以外,還能抽象包含Pipe、Socket之類的其餘IO,成爲了一種通用的I/O接口。

Xv6中,一個文件的數據結構表示以下

struct file {
  enum { FD_NONE, FD_PIPE, FD_INODE } type;
  int ref; // reference count
  char readable;
  char writable;
  struct pipe *pipe;
  struct inode *ip;
  uint off;
};

從中可見,一個file數據結構既能夠表示一個inode,也能夠表示一個pipe。多個file數據結構能夠抽象同一個inode,可是Offset能夠不一樣。

系統全部的打開文件都在全局文件描述符表ftable中,ftable數據結構的聲明以下

struct {
  struct spinlock lock;
  struct file file[NFILE];
} ftable;

從中能夠看出Xv6最多支持同時打開100(NFILE)個文件,從struct proc中能夠看出Xv6中每一個進程最多同時能夠打開16(NOFILE)個文件。

對File數據結構的基本操做包括filealloc(), filedup(), fileclose(), fileread(), filewrite()filestat()。命名風格與Unix提供的接口一致,所以從名字很容易就能看出其基本功能。

對於Inode類型的file而言,上述操做的實現依賴於inode的諸如iread()iwrite()等基本操做。

Xv6中文件相關的系統調用

利用上一層的實現,大多數系統調用的實現都是比較直接的。Xv6中支持的文件相關係統調用列表以下

名稱 功能
sys_link() 爲已有的inode建立一個新的名字
sys_unlink() 爲已有的inode移除一個名字,可能會移除這個inode
sys_open() 打開一個指定的文件描述符
sys_mkdir() 建立一個新目錄
sys_mknod() 建立一個新文件
sys_chdir() 改變進程當前目錄
sys_fstat() 改變文件統計信息
sys_read() 讀文件描述符
sys_write() 寫文件描述符
sys_dup() 增長文件描述符的引用

絕大多數系統調用的語義都與Unix標準相同。

相關文章
相關標籤/搜索