1. 程式人生 > >Linux VFS機制簡析(二)

Linux VFS機制簡析(二)

Linux VFS機制簡析(二)

接上一篇Linux VFS機制簡析(一),本篇繼續介紹有關Address space和address operations、file和file operations、dentry和dentry operations和dentry cache API。

Address Space

Address Space用於管理page caches裡的page頁,它關聯某個檔案的所有pages,並管理檔案的內容到程序地址空間的對映。它還提供了記憶體管理介面(page回收等)、根據地址查詢page、跟蹤page的tags(如dirty和writeback)等等功能。
VM模組會呼叫->write_page方法去嘗試將髒頁刷盤,以及呼叫->releasepage方法將clean page釋放。帶有PagePrivate標記的clean page(引用為0)會被VM直接釋放而不通知Address Space。
為了實現這個功能,Address Space通過lru_cache_add()將page放入LRU,並通過mark_page_active()標記page正在使用。

Pages通過->index儲存在一個radix樹裡,該radix樹維護page的PG_Dirty和PG_Writeback資訊,因此查詢這兩個標識的pages變得非常快。
Dirty標記(PAGECACHE_TAG_DIRTY)主要由->writepages(預設方法mpage_writepages())方法使用。它使用該標記查詢髒頁並呼叫->writepage方法。如果Address operations實現了自己的->writepages(不使用mpage_writepages),則Dirty標記將幾乎沒有作用。write_inode_now()和sync_inode()通過Dirty標記來檢查->writepages是否成功完成。
Writeback標記主要是由filemap_wait

方法和sync_page* 方法使用,通過呼叫filemap_fdatawait_range()等待所有的writeback完成。如果定義了->sync_page,則會呼叫它來等待所有需要writeback的page結束。

Address Space Handler可以通過page的private欄位儲存額外的資料,此時需要設定PG_Private標識。這樣VM的相關操作會呼叫address的handler處理這些資料。

上面說的這麼多Page相關的管理,其實Address Space最核心的作用是充當儲存和應用程式的中間快取。資料從儲存側以page為單位讀入address space,通過拷貝或者mapping的方式提供給應用層。應用寫入資料到address space,然後通過writeback機制寫入到儲存。
讀操作的核心是readpage()

。寫操作稍微複雜些,可以通過write_begin/write_end或者set_page_dirty寫入資料到address space,再通過writepagesync_pagewritepages寫入資料到儲存。從address space裡增加刪除page由inode的i_mutex鎖保護。

當資料寫入到page,需要設定PG_Dirty標識,當writepage準備寫入儲存時清除PG_Dirty,並設定PG_Writeback標識,知道資料完全寫入儲存後清除PG_Writeback。

struct address_space_operations

struct address_space_operations的定義如下:

struct address_space_operations {
    int (*writepage)(struct page *page, struct writeback_control *wbc);
    int (*readpage)(struct file *, struct page *);
    int (*sync_page)(struct page *);
    int (*writepages)(struct address_space *, struct writeback_control *);
    int (*set_page_dirty)(struct page *page);
    int (*readpages)(struct file *filp, struct address_space *mapping,
                    struct list_head *pages, unsigned nr_pages);
    int (*write_begin)(struct file *, struct address_space *mapping,
                            loff_t pos, unsigned len, unsigned flags,
                            struct page **pagep, void **fsdata);
    int (*write_end)(struct file *, struct address_space *mapping,
                            loff_t pos, unsigned len, unsigned copied,
                            struct page *page, void *fsdata);
    sector_t (*bmap)(struct address_space *, sector_t);
    int (*invalidatepage) (struct page *, unsigned long);
    int (*releasepage) (struct page *, int);
    void (*freepage)(struct page *);
    ssize_t (*direct_IO)(int, struct kiocb *, const struct iovec *iov,
                    loff_t offset, unsigned long nr_segs);
    struct page* (*get_xip_page)(struct address_space *, sector_t,
                    int);
    /* migrate the contents of a page to the specified target */
    int (*migratepage) (struct page *, struct page *);
    int (*launder_page) (struct page *);
    int (*error_remove_page) (struct mapping *mapping, struct page *page);
    int (*swap_activate)(struct file *);
    int (*swap_deactivate)(struct file *);
};

writepage:VM呼叫,用於將髒頁寫入後端儲存。引數wbc->sync_mode顯示是什麼原因觸發,'sync'或者'flush'(釋放記憶體)。呼叫時PG_Dirty已經被清除,並且PageLocked已經設定。writepage開始寫入資料時需要設定PG_Writeback,並且寫入結束時清除該標記。無論是同步還是非同步寫入,都要保證函式返回時page處於unlocked狀態。
如果wbc->sync_mode是WB_SYNC_NONE(不等待),則writepage遇到困難時可以不那麼努力的寫入,而是返回AOP_WRITEPAGE_ACTIVATE,這樣VM不會老是來寫該page。

readpage:VM呼叫,用於從後端儲存讀取資料。呼叫時,page處於lock狀態,並且在讀取結束時需要設定為unlock狀態,並設定uptodate。如果readpage處理過程中需要unlock page,則unlcok之後需要返回AOP_TRUNCATED_PAGE,呼叫者將重新定位page並重新lock,成功之後會再次呼叫readpage

sync_page:VM呼叫,用於通知後端儲存處理該page的I/O。該page所屬address space的其他Pages的I/O也可能被處理。該函式是可選的,僅用於等待PG_Writeback的page處理完成。

writepages:VM呼叫,將address space裡所有Dirty的pages寫入後端儲存。如果wbc->sync_mode是WBC_SYNC_ALL,則writeback_control會選取一個範圍的pages必須寫入。如果是WBC_SYNC_NONE,則根據引數nr_to_write儘可能寫入這麼多pages。如果沒有設定,則預設呼叫mpage_writepages()。

set_page_dirty:VM呼叫,用於設定page為dirty。通常用於address space裡有新的資料寫入,如memory mapping的page被修改。該函式將設定PageDirty標記,並在Radix樹裡設定PAGECACHE_TAG_DIRTY標識。

readpages:VM呼叫,用於讀取address space裡的指定pages。主要是通過呼叫readpage將一組pages讀取。通常用於預讀,因此讀取失敗的錯誤碼可能會被忽略。

write_begin:由通用的buffered寫流程呼叫,寫入len長度資料到檔案的offset處。address space可能需要申請額外的儲存空間來保證寫操作可以完成,或者需要從後端儲存讀取不在快取裡的pages。該函式返回的pagep要處於locked狀態,呼叫者將直接寫入資料。返回引數fsdata用於私有資料指標,它將傳遞給write_end函式。如果函式返回<0,則write_end將不會呼叫。
write_end:資料拷貝到write_begin返回的page後,呼叫write_end將page unlock,遞減引用計數並更新i_size欄位。

bmap:VFS呼叫用於對映邏輯塊的偏移和物理塊編號。該方法由FIBMAP ioctl使用,並且是swap檔案。swap系統不直接進入檔案系統,而是通過BMAP方式建立記憶體地址和檔案的塊對映,然後直接使用記憶體地址。

invalidatepage:如果設定了PagePrivate,則當Page部分或者全部從address space裡刪除時呼叫該方法。通常是因為address space裡執行了一個截斷或者是失效所有資料。和page關聯的私有資訊需要更新,或者直接被釋放(如果失效的offset為0的話,整個page將被釋放)。

releasepage:用於將PagePrivate pages釋放,它將把私有資料釋放,然後清除PagePrivate標識。releasepage有兩種使用場景,一是VM發現沒有引用計數的clean page,想將其變成free page。通過呼叫releasepage將其從address space裡摘掉變為clean page。二是有invalid請求需要將address space裡的部分或全部pages失效。通常是fadvice系統呼叫或者檔案系統自己認為快取裡的資料已經不是最新的了,此時通過呼叫invalidate_inode_pages2()將pages釋放。調動該函式前,需要保證pages已經是invalidate的。如果釋放私有資料失敗,則需要在返回錯誤之前將PageUptodate清除。

freepage:用於將不在pagecache裡的page釋放,page必須不屬於任何address space。通常有記憶體回收處理程式呼叫。
direct_IO:由通用讀寫流程呼叫,繞過pagecache,DIO方式讀取資料。
get_xip_page:VM呼叫,將block number轉換為page。支援XIP(execute in place)的檔案系統需要實現該函式。
migrate_page:在old page和new page之間遷移資料,通常用於記憶體整理(減少碎片)。遷移時需要將私有資料和引用一起遷移。
launder_page:在free之前呼叫,用於writeback dirty page。為了防止再次被設定dirty,操作過程中持有page lock。
error_remove_page:用於記憶體分配失敗的處理,如果address space支援truncation,通常設定為generic_error_remove_page()。
swap_activate and swap_deactivate:用於swapon在一個檔案上時,分配空間並將block資訊儲存在記憶體中。以及swapoff時釋放空間。

File

一個File資料結構代表一個程序裡開啟的一個檔案。所以File結構是跟程序相關的,不同的程序開啟同一個檔案會在每個程序裡都有一個File物件,對應到程序的檔案控制代碼。
同一個檔案File結構指向的inode是同一個,所以通過pagecache快取進行資料讀寫的時候,使用的是inode裡同一個address space,保證檔案資料在不同程序裡的一致性。

struct file_operations

struct file_operations的定義如下:

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
    ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
    ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long arg, struct file_lock **);
    long (*fallocate)(struct file *, int mode, loff_t offset, loff_t len);
};

同樣,如果沒有特別說明,則所有操作都在沒有鎖持有的情況下呼叫。
file_operations裡大部分函式跟POSIX檔案系統介面語義一樣,就不單獨列出了。

Dentry

dcache(dentry cache)用於快取dentry,每個dentry用於索引filename和inode number。dentry也有一套操作合集dentry operations用於管理dentry。底層檔案系統可以選擇實現自己的dentry operations來替換預設的operations。

struct dentry_operations

struct dentry_operations的定義如下:

struct dentry_operations {
    int (*d_revalidate)(struct dentry *, unsigned int);
    int (*d_weak_revalidate)(struct dentry *, unsigned int);
    int (*d_hash)(const struct dentry *, const struct inode *,
                    struct qstr *);
    int (*d_compare)(const struct dentry *, const struct inode *,
                    const struct dentry *, const struct inode *,
                    unsigned int, const char *, const struct qstr *);
    int (*d_delete)(const struct dentry *);
    void (*d_release)(struct dentry *);
    void (*d_iput)(struct dentry *, struct inode *);
    char *(*d_dname)(struct dentry *, char *, int);
    struct vfsmount *(*d_automount)(struct path *);
    int (*d_manage)(struct dentry *, bool);
};

d_revalidate:VFS用於檢查在dcache裡找到的dentry是否有效。通常設定為NULL,則只要在dcache找到即認為是有效的。但對網路檔案系統如NFS來說,dentry可能在一段時間之後就會失效,因此需要實現該函式用於檢查是否有效。如果有效,函式需要返回一個正數。
d_revalidate可能在rcu-walk模式(flags & LOOKUP_RCU)下被呼叫。此時該函式裡不能阻塞也不能寫入資料到dentry,並且d_parent和d_inode不能使用,因為他們可能瞬間就可能被修改。如果在rcu-walk模式遇到困難,則返回-ECHILD,將在ref-walk模式下重新呼叫。

d_weak_revalidate:用於檢查'jumped'的dentry,即那些不是通過lookup獲取的dentry,如'', '.'或者'..'。這種場景只需要檢查dentry對應inode是否OK即可。該函式不會在rcu-walk模式下呼叫,所以可以放心的使用inode。

d_hash:用於VFS將dentry放入HASH列表。並不清楚HASH表用來做啥,通常不需要設定它,使用VFS預設的即可。

d_compare:用於比較dentry name和指定的name。該函式必須是可重入的,即每次的返回結果一樣。
d_delete:用於引用計數遞減為0時呼叫,返回1則dcache立即刪除dentry,返回0則繼續快取該dentry。預設為NULL,則總是將dentry進行快取。該函式必須是可重入的,即每次的返回結果一樣。
d_release:用於釋放dentry資源。
d_iput:用於釋放dentry對應inode引用計數。該函式在釋放dentry之前呼叫。如果為NULL,則VFS預設呼叫iput()。
d_dname:用於生成dentry的pathname,主要是一些偽檔案系統(sockfs, pipefs等)用於延遲生成pathname。一般檔案系統不實現該函式,因為其dentry存在於dcache的hash表裡(通過pathname做hash),所以並不希望pathname變化。
d_automount:可選函式,用於穿越到一個自動掛載的dentry。它會建立一個新的vfsmount記錄,並將其返回,成功後呼叫者將根據vfsmount去嘗試mount它到掛載點。
d_manage:可選函式,用於管理從dentry進行transition。

Directory Entry Cache API

以下函式是VFS提供給檔案系統參與維護和管理的dentry cache的API介面。

dget:用於增加dentry引用計數。
dput:遞減引用計數,如果減為0,則呼叫d_delete判斷是否留在快取裡。如果判斷為否,或者該dentry已經不在其父目錄hash列表裡,則將其刪除。如果判斷為是,則dentry放入LRU連結串列,並在觸發記憶體回收時刪除。

d_drop:將dentry從其父目錄的hash列表裡刪除。隨後如果引用計數減為0,該dentry將被刪除。
d_delete:將dentry刪除。如果引用計數不為0,則呼叫d_drop。如果為0,則呼叫d_iput將dentry搞成nagtive dentry。注意該函式不是dentry operations->d_delete函式指標,而是VFS提供的API介面。

d_add:將dentry加入到父目錄的hash列表裡,並呼叫d_instantiate
d_instantiate:將dentry加入到對應的inode的hash列表裡,並更新其d_inode欄位。inode的引用計數i_count欄位需要遞增。該函式通常用於新建立inode給一個nagtive dentry。

d_lookup:根據pathname,查詢父目錄dentry下的某個dentry。如果找到,則增加引用計數並返回dentry。呼叫用完該dentry之後需要通過dput將引用計數遞減。

總結

VFS的角色包括:

  • 管理可用的檔案系統型別,將裝置和檔案系統例項進行關聯。
  • 處理檔案系統的相關操作,為應用程式提供標準檔案系統介面。

VFS和具體的檔案系統系統之間主要通過幾個數據結構:super_block, inode, dentry, file和address space以及對應的operations: sb_ops, i_ops, d_ops, f_ops和a_ops來實現檔案系統的功能。

參考

Linux Documentation: VFS