1. 程式人生 > >深入理解Lustre檔案系統-第3篇 lustre lite

深入理解Lustre檔案系統-第3篇 lustre lite

在file結構體中定義的另外一個欄位是f_dentry,它指向一個儲存在dentry cache(即所謂dcache)中的dentry物件(struct dentry)。實質上,VFS在檔案和資料夾將被首次訪問的時候就會建立一個dentry物件。如果這是一個不存在的檔案/資料夾,那麼將會建立一個無效的dentry。例如,對於如下路徑名:home/bob/research08;它由四個路徑部件構成:/,home,bob和research08。相應的,路徑查詢(lookup)將建立四個dentry,每個部件一個。每個dentry物件通過欄位 d_inode指定的索引節點與各自對應的部件相聯絡。

索引節點物件以一個唯一的索引節點號作為標識,儲存了一個特定檔案的資訊。ULK3為索引節點結構體提供全面的欄位定義列表。非常重要的是,第一,i_sb指向了VFS超級塊,第二,i_op是一個諸如如下索引節點操作的switchboard:

create(dir,dentry, mode, nameidata)

mkdir(dir, dentry,mode)

lookup(dir,dentry,nameidata)

...

第一個方法為一個常規檔案建立了新的磁碟索引節點,該索引節點與某個資料夾中的dentry物件相關聯。第二個方法為資料夾建立了新索引節點,該索引節點與某個資料夾下的某個dentry物件相關聯。而第三個方法在一個資料夾中搜索一個索引節點,該索引節點與一個dentry物件中包含的檔名相關聯。

VFS層定義了一個一般性的超級塊物件(struct super_block),它儲存了所掛載(mount)的檔案系統的資訊。特定的欄位 s_fs_info指向屬於特定檔案系統的超級塊資訊。特別地,在Lustre中,這種檔案系統特有的資訊以結構體 lustre_sb_info的形式表現出來,其中儲存了掛載和解除安裝檔案系統所需要的資訊。它進一步地連線到另外一個結構體ll_sb_info,其中包含了更多Lustre Lite特有的關於檔案系統狀態的資訊,這些資訊只為客戶端準備。

Lustre特有的超級塊操作定義在結構體變數lustre_super_operations中。初始化正確的超級塊操作,是在我們在函式client_common_fill_super()中最初建立記憶體中超級塊資料結構時進行的:

sb->s_op =&lustre_super_operations;

值得指出的是,當建立Lustre檔案時,alloc_inode()超級塊方法由ll_alloc_inode()函式實現。它建立了一個Lustre特有的索引節點物件ll_inode_info,並且返回嵌入其中的VFS 索引節點。請注意這裡一般性的VFS 索引節點和Lustre 索引節點互動方法的特別之處:這種方法建立並且用包含Lustre檔案系統所需的額外狀態資訊來填充VFS 索引節點結構體,但是僅返回嵌入在lli之中的VFS 索引節點結構體&lli->lli_vfs_inode。

static structinode **ll_alloc_inode(struct super_block *sb)

{

structll_inode_info *lli;

...

return&lli->lli_vfs_inode;

}

為了從子結構體中找回父結構體,Lustre定義了一個幫助方法ll_i2info(),它實質上以如下方式呼叫了核心巨集container_of:

/* parameters ofmacro: ptr, type, memeber */

returncontainer_of(inode, struct ll_inode_info, lli_vfs_inode);

初始化正確的索引節點和檔案/資料夾操作是在ll_read_inode2()期間索引節點被填充的時候進行的。為此,定義了四個結構體變數。其中兩個是為索引節點操作定義的:ll_file_inode_operations和ll_dir_inode_operations。另外兩個是為檔案/資料夾操作定義的ll_file_operations和ll_dir_operations。對每組來說,檔案和資料夾都有各自的集合,而到底指定哪個是由索引節點或者檔案本身決定的。如下所示,給出了一個檔案操作的定義例項:

/* in dir.c */

structfile_operations ll_dir_operations = {

       .open = ll_file_open,

       .release = ll_file_release,

       .read = generic_read_dir,

       .readdir = ll_readdir,

       .ioctl = ll_dir_ioctl, ...

};

/* in file.c */

structfile_operations ll_file_operations = {

       .read = ll_file_read,

       .write = ll_file_write,

       .open = ll_file_open, ...

例如,如果將被建立的是一個檔案,那麼i_op將被指定為ll_file_operations;而如果將被建立的是一個資料夾,那麼i_op將被指定為ll_dir_operations:

if(S_ISREG(inode->i_mode)) {

       inode->i_op =&ll_file_inode_operations;

       inode->i_fop = sbi->ll_fop;

       ...

} else if(S_ISDIR(inode->i_mode)) {

       inode->i_op =&ll_dir_inode_operations;

       inode->i_fop = &ll_dir_operations;

       ...

一個一般性的觀察是:指向方法表的指標,是由建立它的新例項的當事人或者函式適當地初始化並建立起來的。

路徑查詢是相對來說較為複雜的任務,而又是最重要和常用的任務之一。Linux VFS承擔了大部分的重擔;而我們想強調之處是Lustre做特殊操作的聯結點。在這一節,我們追尋原始碼主線,以足夠的細節總結了基本步驟,但是也跳過了許多在實際原始碼中所需要注意的分支,例如:

  • 包含.和..的路徑名,
  • 包含軟連結的路徑名,軟連結可能導致迴圈引用,
  • 訪問許可權和許可檢查,
  • 包含另一個檔案系統的掛載點的路徑名,
  • 包含不再存在的檔案的路徑名,
  • LOOKUP_PARENT標識設定,
  • 不包含末尾斜槓(trailing slash)的路徑名。

查詢可能由sys_open()呼叫引發,由此引發的慣常的呼叫路徑是執行filp_open()和open_namei()。就是這個函式最後初始化了path_lookup()的呼叫(?)。特別的,如果一個檔案以O_CREAT標誌作為訪問模式的引數開啟,那這個查詢操作將設定LOOKUP_PARENT、LOOKUP_OPEN和LOOKUP_CREATE。最後的路徑查詢結果是如下之一:

  • 如果已存在,返回最後一個路徑部件的dentry物件,
  • 如果不存在,按照建立新檔案時的情形,返回倒數第二個路徑部件的dentry物件。從這裡,你可以通過呼叫父索引節點的建立方法,分配一個新的磁碟索引節點。

現在,我們來聚焦於路徑查詢的特別之處。如果路徑以/開頭,那麼這是一個絕對路徑:搜尋以在current->fs->root中的程序的根檔案目錄作為開始。否則,搜尋以current->fs->pwd作為開始。此時,我們也知道開始資料夾的dentry物件及其索引節點(檢視Figure3,以弄清為什麼)。nameidata使用欄位dentry和mnt來追蹤解析好的上一個路徑部件。初始時,它們被指定為開始資料夾。核心的查詢操作由link_path_walk(name, nd)進行,其中name是路徑名,nd是nameidata結構體的地址。

1. 對於待解析的下一個部件,從它的名字中計算出32位元的雜湊值,以供在dentry cache雜湊表中查詢使用。

2. 在nd->flags中設定LOOKUP_CONTINUE,標識仍有更多的部件尚待解析。

3. 呼叫do_lookup(),在dentry物件中搜索路徑部件。如果找到了(跳過重新證實),則這個路徑部件已被解析,繼續。如果沒有找到,則呼叫real_lookup()。在迴圈的最後,本地dentry變數next的dentry和mnt 欄位將分別指向dentry物件和已掛載的檔案系統物件,而這裡的dentry物件和檔案系統物件正是我們所嘗試解析的路徑部件的。

4. 如果上述,do_lookup()解析到了路徑名的最後一個部件,而就像我們開始所假設的,假如這不是一個符號連結,那麼我們就達到目的地了。我們所需要做的就是將dentry物件和mnt資訊儲存在傳遞過來的nameidata引數nd中,並無錯返回。

nd->dentry =next->dentry;

nd->mnt =next->mnt;

Lustre特有的操作由real_lookup()函式處理。一個dentry由VFS建立並且傳遞到檔案系統特有的查詢函式中。Lustre查詢函式的責任是定位或者建立對應的索引節點,並以正確資訊對之賦值。如果不能找到索引節點,那麼dentry仍然存在,只是它的索引節點指標指向NULL。這種dentry被稱為無效的(negative),意即沒有這個名字的檔案。這種轉換(switching)的程式碼段如下所示:

struct dentry*result;

struct inode *dir= parent->d_inode;

...

result =d_lookup(parent, name);

if (!result) {

       struct dentry *dentry = d_alloc(parent,name, nd);

       if (dentry) {

       result =dir->i_op->lookup(dir,dentry,nd);

       ...

}

現在,查詢開始在Lustre端進行,為得到更多的資訊,接下來的操作可能需要涉及到與MDS進行的通訊。

同時還存在一個cached_lookup路徑,它呼叫Lustre提供的->revalidate方法。這種方法證實快取的dentry/索引節點仍然有效,無需再從服務端更新。

這一節將探討Lustre所遵循的I/O路徑:非同步I/O、組I/O、和直接I/O。接著我們將探討Lustre怎樣在將I/O的控制權已交給VFS(大多數情況下)的情況下和VFS進行互動,其中VFS做了更多的準備工作,然後以一頁一頁的方式(page-by-pagebasis)通過地址空間方法(addressspace methods)讀寫資料,而其中的地址空間方法則由Lustre提供。所以,試著想像如下過程:VFS掛鉤(hooks)到llite,然後llite呼叫VFS幫助處理,隨後VFS將控制權交還llite——一個進進出出、相互糾纏的過程。

這也許是Lustre中最為曲折(traveled)的I/O路徑,我們將自頂向下地描述一個寫過程。讀操作與之非常類似。在VFS中註冊寫操作,已經在2.1節中談及。

1. writev()是為舊核心提供的,較新的核心使用aio_write()。我們以ll_file_write()作為分析的切入點。這個函式定義了一個iovec,其中base指向使用者空間中提供的快取,而length是要寫入的字元數。

struct ioveclocal_iov = { .iov_base = (void __user *) buf,

.iov_len = count };

這裡初始化的另一個結構體是kiocb,它記錄了I/O的狀態。傳入vec和kiocb引數後,它根據核心版本的不同調用ll_file_writev()或者ll_file_aio_write()。在我這裡,我們追尋後者。從原始碼角度看(codewise?),兩個函式的實現是相同的,只是原型定義稍微有所不同:

#ifdefHAVE_FILE_WRITEV

static sszie_tll_file_writev(

       struct file *file,

       const struct iovect *iov,

       unsigned long nr_segs,

       loff_t *ppos) {

#else /*AIO stuff*/

static ssize_t ll_file_aio_write(

       struct kiocb *iocb,

       const struct iovec *iov,

       unsigned long nr_segs,

       loff_t pos)

{

       struct file *file = iocb->ki_filp;

       loff_t *ppos = &iocb->ki_pos;

#endif

2. 理解ll_file_aio_write()函式的關鍵是Lustre根據分條大小,將寫分割為多個塊,然後在一個迴圈中請求對每個塊上鎖。這種方法的目的在於避免需要對大extent上鎖時的複雜性。雖然我們之前提到過LOV是處理分條資訊的層,但是LustreLite對點認識得也非常清楚,正如這種情況所表現出來的一樣——你可以看到它們合作得多麼緊密。分條資訊通過如下方法得到:

structlov_stripe_md *lsm = ll_l2inof(inode)->lli_smd;

Lustre通過建立一個原始iov控制結構體的複本iov_copy,控制了每次寫入的大小,然後回過頭來,請求核心中常用的慣例來驅動寫入:

retval =generic_file_aio_write(iocb, iov_copy, nrsegs_copy, *pppos);

我們在*iov中記錄需要寫入的位元組數,並用count來跟蹤尚待寫入的位元組數。這個過程一直重複,直至出現錯誤,或者所有位元組都已寫入。

在我們進行一般的寫入慣例之前,在通過ll_file_get_tree_lock_lov()來獲得鎖的基礎上,還有一些這個函式需要處理的邊角:

l  如果使用者應用以O_LOV_DELAY_CREATE標誌開啟檔案,但是未呼叫ioctl來建立檔案,就開始寫入,那麼我們就必須使這個呼叫失敗。

l  如果使用者應用以O_APPEND標誌開啟檔案,那麼我們需要獲取對整個上下文上的鎖:lock_end需要設定OBD_OBJECT_EOF標誌,或者設定為-1來標識檔案的結尾。

3. generic_file_aio_write()只是__generic_file_aio_write_nolock()函式的一個封裝;兩個函式都在ULK3中有所描述,而我們將在此對之進行詳述。由於本節不覆蓋直接I/O的內容,寫流程進入到generic_file_buffered_write()函式。在此期間,寫入被進一步分割成頁,併為之分配了頁快取記憶體。執行上述工作的主要內容,由如下所示的迴圈完成:

do {

       ...

       status = a_ops->prepare_write(file,page, offset, offset+bytes);

       ...

       copied = filemap_copy_from_user(page,offset, buf, bytes);

       ...

       status = a_ops->commit_write(file,page, offset, offset+bytes);

       ...

} while (count);

首先,我們通過使用Lustre註冊的方法來準備頁寫入。這種準備工作包括檢查初始位置是否是在開始處、檢查是否需要先從磁碟中讀入(當然,在Lustre中沒有本地磁碟,但是VFS卻是這樣認為的)。然後它從使用者空間中複製有價值的資訊到核心中。最後,我們再次掉用Lustre特有的方法來將頁寫入。這樣,控制就兩次流入和流出Lustre程式碼。

這裡存在一些關於頁和界限管理的有趣的要點。假設需要寫入的的邏輯檔案位置是8193,而頁大小是4KB。由於這是一個基於頁的寫入(寫入準備和寫入執行都是基於頁的),它首先計算頁索引(2)和頁偏移量(1),而bytes是在該頁中所能寫入的最大位元組數。然而,如果剩下的count數比bytes小,我們需要將bytes調整為所需要寫入的確切的位元組數。

index = pos>> PAGE_CACHE_SHIFT;

offset = (pos& (PAGE_CACHE_SIZE -1));

bytes =PAGE_CACHE_SIZE - offset;

bytes = min(bytes,count);

計算所得的頁索引將被用來在頁快取記憶體中定位或者分配頁,然後按如下方式與檔案對映相關聯:

structaddress_space *mapping = file->f_mapping;

page =__grab_cache_page(mapping, index, &cached_page, &lru_pvec);

稍微離題:__grab_cache_page()是一個在一般的檔案寫請求時使用的幫助函式。這個函式的基本流程是首先檢查這個頁是否存在於頁快取記憶體中,如果存在則返回。否則它將通過呼叫page_cache_alloc()來分配一個新頁,並將之加入到頁快取記憶體中(add_to_page_cache())。然而,在上一次檢查到現在的時間段內,另外一個執行緒可能分配了一個頁,所以加入頁快取記憶體可能會失敗。在這種情況下,我們需要再次檢查並從頁快取記憶體中找到該頁:

repeat:

       page = find_lock_page(mapping, index);

       if (!page) {

              ... page_cache_alloc(mapping);

              err = add_to_page_cache(*cachedpage, mapping, index, GFP_KERNEL);

       if (err == -EEXIST)

              goto repeat;

這裡以沾汙程式碼的代價進行了一個優化:如果像如上所說的,不能將已分配的頁加入到頁快取記憶體中,則將它保留在cached_page中,而不是將它返回,這樣下次再請求一個新頁時,我們不需要再次呼叫分配函式。

4. 現在我們進入到準備寫階段。它按如下描述:

intll_prepare_write(struct file *file, struct page *page,

unsigned from, unsigned to)

以設定為offset的from,設定為offset + bytes的to作為引數,ll_prepare_write()被呼叫。這就是我們在上面提到的分界問題。總的來說。這個方法是用來保證頁已經更新了,如果這不是整頁的寫,那麼首先將讀入特定的頁。

這裡使用了一些新的結構體,它們的含義如下:

結構體obdo是用來以線上(on thewire)的形式表示Lustre物件及其相關資訊進行。

結構體brw_page用來描述待發送的頁的狀態。

結構體obd_info用來在Lustre各層間傳輸引數。

在這個方法中,我們還需要為部分頁寫(partial page write)之類的情況檢查檔案末尾(EOF):如果EOF處於頁內(寫超出了EOF),我們需要將未寫入的部分填充為0;否則,我們需要預讀它。

5. 接著,LOV通過lov_prep_async_page()初試化準備頁。

各層定義了頁寫入需要經歷的三個結構體。在Lustre Lite層,是ll_async_page(LLAP)。LOV則定義了lov_async_page(LAP)和osc_async_page(OAP)。

6. 如果寫操作是非同步的,它將由osc_queue_async_io()處理。為了確定我們不會超過所允許的髒頁數,阻塞在試圖增加額外的髒頁的企圖上,osc_queue_async_io()內部呼叫osc_enter_cache()函式進行清算。以類似的方式,它也強制實行grant。

這樣,在(至少)有兩種方式下,OAP將不能進入快取記憶體。

  • 如果快取記憶體的大小小於32MB,那麼就O.K.了,將OAP放入快取記憶體,返回0,否則返回錯誤碼。
  • 如果客戶端的grant不夠,那麼同樣返回錯誤碼。在每個OSC初試化時,我們可以假設OST伺服器端為客戶端預留(grant)一些空間。grant是伺服器向客戶端保證的能寫入的資料量,客戶端確信伺服器能夠處理這麼多資料。grant的初始值是2MB,這是相當於兩個RPC大小的資料。隨後寫請求將由客戶端發起,如果需要,它能請求更多的grant。同時,在每個資料傳輸或者寫入時,客戶端需要跟蹤這個grant,確保不會超額。

如果頁不能進入緩衝,將會稍後重試組I/O或者非同步I/O。

7. 在OAP快取記憶體檢查完畢之後,將呼叫loi_list_maint()來將OAP放在合適的連結串列裡,並使之準備好讀或寫操作。

8. 函式osc_check_rpcs()對每個lov_oinfo中的物件建立RPC。注意,每個RPC只能攜帶一個數據物件的內容。

組I/O是當OAP不能被成功放入快取記憶體中時激發的,這種不成功最有可能的情況是grant不夠。在這種情形下,Lustre Lite將建立一個obd_to_group結構體來儲存OAP。該頁將會設定好URGENT標識,加入客戶端的obd就緒列表。最後,將呼叫oig_wait()來等待組I/O的完成。

注意,組I/O會等待操作的完成,所以它又被稱為同步I/O。其次,所有的組I/O都是urgent I/O,又同時是讀操作。相比之下,在非同步I/O中,OAP快取記憶體則(當提交時)進入寫列表

同樣值得指出的是,除了直接I/O和無鎖I/O,所有的讀都以組I/O的形式執行。直接I/O將在下面簡要介紹。無鎖I/O是一種型別特別的I/O(現在不可用了,除非在liblustre中),在無鎖I/O中,客戶端不申請任何的鎖,而是通知伺服器根據客戶端的行為處理鎖。

對於直接I/O,VFS傳遞一個引數iovec用來描述所傳輸的資料段。這個引數由一個開始地址void * iobase和大小iov_len簡單的進行定義。直接I/O要求開始地址必須是page-aligned。

Lustre Lite呼叫ll_direct_IO_26()和obd_brw_async()函式處理每個頁。實質上,這將轉化為呼叫osc_brw_async(),稍後它將被用於建立RPC請求。

不論是一般的,在VFS的上層,還是特殊的,在LustreLite裡,有幾個地方可以註冊你自己的讀寫方法,所有這些都是通過地址空間操作結構體提供的。對於讀操作,是readpage()及其向量形式的readpages()。對於寫,事情稍微有點複雜。第一個入口點是writepage(),可能由如下事件觸發:

l 當記憶體壓力超出閾值,VM將觸發髒頁的重新整理。

l 當用戶應用呼叫fsync()強制執行髒頁的重新整理。

l 當核心執行緒週期性地重新整理髒頁。

l 當Lustre鎖管理器撤銷頁的鎖(塊請求),髒頁應當重新整理。

readpage()和writepage()是否實現是可選的,並不是每個檔案系統都支援它們。但是利用核心中預設的讀寫方式(即do_generic_file_read()和do_generic_file_write()的一般化實現)需要者兩個方法。這簡化了和核心快取記憶體和VM的配合。同時,提供mmap的支援同樣需要這兩個方法。

同樣,檔案系統也以地址空間物件的形式註冊prepare_write()和commit_write()函式。因此,地址空間物件可以描繪為連線檔案空間和儲存空間的橋樑。

Lustre的地址空間操作定義在lustre/llite/rw26.c中:

structaddress_space_operations ll_aops = {

      .readpage = ll_readpage,

      .direct_IO = ll_direct_IO_26,

      .writepage = ll_writepage_26,

      .set_page_dirty =__set_page_dirty_nobuffers,

      .sync_page = NULL,

      .prepare_write = ll_prepare_write,

      .commit_write = ll_commit_write,

      .invalidatepage = ll_invalidatepage,

      .releasepage = ll_releasepage,

      .bmap = NULL

};

在file物件中有一個*f_mapping指向檔案對應的地址空間物件。這種聯絡是在檔案物件建立的時候建立的。


3.4     預讀

Lustre客戶端的預讀發生在頁讀取的情況下,由結構體ll_readahead_state控制。該結構體定義在lustre/llite/llite_internal.h中。這個結構體每個檔案一個,包含了以下資訊:

l 讀歷史。1. 發生過多少次連續讀。2. 如果是跳(stride)讀,那麼發生過多少次連續跳讀。3. 跳讀間隔和跳讀長度。

l 預讀視窗(即預讀視窗起點和終點)。讀操作發生愈連續,預讀視窗生長地愈長。

l 幫助監測預讀模式的狀態資訊。

每個客戶端最大可能預讀40MB,預讀演算法如下所示:

1. 在頁讀取中(ll_readpage)檔案的預讀狀態要根據當前狀況進行跟新:

a) 如果頁偏移量處於一個連續的視窗中(在上一次頁的+8、-8視窗中),那麼ras_consecutive_requests(由ll_readahead_state定義)將遞增1。如果這是讀的第一頁,那麼預讀視窗將增加1MB的長度。所以如果讀是連續的,那麼預讀視窗將與讀操作的增加一起增長。

b) 如果頁不處於連續的視窗中,那麼將判斷是否處於跳讀模式。如果是,將現在的長度/步幅間隔同過去歷史中的相對比。如果它們相等,那麼ras_consecutive_stride_requests將加1。如果這是讀的第一頁,那麼預讀視窗也將增加1MB的長度。

c) 如果頁既不處於連續的視窗中,又不處於連續跳躍視窗中,那麼所有的預讀狀態將被重置。例如ras_consecutive_pages和ras_consecutive_requests將重置為0。

       2. 接著,頁的讀取將根據上一步跟新的狀態進行實際的預讀。

a) 增加預讀的視窗,嘗試覆蓋此次讀中所有的頁。

b) 根據實際檔案長度調整預讀視窗,計算此次預讀中將讀入多少頁。

c) 實際執行預讀。

在proc中有一個可以檢視預讀的狀態的預讀狀態檔案(/proc/fs/lustre/llite/XXXXX/read_ahead_stats)。

本文章歡迎轉載,請保留原始部落格連結http://blog.csdn.net/fsdev/article