歡迎您光臨本站 註冊首頁

Linux 文件系統中元數據使用計數的機制

←手機掃碼閱讀     火星人 @ 2014-03-12 , reply:0
  
在 Linux 文件系統中,元數據的引用計數主要用於管理元數據(如 inode, dentry 結構)在內存中的創建、使用和釋放。了解這部分的機制,有利於深入認識文件系統的運行機制,以及Linux如何在內存中管理元數據。這部分內容也是構建分散式文件系統所必須的知識,由此才能保證元數據在分散式文件系統中的正確使用。

概述

元數據是一個文件系統的重要部分。很多書籍和文章都介紹過 dentry 和 inode 在 Linux 中的作用和機制,但卻很少有文獻涉及到它們的使用計數( usage counter )。使用計數的機制看似很簡單:使用了一個元數據就遞增,用完了就遞減。但在這句簡單的描述後面,具體的過程到底是如何進行的呢?這實際上貫穿了整個元數據的操作以及元數據在內存中的管理。了解這部分的機制,是一個很有意思的過程,可以讓你看到 Linux 嚴謹縝密的思路,可以深入認識 Linux 文件系統的運行機制。這部分內容也是構建分散式文件系統所必須的知識。

本文仍然從兩方面來介紹使用計數:增加和減少。最後再看一下在分散式環境中有哪些變化。

這裡所引用的代碼依據的是 Linux 內核 2.6.20 的版本。





使用計數的增加

創建操作

元數據的創建主要可以分為對文件的創建和對目錄的創建。不管是文件還是目錄,它們都對應同樣的元數據結構,在內存中都有 inode 和 dentry 。

下面我們分別看一下主要的兩個創建操作:創建文件和創建目錄。

(1)創建文件

創建文件是通過系統調用 sys_open() ,並設置 O_CREATE 標誌位來實現的。其調用過程如下:

sys_open() > do_sys_open() > do_filp_open() > open_namei()  

在 open_namei() 中,會創建出 dentry 和 inode 結構。先看關於 dentry 的路徑:

open_namei() > lookup_hash() > __lookup_hash()  

這裡會分成3種情況:

  • 在 dcache 中查找: __lookup_hash() > cached_lookup() > d_lookup() > __d_lookup()
  • 分配新的 dentry: __lookup_hash() > d_alloc() > atomic_set(&dentry -> d_count, 1);
  • 在具體文件系統中查找: __lookup_hash() > i_op -> lookup()

和查找有關的內容我們在後面介紹,這裡只看創建,也就 d_alloc() ,它會分配一個新的 dentry 結構,在分配的過程中,就會把 dentry 的使用計數初始化為1。在 d_alloc() 中,還會通過函數 dget() 遞增父目錄的使用計數,這是為了防止父目錄在該 dentry 刪除前被刪除。(“/”除外,它沒有父目錄):

d_alloc() > dget(parent) > atomic_inc(&dentry->d_count);  

我們再看關於 inode 的路徑:

open_namei() > open_namei_create() > vfs_create() > i_op->create()  

最終會調用具體文件系統的 create 函數。這裡以 Ext2 為例,其調用過程如下:

ext2_create() > ext2_new_inode() > new_inode() >   alloc_inode() > atomic_set(&inode->i_count, 1);  

具體文件系統在分配 inode 結構的時候,會通過初始化把 inode 的 i_count 域置為1。同時還把 inode 的 i_nlink 域置為1,這個域表示 inode 的 hard link 的數目,其值會被寫入到具體文件系統的磁碟中。

總結一下,通過創建操作,會在內存中建立起 dentry 和 inode 結構,並且會把它們的使用計數都初始化為1。

(2)創建目錄

創建目錄和創建文件是類似的,這裡我們簡單看一下調用的路徑就清楚了。

創建目錄是通過系統調用 sys_mkdir() 來實現的。關於 dentry 的路徑如下:

sys_mkdir() > sys_mkdirat() > lookup_create() > lookup_hash() > __lookup_hash()  

可以看出,這與前面“創建文件”中介紹的是一樣的。

關於 inode 的路徑如下:

sys_mkdir() > sys_mkdirat() > vfs_mkdir() > i_op->mkdir()  

最終會調用具體文件系統的 mkdir 函數。這裡以 Ext2 為例,其調用過程如下:

ext2_mkdir() > ext2_new_inode() > new_inode() > alloc_inode() > atomic_set()  

可以看出,這與前面“創建文件”中介紹的也是一樣的。

由此也可以看出,從內存中的元數據結構來看,Linux對文件和目錄的管理是一樣的。

查找操作

創建了一個對象(文件或目錄)后,要使用這個對象,就必須先進行查找。查找操作是元數據使用的關鍵操作,基本上所有元數據操作都會以查找操作為起始,因為只有找到了元數據才能進一步對其進行操作。即使對於創建操作,一開始也要進行查找,只不過因為要創建的對象還不存在,所以會查找失敗,然後才進行創建。

查找操作的入口函數是 __link_path_walk() ,其調用過程如下:

__link_path_walk() > do_lookup()  

到了這裡,要做的事情主要是在內存中查找相應文件所對應的 dentry 結構。這會分為兩種情況:

(1)該 dentry 結構在內存中

此時,通過哈希就可以獲取該 dentry 結構,並將其使用計數遞增。

do_lookup() > __d_lookup() > atomic_inc(&dentry->d_count)  

(2)該 dentry 結構不在內存中

此時,該 dentry 結構可能從來就沒在內存中建立起來,或者在內存中存在過,但已經從 LRU 隊列 dentry_unused 中被換出內存。無論如何,都需要從磁碟讀取元數據,在內存中建立起 dentry 和 inode 結構。這時所進行的步驟是:

首先在內存中分配一個dentry結構:

do_lookup() > real_lookup() > d_alloc() > atomic_set(&dentry->d_count, 1);  

這裡的 d_alloc() 和前面“創建操作”介紹的一樣,會把 dentry 的使用計數初始化為1,並將其父目錄的使用計數通過 dget() 遞增。

分配了 dentry 結構后,就要從磁碟找出對應的元數據。這個過程因文件系統而異,所以通過父節點的 inode -> i_op 里的函數來進行。

real_lookup() > i_op->lookup()  

這裡以 Ext2 為例,調用的是 ext2_create() ,過程如下:

(1) ext2_lookup() > iget(dir->i_sb, ino);  (2) ext2_lookup() > d_splice_alias() > __d_find_alias() > __dget_locked() >        atomic_inc(&dentry->d_count);  

前者調用 iget() ,首先通過 ino 在 inode cache 中查找 inode ,如果找到就返回並增加其引用計數;如果沒有找到,就分配一個新的(調用 alloc_inode() ,會把使用計數初始化為1,參照前面“創建操作”),並從磁碟讀入相應索引節點,在內存中建立起 inode 結構。

後者則把 dentry 與 inode 結構綁定,並遞增了 dentry 的使用計數。

總結一下,查找操作的主要過程就是在內存中查找 dentry 結構,如果找到就遞增其使用計數;如果找不到就到磁碟中去取,並在內存建立 dentry 和 inode 結構,同時將它們的使用計數初始化為1。因此查找操作都會增加 dentry 的使用計數,或者遞增,或者初始化為1。

元數據操作對使用計數的運用

這裡我們舉例說明元數據操作對 dentry 使用計數的運用,讓大家對其有個比較具體的認識和感覺。

元數據操作的實質就是對元數據進行使用。那麼,要使用某個元數據時,必須在內存中為其建立相應的結構,即 inode 和 dentry 。但並不是所有的元數據每時每刻都會有對應的結構在內存中,只有需要時才會建立這些結構,並且在特定的時候又會被換出內存。那麼如何管理內存元數據結構的使用,從而決定其何時在內存中,何時被換出,這就是通過 dentry 的使用計數來實現的。

下面我們以兩個常見的元數據操作為例,來看 Linux 如何管理內存元數據結構的使用。

(1) getattr 操作

Linux 內核中有很多操作都會調用到 getattr ,我們舉其中的一個來說明:sys_stat() > vfs_stat_fd() 。

函數 vfs_stat_fd() 比較短,我們將其內容都列出來:

int vfs_stat_fd(int dfd, char __user *name, struct kstat *stat)  {      struct nameidata nd;      int error;        error = __user_walk_fd(dfd, name, LOOKUP_FOLLOW, &nd);      if (!error) {          error = vfs_getattr(nd.mnt, nd.dentry, stat);          path_release(&nd);      }      return error;  }  

這裡先調用了 __user_walk_fd() ,這個函數繼續走下去的路徑是:

__user_walk_fd() > do_path_lookup() > link_path_walk() > __link_path_walk()  

可以看出, __link_path_walk() 就是前面介紹過的查找操作。如果成功返回,就會增加 dentry 的使用計數,否則就不增加。而如果查找成功,就進行具體的 getattr 的工作,調用的是 vfs_stat_fd() 的主體函數 vfs_getattr() 。這之後,會調用 path_release() ,這個函數的路徑是:

path_release(&nd) > dput(nd->dentry)  

函數 dput() 會將 dentry 的使用計數減少,這個函數我們將在後面詳細介紹。

總結一下, getattr 操作首先要查找元數據,找到后,就增加 dentry 的使用計數,只要 dentry 的使用計數不為0,它就會存在於 dcache 中,而不會被換出內存。當 getattr 的主要操作步驟完成後,就會減少 dentry 的使用計數,表明 getattr 操作已經完成,不再需要使用這個 dentry 了。

(2) link 操作

下面再看一個操作。 Link 操作用於創建一個對象鏈接。其調用路徑為:

sys_link() > sys_linkat()  

接下來可以分為七個部分:

(1) error = __user_walk_fd(olddfd, oldname,                         flags & AT_SYMLINK_FOLLOW ? LOOKUP_FOLLOW : 0,                         &old_nd);  (2) error = do_path_lookup(newdfd, to, LOOKUP_PARENT, &nd);  (3) new_dentry = lookup_create(&nd, 0);  (4) error = vfs_link(old_nd.dentry, nd.dentry->d_inode, new_dentry);  (5) dput(new_dentry);  (6) path_release(&nd);  (7) path_release(&old_nd);  

第1步中, __user_walk_fd() 會查找要被鏈接的文件,這和前面 getattr 中的函數一樣,會把這個文件對應的 dentry 的使用計數進行遞增。它和第7步中的 path_release() 對應。

第2步中, do_path_lookup() 會查找要創建的鏈接的父目錄,它同樣會進行查找操作,遞增 dentry 的使用計數。它和第6步中的 path_release() 對應。

第3步中, lookup_create() 會創建鏈接對象的 dentry 結構,這和前面“創建目錄”中介紹的函數一樣。它和第5步中的 dput() 對應。

這裡我們再次看到,一個元數據操作中都會先查找涉及到的元數據,並增加其 dentry 的使用計數,然後在該操作結束的時候遞減這些使用計數。

對於 link 操作,我們還要講講它的主體函數,也就是 vfs_link() ,其路徑為:

vfs_link() > i_op->link()  

以 Ext2 為例,調用的是 ext2_link() ,過程如下:

(1) ext2_link() > inode_inc_link_count() > inc_nlink()  (2) ext2_link() > atomic_inc(&inode->i_count);  

先看前者,這裡的 inode 結構對應的是被鏈接的文件, ext2_link() 會遞增該 inode 的 i_nlink ,前面說過,這個域表示 inode 的 hard link 的數目,因為我們對這個文件建立了一個新的鏈接,所以會對這個域進行遞增。

後者的 inode 結構依然對應的是被鏈接的文件, ext2_link() 會遞增 inode 的使用計數。到目前位置,我們看到,各個操作對元數據使用計數的運用主要都是針對 dentry ,而很少針對 inode 。這是因為 dentry 主要用於內存中目錄樹結構的表示,而查找操作主要就是針對目錄樹結構來進行的,因此它頻繁地對 dentry 的使用計數進行操作。對於 inode 的使用計數,它主要表示這個元數據的存在,因此一般只在創建這個 inode 的時候以及刪除的時候才會用到。我們知道一個 inode 可以對應多個 dentry ,這是因為一個文件可以有多個鏈接,所以當多了一個鏈接時,就要遞增 inode 的使用計數。





使用計數的減少

dput 函數

前面我們提到過,在元數據操作中,通過查找操作增加了 dentry 的使用計數,會在結尾處通過 dput() 進行遞減。這裡我們就來看看 dput() 的機制。

在 dput() 中,處理的步驟如下:

  1. 對 dentry -> d_count 遞減,如果不為0,就直接返回。
  2. 判斷具體文件系統是否定義了 d_op -> d_delete 這個介面函數。本地文件系統 Ext2, Ext3 都沒有定義這個函數, NFS 定義了這個函數。對於該函數的具體作用我們在後面介紹。既然 Ext2 沒有定義,我們就繼續往下看。
  3. 判斷 dentry 是否從 dcache 的哈希鏈上移除了。(1)如果是,表示該元數據對應的對象已經被刪除了,此時可以釋放該元數據;(2)如果不是,表示該元數據對應的對象沒有被刪除,這時把 dentry 掛到 LRU 隊列 dentry_unused 上,然後返回。
  4. 如果進入釋放元數據的那條路徑,會釋放 dentry 結構和 inode 結構。

其中,釋放 inode 結構的調用路徑是:

dput() > dentry_iput() > iput() > iput_final()  

在 iput() 中,會遞減 inode 的使用計數,如果遞減完後為0,就進一步調用 iput_final()

iput_final() > generic_drop_inode()  

在 generic_drop_inode() 中,會判斷 inode -> i_nlink 的值。我們在前面說過, i_nlink 這個域表示 inode 的 hard link 數目,這裡就是通過這個域來判斷該 inode 能否被刪除。

若 inode -> i_nlink 為0,說明沒有 hard link 指向該 inode ,可以將其刪除,路徑為:

generic_drop_inode() > generic_delete_inode() > s_op->delete_inode()  

若 inode -> i_nlink 不為0,說明仍有 hard link 指向該 inode ,不能將其刪除,路徑為:

generic_drop_inode() > generic_forget_inode()  

釋放 inode 結構的步驟就是這些。釋放 dentry 的主要做的事情就是將 dentry與inode 脫離,然後釋放 dentry 結構。要注意的是,每當釋放了一個 dentry ,都要獲取其原來父目錄的 dentry ,然後又跳轉到 dput() 的開頭,繼續對父目錄的 dentry 進行釋放操作。這是因為,前面也提過,每次創建一個 dentry 結構,除了增加自身的使用計數外,還會增加其父目錄 dentry 的使用計數。所以當釋放了一個 dentry 后也要將其父目錄 dentry 使用計數遞減,才能保證父目錄為空時能夠被釋放。

unlink 函數

在 dput() 中我們提到過要判斷 dentry 是否從 dcache 的哈希鏈上移除。在本地文件系統中,當刪除一個文件時,就會將其對應的 dentry 從 dcache 的哈希鏈上移除,這是通過 unlink() 來實現。unlink() 的流程如下:

sys_unlink() > do_unlinkat() >   

在do_unlinkat()中,主要可以分為五個部分:

(1) dentry = lookup_hash(&nd);  (2) atomic_inc(&inode->i_count);  (3) vfs_unlink(nd.dentry->d_inode, dentry);  (4) dput(dentry);  (5) iput(inode);	/* truncate the inode here */  

可以看出,主體函數 vfs_unlink() 前後,有配對的操作對 dentry 和 inode 進行增減。

我們先看 vfs_unlink() ,其路徑為:

vfs_unlink() > d_delete()  

在 d_delete() 中,如果 dentry 的使用計數為1,說明此時沒有其他人引用該 dentry ,那麼就嘗試把該 dentry 的 inode 刪除,這裡調用的是 dentry_iput() ,這個函數已經在 dput() 那部分介紹過了。這個過程的實質就是把 dentry 轉為 negative 狀態,然後返回。轉為 negative 的 dentry 依然在 dcache 的哈希鏈中,但刪除操作已經完成,對應的代碼為:

if (atomic_read(&dentry->d_count) == 1) {      dentry_iput(dentry);      ...      return;  }  

否則,即 dentry 的使用計數大於1(這裡不可能小於1,因為 do_unlinkat() 中調用 lookup_hash() 時已經對 dentry 的使用計數進行了增加),說明有其他人引用該 dentry ,此時不能把這個 dentry 轉為 negative ,那麼就把這個 dentry 從 dcache 的哈希鏈中脫離,對應的代碼為:

if (!d_unhashed(dentry))      __d_drop(dentry);  

那麼什麼時候刪除這個 dentry 所對應的 inode 呢?大家可以回過頭看一下 dput() 介紹過的內容。在 dput() 中,當 dentry 已經從 dcache 的哈希鏈上移除后,就會繼續進行釋放元數據的操作。所以只要當最後一個使用 dentry 的操作結束時調用 dput() ,就會調用 dentry_iput() 。

總結一下,刪除 inode 必須由 unlink 的 d_delete 發起,如果可能就在 d_delete 中完成刪除;否則就 unhash ,然後由最後一個使用者調用 dput() 刪除。但只有在 d_delete 完成 unhash 之後, dput 才有可能刪除 inode 。

看完主體函數 vfs_unlink() 后,我們關注一下對 inode 使用計數的增減。

在 do_unlinkat() 中,調用 vfs_unlink() 之前,遞增了 inode -> i_count ,因此,如果剛進入 do_unlinkat() 時 dentry 的使用計數為1,真正的刪除操作並不是在 vfs_unlink() 的 d_delete() 進行,而是在 vfs_unlink() 之後的 iput() 進行(源碼里也在 iput() 旁邊進行了註釋)。

大家也許要問, vfs_unlink() 之後,在 iput() 之前不是還有一個 dput() 嗎?那麼會不會在這裡就刪除了 inode 呢?其實不會,我們分三種情況來討論:

  1. 如果 dentry 的使用計數為1,說明沒有其他人在使用,則此前 vfs_unlink() 的 d_delete() 必然已經將 dentry 轉為 negative ,但沒有脫離 dcache 的哈希鏈,因此不會進行刪除(參看 dput() 流程)。刪除 inode 的時機是 do_unlinkat() 的 iput() 。
  2. 如果 dentry 的使用計數大於1,說明有其他人在使用。若在 do_unlinkat() 調用 dput() 之前,其他使用者都調用了自己的 dput() ,則此時 dentry 的使用計數又變為了1。由於之前 vfs_unlink() 的 d_delete() 已經將 dentry 從哈希鏈上移除(但也因此沒有走到 dentry_iput() 那步,沒有遞減 inode 的使用計數),因此在 do_unlinkat() 的 dput() 中會走到 dentry_iput() ,但由於 do_unlinkat() 一開始遞增了 inode 的使用計數,所以這個 dput() 也不能刪除 inode 。刪除 inode 的時機仍然是 do_unlinkat() 的 iput() 。
  3. 如果 dentry 的使用計數大於1,並且在 do_unlinkat() 調用 dput() 之前,其他使用者沒有調用自己的 dput() ,則 do_unlinkat() 調用 dput() 遞減了 dentry 的使用計數之後就直接返回了。刪除 inode 的時機是其他使用者的 dput() 。

總結一下,這部分的操作比較繁雜,需要結合 dput() 和 unlink 操作一起來看,但理清思路后,也會發現其邏輯還是很清晰的,而這種機制也與使用計數的增加結合得非常好,共同構成了 Linux 文件系統管理內存元數據結構的機制。





分散式文件系統對使用計數機制的擴展

分散式文件系統和本地文件系統有所差別,典型的結構由三部分構成:客戶端、元數據伺服器、數據伺服器。它的一個特點就是允許多客戶端的模式,因此有些場景跟單機的模式不大一樣。

查找后的 revalidate

前面提到過,在查找操作中,會先到內存去找,如果找不到還要到具體文件系統的磁碟上去取。在分散式環境中,當一個客戶端更新了一個文件,從而相應地修改了其元數據,其他客戶端並不能馬上知道這個更新,於是當查找操作在內存中找到所要的元數據后,會判斷一下文件系統是否實現了 d_op -> d_revalidate 這個介面函數,如果實現了,就會再到元數據伺服器取一份最新的元數據,從而保證客戶端緩存中數據的有效性,這也是這個操作名稱叫做 revalidate 的原因。

本地文件系統如 Ext2, Ext3 都沒有實現這個介面函數,也沒有必要。 NFS 中是有實現的。

前面說過,查找操作會增加 dentry 的使用計數,也就是說不管走了什麼路徑,只要找到了所需的元數據,那麼通過查找操作所返回的 dentry 結構,其使用計數是增加過的。所以具體文件系統的 d_revalidate 函數也要對 dentry 的使用計數進行增加,這樣才能和 VFS 層正確銜接起來。

dput 函數的變動

大家是否還記得,前面介紹 dput() 函數時,曾經提到其處理步驟可分為五步,第2步中要判斷具體文件系統是否定義了 d_op -> d_delete 這個介面函數。由於 Ext2, Ext3 都沒有定義,我們也就跳過了這個判斷。然而,在 NFS 中,是有定義這個介面函數的,但沒有什麼實質內容,基本就是直接返回。其實,是否定義這個函數是一個路口的轉向。如果定義了, dput() 的流程就會直接跳轉到函數 __d_drop() ,其作用是把 dentry 從哈希鏈上移除;再接下來就進入 dentry_iput() 嘗試刪除 inode 。

大家也許會奇怪,前面介紹了 dput() 和 unlink 操作之間那麼多複雜的關係,怎麼這裡直接就都跳過了。這其實是分散式環境的一種解決方法。在分散式環境中,由於有多個客戶端,只要有一個客戶端進行了刪除操作,相應的元數據就應該可以被刪除。如果按照本地文件系統的那種機制,此時只有進行了刪除操作的客戶端才可以通過 dput() 最終走到函數 s_op -> delete_inode() ,從而在元數據伺服器端釋放元數據;其他客戶端由於沒有進行刪除操作,它們的 dput() 都無法走到 dentry_iput() 那一步,更別說 s_op -> delete_inode() 了。所以在分散式環境中,就採用另一種思路:只要 dentry 的使用計數為1,此時調用 dput() 都可以走到 dentry_iput() ,如果此時 inode 的使用計數也為1,就能繼續一直走到 generic_drop_inode() , generic_drop_inode() 可以繼續走到 s_op -> delete_inode() 。這樣,元數據伺服器就可以自己記錄有哪些客戶端使用了這個 dentry ,而各個客戶端在遞減 dentry 的使用計數為0時,都會通過 dput() 與元數據伺服器通信。元數據伺服器就可以知道某個時刻是否還有客戶端在使用這個 dentry ,從而當元數據被刪除時決定何時可以進行釋放。

這裡還有一個問題。前面介紹過, generic_drop_inode() 會根據 inode -> i_nlink 是否為0來判斷要調用哪個函數:

若 inode -> i_nlink為0,說明沒有 hard link 指向該 inode ,可以將其刪除,路徑為:

generic_drop_inode() > generic_delete_inode() > s_op->delete_inode()  

若 inode->i_nlink 不為0,說明仍有 hard link 指向該 inode ,不能將其刪除,路徑為:

generic_drop_inode() > generic_forget_inode()  

也就是說 generic_drop_inode() 不一定能走到 s_op -> delete_inode() 。而且 inode -> i_nlink 會記錄在具體文件系統的磁碟上,它的值是一個全局的值,而不是針對某個客戶端的。所以一種解決辦法就是不以 s_op -> delete_inode() 作為客戶端與元數據伺服器的通信介面,而以 s_op -> clear_inode() 作為通信介面。對於 s_op -> clear_inode() ,無論 inode -> i_nlink 的值為多少,都會被調用:

(1) generic_drop_inode() > generic_delete_inode() > s_op->clear_inode()  (2) generic_drop_inode() > generic_forget_inode() > s_op->clear_inode()  





總結

通過上面的介紹,我們看到了 Linux 如何對 dentry 和 inode 的使用計數進行操作:何時增加,何時減少。並且了解了元數據操作是如何通過這些使用計數來表明自己正在使用這些元數據的。我們也看到了當刪除一個元數據后,如何等到所有使用者都把該元數據的使用計數遞減為0之後,才真正對元數據進行釋放。最後我們通過分散式文件系統的一些機制了解了在分散式環境下對使用計數的操作要做哪些相應的改變。(責任編輯:A6)



[火星人 ] Linux 文件系統中元數據使用計數的機制已經有850次圍觀

http://coctec.com/docs/linux/show-post-69128.html