目錄
1. Linux文件系統簡介 2. 通用文件模型 3. VFS相關數據結構 4. 處理VFS對象 5. 標准函數
1. Linux文件系統簡介
Linux系統由數以萬計的文件組成,其數據存儲在硬盤或者其他塊設備(例如ZIP驅動、軟驅、光盤等)。存儲使用了層次式文件系統,文件系統使用目錄結構組織存儲的數據,並將其他元信息(例如所有者、訪問權限等)與實際數據關聯起來,其中采用了各種方法來永久存儲所需的結構和數據
Linux支持許多不同的文件系統
1. Ext2 2. Ext3 3. ReiserFS 4. XFS 5. VFAT(兼容DOS) ..
每種操作系統都至少有一種"標准文件系統",提供了一些文件操作功能,用以可靠高效地執行所需的任務,Linux附帶的Ext2/3文件系統是一種標准文件系統
不同文件系統鎖基於的概念抽象差別很大,例如
1. Ext2基於"Inode",它對每個文件單獨構造了一個單獨的管理結構即"Inode",這些元信息也存儲到磁盤上。inode包含了文件所有的元信息,以及指向相關數據塊的指針,目錄可以表示為普通文件,其數據包括了指向目錄下所有文件的inode指針,因而層次結構得以建立 2. ReiserFS廣泛應用了樹形結構來提供同樣的層次功能
為支持各種本機文件系統,且在同時允許訪問其他操作系統的文件,Linux內核提供了一個額外的軟件抽象層,將各種底層文件系統的具體特性與應用層包括內核自身隔離開來,該軟件層稱為VFS(Virtual Filesystem/Virtual Filesystem Switch 虛擬文件系統/虛擬文件系統交換器)。VFS既是向下的接口(所有文件系統都必須實現該接口,以此來和內核通信),同時也是向上的接口(用戶進程通過系統調用訪問的對外接口 實現系統調用功能)
VFS的任務很復雜,它起到承上啟下的作用
1. 一方面,它用來向用戶態提供一種操作文件、目錄及其他對象的統一方法 2. 另一方面,它必須能夠與各種方法給出的具體文件系統的底層實現進行兼容
這導致了VFS在實現上的復雜性。但VFS帶來的好處是使得Linux內核更加靈活了,Linux內核支持很多種文件系統
1. ext2 是Linux使用的,性能很好的文件系統,用於固定文件系統和可活動文件系統。它是作為ext文件系統的擴展而設計的。ext2在Linux所支持的文件系統中,提供最好的性能(在速度和CPU使用方面),ext2是Linux目前的主要文件系統 2. ext3文件系統 是對ext2增加日志功能后的擴展。它向前、向后兼容ext2。意為ext2不用丟失數據和格式化就可以轉換為ext3,ext3也可以轉換為ext2而不用丟失數據(只要重新安裝該分區就行了) 3. proc 是一種假的文件系統,用於和內核數據結構接口,它不占用磁盤空間。參考 man proc 4. Devpts 是一個虛擬的文件系統,一般安裝在/dev/pts。為了得到一個虛擬終端,進程打開/dev/ptmx,然后就可使用虛擬終端 5. raiserfs 是Linux內核2.4.1以后(2001年1 月)支持的,一種全新的日志文件系統 6. swap文件系統 swap文件系統用於Linux的交換分區。在Linux中,使用整個交換分區來提供虛擬內存,其分區大小一般應是系統物理內存的2倍,在安裝Linux 操作系統時,就應創分交換分區,它是Linux正常運行所必需的,其類型必須是swap,交換分區由操作系統自行管理 7. vfat文件系統 vfat是Linux對DOS、Windows系統下的FAT(包括fat16和Fat32)文件系統的一個統稱 8. NFS文件系統 NFS即網絡文件系統,用於在UNIX系統間通過網絡進行文件共享,用戶可將網絡中NFS服務器提供的共享目錄掛載到本地的文件目錄中,從而實現操作和訪問NFS文件系統中的內容 9. ISO 9660文件系統 文件系統中光盤所使用的標准文件系統,是一種針對ISO9660標准的CD-ROM文件系統,Linux對該文件系統也有很好的支持,不僅能讀取光盤和光盤ISO映像文件,而且還支持在Linux環境中刻錄光盤
0x1: 文件系統類型
文件系統一般可以分為以下幾種
1. 基於磁盤的文件系統(Disk-based Filesystem) 是在非易失介質存儲文件的經典方式,用以在多次會話之間保持文件的內容。實際上,大多數文件系統都由此演變而來,例如 1) Ext2/3 2) Reiserfs 3) FAT 4) ISO9660 所有這些文件系統都使用面向塊的介質,必須解決以下問題: 如何將文件內容和結構信息存儲在目錄層次結構上 2. 虛擬文件系統(Virtual Filesystem) 在內核中生成,是一種使用戶應用程序與內核通信的方法。proc文件系統就是這一類的最好示例,它不需要任何種類的硬件設備上分配存儲空間,而是內核建立了一個層次化的文件結構,其中的項包含了與系統特定部分相關的信息 ll /proc/version /* 占用空間: 0字節 -r--r--r--. 1 root root 0 Feb 27 23:39 /proc/version */ cat /proc/version /* 從內核內存中的數據結構提取出來 Linux version 2.6.32-504.el6.x86_64 (mockbuild@c6b9.bsys.dev.centos.org) (gcc version 4.4.7 20120313 (Red Hat 4.4.7-11) (GCC) ) #1 SMP Wed Oct 15 04:27:16 UTC 2014 */ 3. 網絡文件系統(Network Filesystem) 基於磁盤的文件系統和虛擬文件系統之間的折中。這種文件系統允許訪問另一台計算機上的數據,該計算機通過網絡連接到本地計算機。它仍然需要文件長度、文件在目錄層次中的位置、文件的其他重要信息。它也必須提供函數,使得用戶進程能夠執行通常的文件相關操作,如打開、讀、刪除等。由於VFS抽象層的存在,用戶空間進程不會看到本地文件系統和網絡文件系統之間的區別
2. 通用文件模型
VFS不僅為文件系統提供了方法和抽象,還支持文件系統中對象(或文件)的統一視圖。由於各個文件系統的底層實現不同,文件在不同的底層文件系統環境下特性存在微秒的差異
1. 並非所有文件系統都支持同樣的功能,而有些操作對"普通"文件是不可缺少的,卻對某些對象完全沒有意義,例如集成到VFS中的命名管道 2. 並非每一種文件系統都支持VFS中的所有抽象,例如設備文件無法存儲在源自其他系統的文件系統中(例如FAT),因為FAT的設計沒有考慮到設備文件這類對象
VFS的設計思想是提供一種結構模型,包含一個強大文件系統所應具備的所有組件,但該模型只存在於虛擬中,必須使用各種對象和函數指針與每種文件系統適配。所有文件系統的實現都必須提供與VFS定義的結構配合的例程,以彌合兩種視圖之間的差異(由底層的文件系統實現這個向上適配工作)
需要明白的是,虛擬文件系統的結構並非是憑空創造出來的,而是基於描述經典文件系統所使用的結構。VFS抽象層的組織和Ext2文件系統類似,這對基於完全不同概念的文件系統(例如ReiserFS、XFS)來說,會更加困難,但處理Ext2文件系統時會提高性能,因為在Ext2和VFS結構之間轉換,幾乎不會損失時間
另一方面來說,在處理文件時,內核空間和用戶空間所使用的主要對象是不同的
1. 對用戶程序來說 一個文件由一個"文件描述符"標識,文件描述符是一個整數,在所有有關文件的操作中用作標識文件的參數。文件描述符是在打開文件時由內核分配的,只在一個進程內部有效,兩個進程可以使用同樣的文件描述符,但二者並不指向同一個文件,基於同一個描述符來共享文件是不可能的 2. 對內核來說 內核處理文件的關鍵是inode,每個文件(目錄)都有且只有一個對應的inode,其中包含元數據(如訪問權限、上次修改時間、等等)和指向文件數據的指針。但inode並不包含文件名
關於Linux下inode、鏈接的相關知識,請參閱另一篇文章
http://www.cnblogs.com/LittleHann/p/4208619.html
0x1: 編程接口
用戶進程和內核的VFS實現之間由"系統調用"組成,其中大多數涉及對文件、目錄和一般意義上的文件系統的操作,和文件操作相關的系統調用請參閱另一篇文章
http://www.cnblogs.com/LittleHann/p/3850653.html //搜索:0x3: 系統調用分類
文件使用之前,必須用open或openat系統調用打開,在成功打開文件之后,內核向用戶層返回一個非負的整數,這種分配的文件描述符起始於3(0表示標准輸入、1表示標准輸出、2表示標准錯誤輸出)。在文件已經打開后,其名稱就沒有用處了(文件名只是在利用inode進行文件遍歷的時候起過濾作用),它現在由文件描述符唯一標識,所有其他庫函數都需要傳遞文件描述符作為一個參數(進一步傳遞到系統調用)
盡管傳統上文件描述符在內核中足以標識一個文件,但是由於多個命名空間(namespace)和容器(container)的引入,在不同層次命名空間中看到的同一個進程中的文件描述符(fd)是不同的,因為對文件的唯一表示由一個特殊的數據結構(struct file)提供
系統調用read需要將文件描述符作為第一個參數,以標識讀取數據的來源
在一個打開文件中的當前位置保存在"文件位置指針(f_pos)",這是一個整數,指定了當前位置與文件起始點的偏移量。對隨機存取文件而言,該指針可以設置成任何值,只要不超出文件存儲容量范圍即可,這用於支持對文件數據的隨機訪問。其他文件類型,如命名管道或字符設備的設備文件,不支持這種做法,它們只能從頭至尾順序讀取
系統調用close關閉與文件的"連接"(釋放文件描述符,以便在后續打開其他文件時使用)
關於struct file數據結構的相關知識,請參閱另一篇文章
http://www.cnblogs.com/LittleHann/p/3865490.html //搜索:0x1: struct file
0x2: 將文件作為通用接口(萬物皆文件)
*nix似乎基於少量審慎選擇的范型而建立的,一個非常重要的隱喻貫穿內核的始終(特別是VFS),尤其是在有關輸入和輸出機制的實現方面
大多數內核導出、用戶程序使用的函數都可以通過(VFS)定義的文件接口訪問,以下是使用文件作為其主要通信手段的一部分內核子系統
1. 字符和塊設備 2. 進程之間的管道 3. 用於所有網絡協議的套接字 4. 用戶交互式輸入和輸出的終端
要注意的是,上述的某些對象不一定聯系到文件系統中的某個項。例如,管道是通過特殊的系統調用生成,然后由內核中VFS的數據結構中管理,管道並不對應於一個可以用通常的rm、ls等命令訪問的真正的文件系統項
3. VFS相關數據結構
0x1: 結構概觀
VFS由兩個部分組成: 文件、文件系統,這些都需要管理和抽象
1. 文件的表示 inode是內核選擇用於表示文件內容和相關元數據的方法,理論上,實現這個概念只需要一個大的數據結構,其中包含了所有必要的數據,但實際上,linux內核將數據結構分散到一些列較小的、布局清晰的結構中 在抽象對底層文件系統的訪問時,並未使用固定的函數,而是使用了函數指針。這些函數指針保存在兩個結構中,包括了所有相關的函數,因為實際數據是通過具體文件系統的實現操作的,調用接口總是保持不變,但實際的工作是由特定於實現的函數完成的 1) inode操作: "struct inode"->"const struct inode_operations *i_op" 特定於inode操作有關,負責管理結構性的操作,例如創建鏈接、文件重命名、在目錄中生成新文件、刪除文件 2) 文件操作: "struct file"->"const struct file_operations *f_op;" 特定於文件的數據內容的操作,它們包含一些常用的操作(如讀和寫)、設置文件位置指針、創建內存映射等操作 除此之外,還需要其他結構來保存與inode相關的信息,特別重要的是與每個inode關聯的數據段,其中存儲了文件的內容或目錄項表。每個inode還包含了一個指向底層文件系統的超級快對象的指針(struct super_block *i_sb;),用於執行對inode本身的操作(這些操作也是通過函數指針數組實現的) 因為打開的文件總是分配到系統中的一個特定的進程,內核必須在數據結構中存儲文件和進程之間的關聯,我們知道,task_struct包含了一個成員,保存了所有打開的文件的一個數組 各個文件系統的實現也能在VFS inode中存儲自身的數據(不通過VFS層操作) /* http://www.cnblogs.com/LittleHann/p/3865490.html 搜索:0x2: struct inode 搜索:0x1: struct file */ 2. 文件系統和超級塊信息 VFS支持的文件系統類型通過一種特殊的內核對象連接進來,該對象提供了一種讀取"超級塊"的方法,除了文件系統的關鍵信息(塊長度、最大文件長度、..),超級塊還包含了讀、寫、操作inode的函數指針 內核還建立了一個鏈表,包含所有"活動"(active、或者稱為"已裝載(mounted)")文件系統的超級塊實例 超級塊結構的一個重要成員是一個列表,包括相關文件系統中所有修改過的inode(臟inode),根據該列表很容易標識已經修改過的文件和目錄,以便將其寫回到存儲介質,回寫必須經過協調,保證在一定程度上最小化開銷 1) 因為這是一個非常費時的操作,硬盤、軟盤驅動器及其他介質與系統其余組件相比,速度慢了幾個數量級 2) 另一方面,如果寫回修改數據的間隔時間太長也可能帶來嚴重后果,因為系統崩潰(停電)會導致不能恢復的數據丟失 內核會周期性掃描臟塊(dirty inode)的列表,並將修改傳輸(同步)到底層硬件 /* http://www.cnblogs.com/LittleHann/p/3865490.html 搜索:0x10: struct super_block */
值得注意的,inode和file結構體都包含了file_operations結構的指針,而inode還額外包含inode_operations結構指針
0x2: 特定於進程的信息
文件描述符(fd)用於在一個進程內唯一地標識打開的文件,這使得內核能夠在用戶進程中的描述符和內核內部使用的結構之間,建立一種關聯
struct task_struct { ... /* 文件系統信息,整數成員link_count、total_link_count用於在查找環形鏈表時防止無限循環 */ int link_count, total_link_count; //用來表示進程與文件系統的聯系,包括當前目錄和根目錄、以及chroot有關的信息 struct fs_struct *fs; //表示進程當前打開的文件 struct files_struct *files; //命名空間 strcut nsproxy *nsproxy; ... }
由於命名空間、容器的引入,從容器(container)角度看似"全局"的每個資源,都由內核包裝起來,分別由每個容器進行管理,表現出一種虛擬的分組獨立的概念。虛擬文件系統(VFS)同樣也受此影響,因為各個容器可能裝載點的不同導致不同的目錄層次結構(即在不同命名空間中看到的目錄結構不同),對應的信息包含在ns_proxy->mnt_namespacez中
關於命名空間的相關知識,請參閱另一篇文章
http://www.cnblogs.com/LittleHann/p/4026781.html //搜索:2. Linux命名空間
0x3: 文件操作
文件不能只存儲信息,必須容許操作其中的信息。從用戶的角度來看,文件操作由標准庫的函數執行。這些函數指示內核執行體統調用(VFS提供的系統調用),然后VFS系統調用執行所需的操作,當然各個文件系統實現的接口可能不同,因而VFS層提供了抽象的操作,以便將通用文件對象與具體文件系統實現的底層機制關聯起來
用於抽象文件操作的結構必須盡可能通用,以考慮到各種各樣的目標文件。同時,它不能帶有過多只適用於特定文件類型的專門操作。盡管如此,仍然必須滿足各種文件(普通文件、設備文件、..)的特殊需求,以便充分利用。可見,VFS的數據機構是一個承上啟下的關鍵層
各個file實例都包含一個指向struct file_operation實例的指針,該結構保存了指向所有可能文件操作的函數指針
如果一個對象使用這里給出的結構作為接口,那么並不必實現所有的操作,例如進程間管道只提供了少量的操作,因為剩余的操作根本沒有意義,例如無法對管道讀取目錄內容,因此readdir對於管道文件是不可用的。有兩種方法可以指定某個方法不可用
1. 將函數指針設置為NULL 2. 將函數指針指向一個占位函數,該函數直接返回錯誤值
0x4: VFS命名空間
我們知道,內核提供了實現容器的底層機制,單一的系統可以提供很多容器,但容器中的進程無法感知容器外部的情況,也無法得知所在容器有關的信息,容器彼此完全獨立,從VFS的角度來看,這意味着需要針對每個容器分別跟蹤裝載的文件系統,單一的全局視圖是不夠的
VFS命名空間是所有已經裝載的、構成某個容器目錄樹的文件系統的集合
通常調用fork、clone建立的進程會繼承其父進程的命名空間(即默認情況,新進程和父進程會存在於同一個命名空間中),但可以設置CLONE_NEWNS標志,以建立一個新的VFS命名空間
關於VFS命名空間的相關知識,請參閱另一篇文章
http://www.cnblogs.com/LittleHann/p/3865490.html //搜索:0x3: struct nsproxy
命名空間操作(mount、umount)並不作用於內核的全局數據結構,而是操作當前命名空間的實例,可以通過task_strcut的同名成員訪問,改變會影響命名空間的所有成員,因為一個命名空間中的所有進程共享同一個命名空間實例
0x5: 目錄項緩存: dentry緩存
我們知道,若干dentry描繪了一個樹型的目錄結構(dentry樹),這就是用戶所看到的目錄結構,每個dentry指向一個索引節點(inode)結構。然而,這些dentry結構並不是常駐內存的,因為整個目錄結構可能會非常大,以致於內存根本裝不下。Linux的處理方式為
1. 初始狀態下: 系統中只有代表根目錄的dentry和它所指向的inode 2. 當要打開一個文件: 文件路徑中對應的節點都是不存在的,根目錄的dentry無法找到需要的子節點(它現在還沒有子節點),這時候就要通過inode->i_op中的lookup方法來尋找需要的inode的子節點,找到以后(此時inode已被載入內存),再創建一個dentry與之關聯上
由這一過程可見,其實是先有inode再有dentry。inode本身是存在於文件系統的存儲介質上的,而dentry則是在內存中生成的。dentry的存在加速了對inode的查詢
每個dentry對象都屬於下列幾種狀態之一
1. 未使用(unused)狀態 該dentry對象的引用計數d_count的值為0,但其d_inode指針仍然指向相關的的索引節點。該目錄項仍然包含有效的信息,只是當前沒有人引用他。這種dentry對象在回收內存時可能會被釋放 2. 正在使用(inuse)狀態 處於該狀態下的dentry對象的引用計數d_count大於0,且其d_inode指向相關的inode對象。這種dentry對象不能被釋放 3. 負(negative)狀態 與目錄項(dentry)相關的inode對象不復存在(相應的磁盤索引節點可能已經被刪除),dentry對象的d_inode指針為NULL。但這種dentry對象仍然保存在dcache中,以便后續對同一文件名的查找能夠快速完成。這種dentry對象在回收內存時將首先被釋放
為了提高目錄項對象的處理效率,加速對重復的路徑的訪問,引入dentry cache(簡稱dcache),即目錄項高速緩。它主要由兩個數據結構組成:
1. 哈希鏈表(dentry_hashtable): 內存中所有活動的dentry實例在保存在一個散列表中,該散列表使用fs/dcache.c中的全局變量dentry_hashtable實現,dcache中的所有dentry對象都通過d_hash指針域鏈到相應的dentry哈希鏈表中。d_hash是一種溢出鏈,用於解決散列碰撞 2. 未使用的dentry對象鏈表(dentry_unused): 內核中還有另一個dentry鏈表,表頭是全局變量dentry_unused(在fs/dcache.c中初始化) dcache中所有處於unused狀態和negative狀態的dentry對象都通過其d_lru指針域鏈入dentry_unused鏈表(super_block->s_dentry_lru)中。該鏈表也稱為LRU鏈表 為了保證內存的充分利用,在內存中生成的dentry將在無人使用時被釋放。d_count字段記錄了dentry的引用計數,引用為0時,dentry將被釋放。 這里的釋放dentry並不是直接銷毀並回收,而是將dentry放入目錄項高速緩的LRU鏈表中(即dentry_unused指向的鏈表中)。當隊列過大,或系統內存緊缺時,最近最少使用的一些dentry才真正被釋放
目錄項高速緩存dcache是索引節點緩存icache(inode cache)的主控器(master),即dcache中的dentry對象控制着icache中的inode對象的生命期轉換。無論何時,只要一個目錄項對象存在於dcache中(非negative狀態),則相應的inode就將總是存在,因為inode的引用計數i_count總是大於0。當dcache中的一個dentry被釋放時,針對相應inode對象的iput()方法就會被調用
當尋找一個文件路徑時,對於其中經歷的每一個節點,有三種情況:
1. 對應的dentry引用計數尚未減為0,它們還在dentry樹中,直接使用即可 2. 如果對應的dentry不在dentry樹中,則試圖從LRU隊列去尋找。LRU隊列中的dentry同時被散列到一個散列表中,以便查找。查找到需要的dentry后,這個dentry被從LRU隊列中拿出來,重新添加到dentry樹中 3. 如果對應的dentry在LRU隊列中也找不到,則只好去文件系統的存儲介質里面查找inode了。找到以后dentry被創建,並添加以dentry樹中
dentry結構不僅使得易於處理文件系統,對提高系統性能也很關鍵,它們通過最小化與底層文件系統實現的通信,加速了VFS的處理。每個由VFS發送到底層實現的請求,都會導致創建一個新的dentry對象,以保存請求的結果,這些對象保存在一個緩存中,在下一次需要時可以更快速地訪問,這樣操作就能夠更快速地執行
0x6: dentry管理
關於struct dentry結構的相關知識,請參閱另一篇文章
http://www.cnblogs.com/LittleHann/p/3865490.html //搜索:0x7: struct dentry
各個dentry實例組成了一個網絡(層次目錄網絡),與文件系統的結構形成一定的映射關系。在內核中需要獲取有關文件的信息時,使用dentry對象很方便,dentry更多地體現了linux目錄組織關系,但它不是表示文件及文件內容,這一職責分配給了inode,使用dentry對象很容易找到inode實例
Relevant Link:
http://blog.csdn.net/denzilxu/article/details/9188003 http://www.cnblogs.com/hzl6255/archive/2012/12/31/2840854.html
4. 處理VFS對象
0x1: 文件系統操作
盡管"文件操作"對所有應用程序來說都屬於標准功能,但"文件系統操作"只限於少量幾個系統程序,即用於裝載和卸載文件系統的mount、unmount程序。同時還必須考慮到另一個重要的方面,即文件文件在內核中是以模塊化形式實現的,這意味着可以將文件系統編譯到內核中,而內核自身在編譯時也完全可以限制不支持某個特定的文件系統。因此,每個文件系統在使用以前必須注冊到內核,這樣內核能夠了解可用的文件系統,並按需調用裝載功能(mount)
1. 注冊文件系統
在文件系統注冊到內核時,文件系統是編譯為模塊(LKM),或者持久編譯到內核中,都沒有差別。如果不考慮注冊的時間(持久編譯到內核的文件系統在啟動時注冊,模塊化文件系統在相關模塊載入內核時注冊),在兩種情況下所用的技術是同樣的
\linux-2.6.32.63\fs\filesystems.c
/** * register_filesystem - register a new filesystem * @fs: the file system structure * * Adds the file system passed to the list of file systems the kernel * is aware of for mount and other syscalls. Returns 0 on success, * or a negative errno code on an error. * * The &struct file_system_type that is passed is linked into the kernel * structures and must not be freed until the file system has been * unregistered. */ int register_filesystem(struct file_system_type * fs) { int res = 0; struct file_system_type ** p; BUG_ON(strchr(fs->name, '.')); if (fs->next) return -EBUSY; //所有文件系統都保存在一個單鏈表中,各個文件系統的名稱存儲為字符串 INIT_LIST_HEAD(&fs->fs_supers); write_lock(&file_systems_lock); /* 在新的文件系統注冊到內核時,將逐元素掃描該單鏈表 1. 到達鏈表尾部: 將描述新文件系統的對象置於鏈表末尾,完成了向內核的注冊 2. 找到對應的文件系統: 返回一個適當的錯誤信息,表明一個文件系統不能被注冊兩次 */ p = find_filesystem(fs->name, strlen(fs->name)); if (*p) res = -EBUSY; else *p = fs; write_unlock(&file_systems_lock); return res; } EXPORT_SYMBOL(register_filesystem);
關於"struct file_system_type"的相關知識,請參閱另一篇文章
http://www.cnblogs.com/LittleHann/p/3865490.html //搜索:0x11: struct file_system_type
2. 裝載和卸載
目錄樹的裝載和卸載比僅僅注冊文件系統要復雜得多,因為后者(注冊文件系統)只需要向一個鏈表添加對象,而前者(目錄樹的裝載和卸載)需要對內核的內部數據結構執行很多操作,所以要復雜得多。文件系統的裝載由mount系統調用發起,在詳細討論各個步驟之前,我們需要闡明在現存目錄樹中裝載新的文件系統必須執行的任務,我們還需要討論用於描述裝載點的數據結構
vfsmount結構
unix采用了一種單一的文件系統層次結構,新的文件系統可以集成到其中
使用mount指令可查詢目錄樹中各種文件系統的裝載情況
在這里例子中,/mnt和/cdrom目錄被稱為"裝載點",因為這是附接(裝載)文件系統的位置。每個裝載的文件系統都有一個"本地根目錄",其中包含了系統目錄(例如對於cdrom這個裝載點來說,它的系統目錄就是src、libs)。在將文件系統裝載到一個目錄時,裝載點的內容被替換為即將裝載的文件系統的相對根目錄的內容,前一個目錄數據消失,直至新文件系統卸載才重新出現(在此期間舊文件系統的數據不會被改變,但是無法訪問)
從這個例子中可以看到,裝載是可以嵌套的,光盤裝載在/mnt/cdrom目錄中,這意味着ISO9660文件系統的相對根目錄裝載在一個reiser文件系統內部,因而與用作全局根目錄的ext2文件系統是完全分離的
在內核其他部分常見的父子關系,也可以用於更好地描述兩個文件系統之間的關系
1. ext2是/mnt中的reiserfs的父文件系統 2. /mnt/cdrom中包含的是/mnt的子文件系統,與根文件系統ext2
每個裝載的文件系統都對應於一個vfsmount結構的實例,關於結構體定義的相關知識,請參閱另一篇文章
http://www.cnblogs.com/LittleHann/p/3865490.html //搜索:0x8: struct vfsmount
超級塊管理
在裝載新的文件系統時,vfsmount並不是唯一需要在內存中創建的結構,裝載操作開始於超級塊的讀取。file_system_type對象中保存的read_super函數指針返回一個類型為super_block的對象,用於在內存中表示一個超級塊,它是借助底層實現產生的
關於struct super_block的相關知識,請參閱另一篇文章
http://www.cnblogs.com/LittleHann/p/3865490.html 搜索:0x10: struct super_block
mount系統調用
mount系統調用的入口點是sys_mount函數,\linux-2.6.32.63\fs\namespace.c
SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name, char __user *, type, unsigned long, flags, void __user *, data) { int ret; char *kernel_type; char *kernel_dir; char *kernel_dev; unsigned long data_page; /*從用戶空間復制到系統空間*/ /* 以下幾個函數將用戶態參數拷貝至內核態,在后面需要使用這些參數,包括: 1. kernel_type: 掛載文件系統類型,如ext3 2. kernel_dir: 載點路徑 3. dev_name: 設備名稱 4. data_pages: 選項信息 */ ret = copy_mount_string(type, &kernel_type); if (ret < 0) goto out_type; kernel_dir = getname(dir_name); if (IS_ERR(kernel_dir)) { ret = PTR_ERR(kernel_dir); goto out_dir; } ret = copy_mount_string(dev_name, &kernel_dev); if (ret < 0) goto out_dev; /*用戶空間復制到系統空間,拷貝整個頁面*/ ret = copy_mount_options(data, &data_page); if (ret < 0) goto out_data; /*操作主體 調用do_mount 完成主要掛載工作*/ ret = do_mount(kernel_dev, kernel_dir, kernel_type, flags, (void *) data_page); free_page(data_page); out_data: kfree(kernel_dev); out_dev: putname(kernel_dir); out_dir: kfree(kernel_type); out_type: return ret; }
調用do_mount 完成主要掛載工作
long do_mount(char *dev_name, char *dir_name, char *type_page, unsigned long flags, void *data_page) { struct path path; int retval = 0; int mnt_flags = 0; /* Discard magic */ if ((flags & MS_MGC_MSK) == MS_MGC_VAL) flags &= ~MS_MGC_MSK; /* Basic sanity checks */ if (!dir_name || !*dir_name || !memchr(dir_name, 0, PAGE_SIZE)) return -EINVAL; if (data_page) ((char *)data_page)[PAGE_SIZE - 1] = 0; /* Default to relatime unless overriden */ if (!(flags & MS_NOATIME)) mnt_flags |= MNT_RELATIME; /* Separate the per-mountpoint flags */ if (flags & MS_NOSUID) mnt_flags |= MNT_NOSUID; if (flags & MS_NODEV) mnt_flags |= MNT_NODEV; if (flags & MS_NOEXEC) mnt_flags |= MNT_NOEXEC; if (flags & MS_NOATIME) mnt_flags |= MNT_NOATIME; if (flags & MS_NODIRATIME) mnt_flags |= MNT_NODIRATIME; if (flags & MS_STRICTATIME) mnt_flags &= ~(MNT_RELATIME | MNT_NOATIME); if (flags & MS_RDONLY) mnt_flags |= MNT_READONLY; flags &= ~(MS_NOSUID | MS_NOEXEC | MS_NODEV | MS_ACTIVE | MS_NOATIME | MS_NODIRATIME | MS_RELATIME| MS_KERNMOUNT | MS_STRICTATIME); /* ... and get the mountpoint */ /*獲得安裝點path結構,用kern_path(),根據掛載點名稱查找其dentry等信息 */ retval = kern_path(dir_name, LOOKUP_FOLLOW, &path); if (retval) return retval; //LSM的hook掛載點 retval = security_sb_mount(dev_name, &path, type_page, flags, data_page); if (retval) goto dput_out; //對於掛載標志的檢查和初始化 if (flags & MS_REMOUNT) //修改已經存在的文件系統參數,即改變超級塊對象s_flags字段的安裝標志 retval = do_remount(&path, flags & ~MS_REMOUNT, mnt_flags, data_page); else if (flags & MS_BIND) //要求在系統目錄樹的另一個安裝點上得文件或目錄能夠可見 retval = do_loopback(&path, dev_name, flags & MS_REC); else if (flags & (MS_SHARED | MS_PRIVATE | MS_SLAVE | MS_UNBINDABLE)) retval = do_change_type(&path, flags); else if (flags & MS_MOVE) //改變已安裝文件的安裝點* retval = do_move_mount(&path, dev_name); else retval = do_new_mount(&path, type_page, flags, mnt_flags, dev_name, data_page); dput_out: path_put(&path); return retval; }
retval = do_new_mount(&path, type_page, flags, mnt_flags, dev_name, data_page);,該函數接手來完成接下來的掛載工作
static int do_new_mount(struct path *path, char *type, int flags, int mnt_flags, char *name, void *data) { struct vfsmount *mnt; if (!type) return -EINVAL; /* we need capabilities... 必須是root權限 */ if (!capable(CAP_SYS_ADMIN)) return -EPERM; lock_kernel(); /* 調用do_kern_mount()來完成掛載第一步 1. 處理實際的安裝操作並返回一個新的安裝文件系統描述符地址 2. 使用get_fs_type()輔助函數掃描已經注冊文件系統鏈表,找到匹配的file_system_type實例,該輔助函數掃描已注冊文件系統的鏈表,返回正確的項。如果沒有找到匹配的文件系統,該例程就自動加載對應的模塊 3. 調用vfs_kern_mount用以調用特定於文件系統的get_sb函數讀取sb結構(超級塊),並與mnt關聯,初始化mnt並返回 */ mnt = do_kern_mount(type, flags, name, data); unlock_kernel(); if (IS_ERR(mnt)) return PTR_ERR(mnt); /* do_add_mount處理一些必須的鎖定操作,並確保一個文件系統不會重復裝載到同一位置,並將創建的vfsmount結構添加到全局結構中,以便在內存中形成一棵樹結構 */ return do_add_mount(mnt, path, mnt_flags, NULL); }
調用do_kern_mount()來完成掛載第一步
struct vfsmount * do_kern_mount(const char *fstype, int flags, const char *name, void *data) { /* 首先根據文件系統名稱獲取文件系統結構file_system_type 內核中所有支持的文件系統的該結構通過鏈表保存 */ struct file_system_type *type = get_fs_type(fstype); struct vfsmount *mnt; if (!type) return ERR_PTR(-ENODEV); /* 調用vfs_kern_mount()完成主要掛載 1. 分配一個代表掛載結構的struct vfs_mount結構 2. 調用具體文件系統的get_sb方法,從name代表的設備上讀出超級塊信息 3. 設置掛載點的dentry結構為剛讀出的設備的根目錄 */ mnt = vfs_kern_mount(type, flags, name, data); if (!IS_ERR(mnt) && (type->fs_flags & FS_HAS_SUBTYPE) && !mnt->mnt_sb->s_subtype) mnt = fs_set_subtype(mnt, fstype); put_filesystem(type); return mnt; } EXPORT_SYMBOL_GPL(do_kern_mount);
至此,我們第一部分的主要工作就完成了,在該部分的核心工作就是創建一個struct vfsmount,並讀出文件系統超級塊來初始化該結構。接下來就是將該結構添加到全局結構中,這就是do_add_mount()的主要工作,真正的掛載過程在函數do_add_mount()中完成
int do_add_mount(struct vfsmount *newmnt, struct path *path, int mnt_flags, struct list_head *fslist) { int err; down_write(&namespace_sem); /* Something was mounted here while we slept 如果在獲取信號量的過程中別人已經進行了掛載,那么我們進入已掛載文件系統的根目錄 */ while (d_mountpoint(path->dentry) && follow_down(path)) ; err = -EINVAL; if (!(mnt_flags & MNT_SHRINKABLE) && !check_mnt(path->mnt)) goto unlock; /* Refuse the same filesystem on the same mount point */ err = -EBUSY; if (path->mnt->mnt_sb == newmnt->mnt_sb && path->mnt->mnt_root == path->dentry) goto unlock; err = -EINVAL; if (S_ISLNK(newmnt->mnt_root->d_inode->i_mode)) goto unlock; newmnt->mnt_flags = mnt_flags; /* 調用graft_tree()來實現文件系統目錄樹結構,其中newmnt是本次創建的vfsmount結構,path是掛載點信息 */ if ((err = graft_tree(newmnt, path))) goto unlock; if (fslist) /* add to the specified expiration list */ list_add_tail(&newmnt->mnt_expire, fslist); up_write(&namespace_sem); return 0; unlock: up_write(&namespace_sem); mntput(newmnt); return err; } EXPORT_SYMBOL_GPL(do_add_mount);
主要是將當前新建的vfsmount結構與掛載點掛鈎,並和掛載點所在的vfsmount形成一種父子關系結構。形成的結構圖如下所示
至此,整個mount過程分析完畢
Relevant Link:
http://blog.csdn.net/kai_ding/article/details/9050429 http://blog.csdn.net/ding_kai/article/details/7106973 http://www.2cto.com/os/201202/119141.html
共享子樹
對於文件系統的裝載,Linux支持一些更高級的特性,可以更好地利用命名空間機制,這些擴展裝載選項(我們將集合稱之為"共享子樹")對裝載操作實現了以下幾個新的屬性
1. 共享裝載 一組已經裝載的文件系統,裝載事件將在這些文件系統之間傳播。如果一個新的文件系統裝載到該集合的某個成員中,則裝載的文件系統就將復制到集合的所有其他成員中 2. 從屬裝載 相比共享裝載,它只是去掉了集合的所有成員之間的對稱性。集合中有一個文件系統稱之為主裝載,主裝載中的所有裝載操作都會傳播到從屬裝載中,但從屬裝載中的裝載操作不會反向傳播到主裝載中 3. 不可綁定的裝載 不能通過綁定操作復制 4. 私有裝載 本質上就是經典的UNIX裝載類型,它們可以裝載到文件系統中多個位置,但裝載事件不會傳播到這種文件系統,也不會從這種文件系統向外傳播
考慮以下這種情況
目錄/virtual包含了root文件系統3個相同的綁定裝載,/virtual/a、/virtual/b、/virtual/c。同時我們還需要任何裝載在/media中的媒介還能在/virtual/a/media中可見,即使該媒介是在裝載結構建立之后添加的,解決方案是用"共享裝載"替換"綁定裝載"。在這種情況下,任何裝載在/media中的文件系統,都可以在其共享裝載集合的其他成員(/、/virtual/a、/virtual/b、/virtual/c)中看到
如果將上文介紹的文件系統結構用作容器的基礎,一個容器的每個用戶都可以看到所有其他容器,但通常這不是我們想要的,一個解決問題的思路是將/virtual轉換為"不可綁定子樹",其內容接下來不能被綁定裝載看到,而容器中的用戶也無法看到看到外部的情況
例如下面這種情況
我們繼續思考這一場景的變形,在所有容器的用戶都應該看到裝載在/media的設備時(例如,裝載到/media/usbstick的USB存儲棒),會引發另一個問題,即/media在各個容器之間共享到導致任何容器的用戶都會看到由任何其他容器裝載的媒介。解決這個問題的思路是將/media轉換為從屬裝載,則能夠保持我們想要的特性(在從屬裝載模式下,裝載事件只會從/傳播過來),而且將各個容器彼此隔離開來
如下圖所示
可以看到,用戶A裝載的camera不能被其他任何容器看到,而USB存儲棒的裝載點則會向下傳播到/virtual的所有子目錄中
回想我們之前學習的文件系統相關數據結構,這是共享子樹的基礎,如果"MS_SHARED"、"MS_PRIVATE"、"MS_SLAVE"、"MS_UNBINDABLE"其中某個標志傳遞到mount()系統調用,則do_mount將調用do_change_type改變給定裝載的類型
long do_mount(char *dev_name, char *dir_name, char *type_page, unsigned long flags, void *data_page) { ... else if (flags & (MS_SHARED | MS_PRIVATE | MS_SLAVE | MS_UNBINDABLE)) retval = do_change_type(&path, flags); ...
\linux-2.6.32.63\fs\namespace.c
/* * recursively change the type of the mountpoint. */ static int do_change_type(struct path *path, int flag) { struct vfsmount *m, *mnt = path->mnt; //如果設置了MS_REC標志,則所有裝載的裝載類型都將遞歸地改變 int recurse = flag & MS_REC; int type = flag & ~MS_REC; int err = 0; if (!capable(CAP_SYS_ADMIN)) return -EPERM; if (path->dentry != path->mnt->mnt_root) return -EINVAL; down_write(&namespace_sem); if (type == MS_SHARED) { err = invent_group_ids(mnt, recurse); if (err) goto out_unlock; } spin_lock(&vfsmount_lock); //next_mnt提供了一個迭代器,能夠遍歷給定裝載的所有子裝載 for (m = mnt; m; m = (recurse ? next_mnt(m, mnt) : NULL)) //change_mnt_propagation負責對struct vfsmount的實例設置適當的傳播標志 change_mnt_propagation(m, type); spin_unlock(&vfsmount_lock); out_unlock: up_write(&namespace_sem); return err; }
\linux-2.6.32.63\fs\pnode.c
void change_mnt_propagation(struct vfsmount *mnt, int type) { //對於共享裝載是很簡單的,用輔助函數set_mnt_shared設置MNT_SHARED標識就足夠了 if (type == MS_SHARED) { set_mnt_shared(mnt); return; } /* 如果必須建立從屬裝載、私有裝載、或不可綁定裝載,內核必須重排裝載相關的數據結構,使得目標vfsmount實例轉化為從屬裝載,這是通過do_make_slave完成的,該函數執行以下步驟 1. 需要對指定的vfsmount實例,找到一個主裝載和任何可能的從屬裝載 1) 內核搜索共享裝載集合的各個成員,遍歷到的各個vfsmount實例中,mnt_root成員與指定的vfsmount實例的mnt_root成員相同的第一個vfsmount實例,將指定為新的主裝載 2) 如果共享裝載集合中不存在這樣的成員,則將成員鏈表中第一個vfsmount實例用作主裝載 2. 如果已經發現一個新的主裝載,那么將所述vfsmount實例以及所有從屬裝載的實例,都設置為新的主裝載的從屬裝載 3. 如果內核找不到一個新的主裝載,所述裝載的所有從屬裝載現在都是自由的,它們不再有主裝載了 4. 無論如何,都會移除MNT_SHARED標志 */ do_make_slave(mnt); //在do_make_slave執行了這些調整后,change_mnt_propagation還需要一些步驟來處理不可綁定裝載和私有裝載 if (type != MS_SLAVE) { //如果所屬裝載不是從屬裝載,則將其從從屬裝載鏈表中刪除,並將mnt_master設置為NULL list_del_init(&mnt->mnt_slave); mnt->mnt_master = NULL; //不可綁定裝載和私有裝載都有沒有主裝載,對於不可綁定裝載,將設置MS_UNBINDABLE標志,以便識別 if (type == MS_UNBINDABLE) mnt->mnt_flags |= MNT_UNBINDABLE; else mnt->mnt_flags &= ~MNT_UNBINDABLE; } }
在向新系統裝載新的文件系統時,共享子樹顯然也影響到內核的行為,決定性的步驟在attach_recursive_mnt中進行
\linux-2.6.32.63\fs\namespace.c
static int attach_recursive_mnt(struct vfsmount *source_mnt, struct path *path, struct path *parent_path) { LIST_HEAD(tree_list); struct vfsmount *dest_mnt = path->mnt; struct dentry *dest_dentry = path->dentry; struct vfsmount *child, *p; int err; if (IS_MNT_SHARED(dest_mnt)) { err = invent_group_ids(source_mnt, true); if (err) goto out; } /* 首先,函數需要調查、讀取裝載事件應該傳播到哪些裝載 propagate_mnt遍歷裝載目標的所有從屬裝載和共享裝載,並分別使用mnt_set_mountpoint將新文件系統裝載到這些文件系統中,所有受該操作影響的裝載點都在tree_list中返回 */ err = propagate_mnt(dest_mnt, dest_dentry, source_mnt, &tree_list); if (err) goto out_cleanup_ids; //如果目標裝載點是一個共享裝載,那么新的裝載及其所有子裝載都會變為共享的 if (IS_MNT_SHARED(dest_mnt)) { for (p = source_mnt; p; p = next_mnt(p, source_mnt)) set_mnt_shared(p); } spin_lock(&vfsmount_lock); if (parent_path) { detach_mnt(source_mnt, parent_path); attach_mnt(source_mnt, path); touch_mnt_namespace(parent_path->mnt->mnt_ns); } else { //最后,內核需要調用mnt_set_mountpoint、commit_tree結束裝載過程,並將修改引入到普通裝載的數據結構中 mnt_set_mountpoint(dest_mnt, dest_dentry, source_mnt); commit_tree(source_mnt); } //需要注意的是,需要對共享裝載集合的每個成員或每個從屬裝載分別調用commit_tree list_for_each_entry_safe(child, p, &tree_list, mnt_hash) { list_del_init(&child->mnt_hash); commit_tree(child); } spin_unlock(&vfsmount_lock); return 0; out_cleanup_ids: if (IS_MNT_SHARED(dest_mnt)) cleanup_group_ids(source_mnt, NULL); out: return err; }
unmount系統調用
\linux-2.6.32.63\fs\namespace.c
SYSCALL_DEFINE2(umount, char __user *, name, int, flags) { struct path path; int retval; int lookup_flags = 0; if (flags & ~(MNT_FORCE | MNT_DETACH | MNT_EXPIRE | UMOUNT_NOFOLLOW)) return -EINVAL; if (!(flags & UMOUNT_NOFOLLOW)) lookup_flags |= LOOKUP_FOLLOW; retval = user_path_at(AT_FDCWD, name, lookup_flags, &path); if (retval) goto out; retval = -EINVAL; if (path.dentry != path.mnt->mnt_root) goto dput_and_out; if (!check_mnt(path.mnt)) goto dput_and_out; retval = -EPERM; if (!capable(CAP_SYS_ADMIN)) goto dput_and_out; //實際工作委托給do_umount retval = do_umount(path.mnt, flags); dput_and_out: /* we mustn't call path_put() as that would clear mnt_expiry_mark */ dput(path.dentry); mntput_no_expire(path.mnt); out: return retval; }
\linux-2.6.32.63\fs\namespace.c
static int do_umount(struct vfsmount *mnt, int flags) { struct super_block *sb = mnt->mnt_sb; int retval; LIST_HEAD(umount_list); //LSM hook掛載點 retval = security_sb_umount(mnt, flags); if (retval) return retval; /* * Allow userspace to request a mountpoint be expired rather than * unmounting unconditionally. Unmount only happens if: * (1) the mark is already set (the mark is cleared by mntput()) * (2) the usage count == 1 [parent vfsmount] + 1 [sys_umount] */ if (flags & MNT_EXPIRE) { if (mnt == current->fs->root.mnt || flags & (MNT_FORCE | MNT_DETACH)) return -EINVAL; if (atomic_read(&mnt->mnt_count) != 2) return -EBUSY; if (!xchg(&mnt->mnt_expiry_mark, 1)) return -EAGAIN; } /* * If we may have to abort operations to get out of this * mount, and they will themselves hold resources we must * allow the fs to do things. In the Unix tradition of * 'Gee thats tricky lets do it in userspace' the umount_begin * might fail to complete on the first run through as other tasks * must return, and the like. Thats for the mount program to worry * about for the moment. */ //如果定了特定於超級塊的unmount_begin函數,則調用該函數。例如,這允許網絡文件系統在強制卸載之前,終止與遠程文件系統提供者的通信 if (flags & MNT_FORCE && sb->s_op->umount_begin) { sb->s_op->umount_begin(sb); } /* * No sense to grab the lock for this test, but test itself looks * somewhat bogus. Suggestions for better replacement? * Ho-hum... In principle, we might treat that as umount + switch * to rootfs. GC would eventually take care of the old vfsmount. * Actually it makes sense, especially if rootfs would contain a * /reboot - static binary that would close all descriptors and * call reboot(9). Then init(8) could umount root and exec /reboot. */ if (mnt == current->fs->root.mnt && !(flags & MNT_DETACH)) { /* * Special case for "unmounting" root ... * we just try to remount it readonly. */ down_write(&sb->s_umount); if (!(sb->s_flags & MS_RDONLY)) retval = do_remount_sb(sb, MS_RDONLY, NULL, 0); up_write(&sb->s_umount); return retval; } down_write(&namespace_sem); spin_lock(&vfsmount_lock); event++; if (!(flags & MNT_DETACH)) shrink_submounts(mnt, &umount_list); retval = -EBUSY; /* 如果裝載的文件系統不再需要(通過使用計數器判斷),或者指定了MNT_DETACH來強制卸載文件系統,則調用umount_tree 實際工作委托為umount_tree、release_mounts 1. umount_tree: 負責將計數器d_mounted減一 2. release_mounts: 后者使用保存在mnt_mountpoint和mnt_parent中的數據,將環境恢復到所述文件系統裝載之前的原始狀態,被卸載文件系統的數據結構,也從內核鏈表中移除 */ if (flags & MNT_DETACH || !propagate_mount_busy(mnt, 2)) { if (!list_empty(&mnt->mnt_list)) umount_tree(mnt, 1, &umount_list); retval = 0; } spin_unlock(&vfsmount_lock); if (retval) security_sb_umount_busy(mnt); up_write(&namespace_sem); release_mounts(&umount_list); return retval; }
自動過期
內核也提供了一些基礎設施,允許裝載自動過期。在任何進程或內核本身都"未使用"某個裝載時,如果使用了自動過期機制,那么該裝載將自動從vfsmount樹中移除。當前NFS和AFS網絡文件系統使用了該機制。所有子裝載的vfsmount實例,如果被認為將自動到期,都需要使用vfsmount->mnt_expire鏈表元素,將其添加到鏈表中
接下來對鏈表周期性地應用mark_mounts_for_expiry即可
\linux-2.6.32.63\fs\namespace.c
/* * process a list of expirable mountpoints with the intent of discarding any * mountpoints that aren't in use and haven't been touched since last we came * here */ void mark_mounts_for_expiry(struct list_head *mounts) { struct vfsmount *mnt, *next; LIST_HEAD(graveyard); LIST_HEAD(umounts); if (list_empty(mounts)) return; down_write(&namespace_sem); spin_lock(&vfsmount_lock); /* extract from the expiration list every vfsmount that matches the * following criteria: * - only referenced by its parent vfsmount * - still marked for expiry (marked on the last call here; marks are * cleared by mntput()) */ /* 掃描所有鏈表項,如果裝載的使用計數為1,即它只被父裝載引用,那么它處於未使用狀態,在找到這樣的未使用裝載時,將設置mnt_expiry_mark 在mark_mounts_for_expiry下一次遍歷鏈表時,如果發現未使用項設置了mnt_expiry_mark,那么將該裝載從命名空間移除 */ list_for_each_entry_safe(mnt, next, mounts, mnt_expire) { if (!xchg(&mnt->mnt_expiry_mark, 1) || propagate_mount_busy(mnt, 1)) continue; list_move(&mnt->mnt_expire, &graveyard); } while (!list_empty(&graveyard)) { mnt = list_first_entry(&graveyard, struct vfsmount, mnt_expire); touch_mnt_namespace(mnt->mnt_ns); umount_tree(mnt, 1, &umounts); } spin_unlock(&vfsmount_lock); up_write(&namespace_sem); release_mounts(&umounts); }
要注意的是,mntput負責清除mnt_expiry_mark標志位,這確保以下情形
如果一個裝載已經處於過期鏈表中,然后又再次使用,那么在接下來調用mntput將計數器減一時,不會立即過期而被移除,代碼流如下
1. mark_mounts_for_expiry將未使用的裝載標記為到期 2. 此后,該裝載再次被使用,因此其mnt_count加1,這防止了mark_mounts_for_expiry將該裝載從命名空間中移除,盡管此時仍然設置着過期標志 3. 在用mntput將使用計數減1時,該函數也會確認移除過期標記。下一周期的mark_mounts_for_expiry將照常開始工作
偽文件系統
文件系統未必需要底層塊設備支持,它們可以使用內存作為后備存儲器(如ramfs、tmpfs),或者根本不需要后備存儲器(如procfs、sysfs),其內容是從內核數據結構包含的信息生成的。偽文件系統的例子包括
1. 負責管理表示塊設備的inode的bdev 2. 處理管道的pipefs 3. 處理套接字的sockfs
所有這些都出現在/proc/filesystems中,但不能裝載,內核提供了裝載標志"MS_NOUSER",防止此類文件系統被裝載,偽文件系統的加載和卸載都和普通文件系統都調用同一套函數API。內核可以使用kern_mount、kern_mount_data裝載一個偽文件系統,最后調用vfs_kern_mount,將文件系統數據集成到VFS數據結構中。
在從用戶層裝載一個文件系統時,只有do_kern_mount並不夠,還需要將文件和目錄集成到用戶可見的空間中,該工作由graft_tree處理,但如果設置了MS_NOUSER,則graft_tree拒絕工作
static int graft_tree(struct vfsmount *mnt, struct path *path) { int err; if (mnt->mnt_sb->s_flags & MS_NOUSER) return -EINVAL; ... }
需要明白的是,偽文件系統的結構內容對內核都是可用的,文件系統庫提供了一些方法,可以用於向偽文件系統寫入數據
0x2: 文件操作
操作整個文件系統是VFS的一個重要方面,但相對而言很少發生,因為除了可移動設備之外,文件系統都是在啟動過程中裝載,在關機時卸載。更常見的是對文件的頻繁操作,所有系統進程都需要執行此類操作
為容許對文件的通用存取,而無需考慮所用的文件系統,VFS以各種系統調用的形式提供了用於文件處理的接口函數
1. 查找inode
一個主要操作是根據給定的文件名查找inode,nameidata結構用來向查找函數傳遞參數,並保存查找結果
有關struct nameidata的相關知識,請參閱另一篇文章
http://www.cnblogs.com/LittleHann/p/3865490.html //搜索:0x9: struct nameidata
內核使用path_lookup函數查找路徑或文件名
\linux-2.6.32.63\fs\namei.c
/* Returns 0 and nd will be valid on success; Retuns error, otherwise. 除了所需的名稱name和查找標志flags之外,該函數需要一個指向nameidata實例的指針,用作臨時結果的"暫存器" */ static int do_path_lookup(int dfd, const char *name, unsigned int flags, struct nameidata *nd) { //內核使用nameidata實例規定查找的起點,如果名稱以/開始,則使用當前根目錄的dentry和vfsmount實例(要考慮到chroot情況),否則從當前進程的task_struct獲得當前工作目錄的數據 int retval = path_init(dfd, name, flags, nd); if (!retval) { retval = path_walk(name, nd); } if (unlikely(!retval && !audit_dummy_context() && nd->path.dentry && nd->path.dentry->d_inode)) { audit_inode(name, nd->path.dentry); } if (nd->root.mnt) { path_put(&nd->root); nd->root.mnt = NULL; } return retval; } int path_lookup(const char *name, unsigned int flags, struct nameidata *nd) { return do_path_lookup(AT_FDCWD, name, flags, nd); }
其中的核心path_walk()的流程是一個不斷穿過目錄層次的過程,逐分量處理文件名或路徑名,名稱在循環內部分解為各個分量(通過一個或多個斜線分隔),每個分量表示一個目錄名,最后一個分量例外,總是文件名
static int path_walk(const char *name, struct nameidata *nd) { current->total_link_count = 0; return link_path_walk(name, nd); } /* * Wrapper to retry pathname resolution whenever the underlying * file system returns an ESTALE. * * Retry the whole path once, forcing real lookup requests * instead of relying on the dcache. */ static __always_inline int link_path_walk(const char *name, struct nameidata *nd) { struct path save = nd->path; int result; /* make sure the stuff we saved doesn't go away */ path_get(&save); result = __link_path_walk(name, nd); if (result == -ESTALE) { /* nd->path had been dropped */ nd->path = save; path_get(&nd->path); nd->flags |= LOOKUP_REVAL; result = __link_path_walk(name, nd); } path_put(&save); return result; } static int __link_path_walk(const char *name, struct nameidata *nd) { struct path next; struct inode *inode; int err; unsigned int lookup_flags = nd->flags; while (*name=='/') name++; if (!*name) goto return_reval; inode = nd->path.dentry->d_inode; if (nd->depth) lookup_flags = LOOKUP_FOLLOW | (nd->flags & LOOKUP_CONTINUE); /* At this point we know we have a real path component. */ for(;;) { unsigned long hash; struct qstr this; unsigned int c; nd->flags |= LOOKUP_CONTINUE; /* 檢查權限 */ err = exec_permission_lite(inode); if (err) break; this.name = name; c = *(const unsigned char *)name; //計算路徑中下一個部分的散列值 hash = init_name_hash(); do { name++; //路徑分量的每個字符都傳遞給partial_name_hash,用於計算一個遞增的散列和,當路徑分量的所有字符都已經計算,則將該散列和轉換為最后的散列值,並保存到一個qstr中 hash = partial_name_hash(c, hash); c = *(const unsigned char *)name; } while (c && (c != '/')); this.len = name - (const char *) this.name; this.hash = end_name_hash(hash); /* remove trailing slashes? */ if (!c) goto last_component; while (*++name == '/'); if (!*name) goto last_with_slashes; /* * "." and ".." are special - ".." especially so because it has * to be able to know about the current root directory and * parent relationships. */ if (this.name[0] == '.') switch (this.len) { default: break; case 2: if (this.name[1] != '.') break; /* 如果路徑分量中出現兩個點(..) 1. 當查找操作處理進程的根目錄時,沒有效果,因為根目錄是沒有父母了的 2. 如果當前目錄不是一個裝載點的根目錄,則將當前dentry對象的d_parent成員用作新的目錄,因為它總是表示父目錄 3. 如果當前目錄是一個已裝載文件系統的根目錄,保存在mnt_mountpoint和mnt_parent中的信息用於定義新的dentry和vfsmount對象 */ follow_dotdot(nd); inode = nd->path.dentry->d_inode; /* fallthrough */ case 1: continue; } /* * See if the low-level filesystem might want * to use its own hash.. */ if (nd->path.dentry->d_op && nd->path.dentry->d_op->d_hash) { err = nd->path.dentry->d_op->d_hash(nd->path.dentry, &this); if (err < 0) break; } /* This does the actual lookups.. 如果路徑分量是一個普通文件,則內核可以通過兩種方法查找對應的dentry實例(以及對應的inode) 1. 位於dentry cache中,訪問它僅需很小的延遲 2. 需要通過文件系統底層實現進行查找 */ err = do_lookup(nd, &this, &next); if (err) break; err = -ENOENT; inode = next.dentry->d_inode; if (!inode) goto out_dput; /* 處理路徑的最后一步是內核判斷該分量是否為符號鏈接look */ if (inode->i_op->follow_link) { err = do_follow_link(&next, nd); if (err) goto return_err; err = -ENOENT; inode = nd->path.dentry->d_inode; if (!inode) break; } else path_to_nameidata(&next, nd); err = -ENOTDIR; if (!inode->i_op->lookup) break; continue; /* here ends the main loop */ last_with_slashes: lookup_flags |= LOOKUP_FOLLOW | LOOKUP_DIRECTORY; last_component: /* Clear LOOKUP_CONTINUE iff it was previously unset */ nd->flags &= lookup_flags | ~LOOKUP_CONTINUE; if (lookup_flags & LOOKUP_PARENT) goto lookup_parent; if (this.name[0] == '.') switch (this.len) { default: break; case 2: if (this.name[1] != '.') break; follow_dotdot(nd); inode = nd->path.dentry->d_inode; /* fallthrough */ case 1: goto return_reval; } if (nd->path.dentry->d_op && nd->path.dentry->d_op->d_hash) { err = nd->path.dentry->d_op->d_hash(nd->path.dentry, &this); if (err < 0) break; } err = do_lookup(nd, &this, &next); if (err) break; inode = next.dentry->d_inode; if (follow_on_final(inode, lookup_flags)) { err = do_follow_link(&next, nd); if (err) goto return_err; inode = nd->path.dentry->d_inode; } else path_to_nameidata(&next, nd); err = -ENOENT; if (!inode) break; if (lookup_flags & LOOKUP_DIRECTORY) { err = -ENOTDIR; if (!inode->i_op->lookup) break; } goto return_base; lookup_parent: nd->last = this; nd->last_type = LAST_NORM; if (this.name[0] != '.') goto return_base; if (this.len == 1) nd->last_type = LAST_DOT; else if (this.len == 2 && this.name[1] == '.') nd->last_type = LAST_DOTDOT; else goto return_base; return_reval: /* * We bypassed the ordinary revalidation routines. * We may need to check the cached dentry for staleness. */ if (nd->path.dentry && nd->path.dentry->d_sb && (nd->path.dentry->d_sb->s_type->fs_flags & FS_REVAL_DOT)) { err = -ESTALE; /* Note: we do not d_invalidate() */ if (!nd->path.dentry->d_op->d_revalidate( nd->path.dentry, nd)) break; } return_base: return 0; out_dput: path_put_conditional(&next, nd); break; } path_put(&nd->path); return_err: return err; } static int exec_permission_lite(struct inode *inode) { int ret; /* 判斷inode是否定義了permission方法 */ if (inode->i_op->permission) { ret = inode->i_op->permission(inode, MAY_EXEC); if (!ret) goto ok; return ret; } ret = acl_permission_check(inode, MAY_EXEC, inode->i_op->check_acl); if (!ret) goto ok; if (capable(CAP_DAC_OVERRIDE) || capable(CAP_DAC_READ_SEARCH)) goto ok; return ret; ok: //LSM hook掛載點 return security_inode_permission(inode, MAY_EXEC); }
循環一直重復下去,直至到達文件名的末尾,如果內核發現文件名不再出現,則確認已經到達文件名末尾
do_lookup起始於一個路徑分量,並且包含最初目錄數據的nameidata實例,最終返回與之相關的inode
1. 內核首先試圖在dentry緩存中查找inode,使用__d_lookup函數,找到匹配的數據,並不意味着它是最新的,必須調用底層文件系統的dentry_operation中的d_revalidate函數,來檢查緩存項是否仍然有效 1) 如果有效: 則將其作為緩存搜索的結果返回 2) 如果無效: 必須在底層文件系統中發起一個查找操作 如果緩存沒有找到,也必須在底層文件系統中發起一個查找操作,即在內存中建立dentry結構
do_follow_link,在內核跟蹤符號鏈接時,它必須要注意死循環符號鏈接的可能性
static inline int do_follow_link(struct path *path, struct nameidata *nd) { //檢查鏈接限制 int err = -ELOOP; if (current->link_count >= MAX_NESTED_LINKS) goto loop; if (current->total_link_count >= 40) goto loop; BUG_ON(nd->depth >= MAX_NESTED_LINKS); cond_resched(); err = security_inode_follow_link(path->dentry, nd); if (err) goto loop; current->link_count++; current->total_link_count++; nd->depth++; err = __do_follow_link(path, nd); current->link_count--; nd->depth--; return err; loop: path_put_conditional(path, nd); path_put(&nd->path); return err; }
task_struct結構包含兩個計數變量,用於跟蹤連接
strcut task_struct { .. //link_count用於防止遞歸循環 int link_count; //total_link_count限制路徑名中連接的最大數目 int total_link_count; .. }
2. 打開文件
在讀寫文件之前,必須先打開文件,從應用程序的角度來看,這是通過標准庫的open函數完成的,該函數返回一個文件描述符。該函數使用了同名的open()系統調用
\linux-2.6.32.63\fs\open.c
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode) { long ret; /* 檢查是否應該不考慮用戶層傳遞的標志,總是強行設置O_LARGEFILE。如果底層處理器字長是64位系統,就需要設置這個選項 */ if (force_o_largefile()) { flags |= O_LARGEFILE; } //調用do_sys_open完成實際功能 ret = do_sys_open(AT_FDCWD, filename, flags, mode); /* avoid REGPARM breakage on x86: */ asmlinkage_protect(3, ret, filename, flags, mode); return ret; } long do_sys_open(int dfd, const char __user *filename, int flags, int mode) { /*獲取文件名稱,由getname()函數完成,其內部首先創建存取文件名稱的空間,然后從用戶空間把文件名拷貝過來*/ char *tmp = getname(filename); int fd = PTR_ERR(tmp); if (!IS_ERR(tmp)) { /* 在內核中,每個打開的文件由一個文件描述符表示,該描述符在特定於進程的數組中充當位置索引(task_strcut->files->fd_array),該數組的元素包含了file結構,其中包含了每個打開文件所有必要的信息 獲取一個可用的fd,此函數調用alloc_fd()函數從fd_table中獲取一個可用fd,並進行初始化 */ fd = get_unused_fd_flags(flags); if (fd >= 0) { /*fd獲取成功則開始打開文件,此函數是主要完成打開功能的函數,用於獲取對應文件的inode*/ struct file *f = do_filp_open(dfd, tmp, flags, mode, 0); if (IS_ERR(f)) { /*打開失敗,釋放fd*/ put_unused_fd(fd); fd = PTR_ERR(f); } else { //文件如果已經被打開了,調用fsnotify_open()函數 fsnotify_open(f->f_path.dentry); //將文件指針安裝在fd數組中,每個進程都會將打開的文件句柄保存在fd_array[]數組中 fd_install(fd, f); } } //釋放放置從用戶空間拷貝過來的文件名的存儲空間 putname(tmp); } return fd; }
do_filp_open完成查找文件inode的主要工作
struct file *do_filp_open(int dfd, const char *pathname, int open_flag, int mode, int acc_mode) { /* 若干變量聲明 */ struct file *filp; struct nameidata nd; int error; struct path path; struct dentry *dir; int count = 0; int will_write; /*改變參數flag的值,具體做法是flag+1*/ int flag = open_to_namei_flags(open_flag); /*設置訪問權限*/ if (!acc_mode) { acc_mode = MAY_OPEN | ACC_MODE(flag); } /* O_TRUNC implies we need access checks for write permissions */ /* 根據O_TRUNC標志設置寫權限 */ if (flag & O_TRUNC) { acc_mode |= MAY_WRITE; } /* Allow the LSM permission hook to distinguish append access from general write access. */ /* 設置O_APPEND標志 */ if (flag & O_APPEND) { acc_mode |= MAY_APPEND; } /* The simplest case - just a plain lookup. */ /* 如果不是創建文件 */ if (!(flag & O_CREAT)) { /* 當內核要訪問一個文件的時候,第一步要做的是找到這個文件,而查找文件的過程在vfs里面是由path_lookup或者path_lookup_open函數來完成的 這兩個函數將用戶傳進來的字符串表示的文件路徑轉換成一個dentry結構,並建立好相應的inode和file結構,將指向file的描述符返回用戶 用戶隨后通過文件描述符,來訪問這些數據結構 */ error = path_lookup_open(dfd, pathname, lookup_flags(flag), &nd, flag); if (error) { return ERR_PTR(error); } goto ok; } /* * Create - we need to know the parent. */ //path-init為查找作准備工作,path_walk真正上路查找,這兩個函數聯合起來根據一段路徑名找到對應的dentry error = path_init(dfd, pathname, LOOKUP_PARENT, &nd); if (error) { return ERR_PTR(error); } /* 這個函數相當重要,是整個NFS的名字解析函數,其實也是NFS得以構築的函數 該函數采用一個for循環,對name路徑根據目錄的層次,一層一層推進,直到終點或失敗。在推進的過程中,一步步建立了目錄樹的dentry和對應的inode */ error = path_walk(pathname, &nd); if (error) { if (nd.root.mnt) { /*減少dentry和vsmount得計數*/ path_put(&nd.root); } return ERR_PTR(error); } if (unlikely(!audit_dummy_context())) { /*保存inode節點信息*/ audit_inode(pathname, nd.path.dentry); } /* * We have the parent and last component. First of all, check * that we are not asked to creat(2) an obvious directory - that * will not do. */ error = -EISDIR; /*父節點信息*/ if (nd.last_type != LAST_NORM || nd.last.name[nd.last.len]) { goto exit_parent; } error = -ENFILE; /* 返回特定的file結構體指針 */ filp = get_empty_filp(); if (filp == NULL) { goto exit_parent; } /* 填充nameidata結構 */ nd.intent.open.file = filp; nd.intent.open.flags = flag; nd.intent.open.create_mode = mode; dir = nd.path.dentry; nd.flags &= ~LOOKUP_PARENT; nd.flags |= LOOKUP_CREATE | LOOKUP_OPEN; if (flag & O_EXCL) { nd.flags |= LOOKUP_EXCL; } mutex_lock(&dir->d_inode->i_mutex); /*從哈希表中查找nd對應的dentry*/ path.dentry = lookup_hash(&nd); path.mnt = nd.path.mnt; do_last: error = PTR_ERR(path.dentry); if (IS_ERR(path.dentry)) { mutex_unlock(&dir->d_inode->i_mutex); goto exit; } if (IS_ERR(nd.intent.open.file)) { error = PTR_ERR(nd.intent.open.file); goto exit_mutex_unlock; } /* Negative dentry, just create the file */ /*如果此dentry結構沒有對應的inode節點,說明是無效的,應該創建文件節點 */ if (!path.dentry->d_inode) { /* * This write is needed to ensure that a * ro->rw transition does not occur between * the time when the file is created and when * a permanent write count is taken through * the 'struct file' in nameidata_to_filp(). */ /*write權限是必需的*/ error = mnt_want_write(nd.path.mnt); if (error) { goto exit_mutex_unlock; } /*按照namei格式的flag open*/ error = __open_namei_create(&nd, &path, flag, mode); if (error) { mnt_drop_write(nd.path.mnt); goto exit; } /*根據nameidata 得到相應的file結構*/ filp = nameidata_to_filp(&nd, open_flag); if (IS_ERR(filp)) { ima_counts_put(&nd.path, acc_mode & (MAY_READ | MAY_WRITE | MAY_EXEC)); } /*放棄寫權限*/ mnt_drop_write(nd.path.mnt); if (nd.root.mnt) { /*計數減一*/ path_put(&nd.root); } return filp; } /* * It already exists. */ /*要打開的文件已經存在*/ mutex_unlock(&dir->d_inode->i_mutex); /*保存inode節點*/ audit_inode(pathname, path.dentry); error = -EEXIST; /*flag標志檢查代碼*/ if (flag & O_EXCL) { goto exit_dput; } if (__follow_mount(&path)) { error = -ELOOP; if (flag & O_NOFOLLOW) { goto exit_dput; } } error = -ENOENT; if (!path.dentry->d_inode) { goto exit_dput; } if (path.dentry->d_inode->i_op->follow_link) { goto do_link; } /*路徑裝化為相應的nameidata結構*/ path_to_nameidata(&path, &nd); error = -EISDIR; /*如果是文件夾*/ if (path.dentry->d_inode && S_ISDIR(path.dentry->d_inode->i_mode)) { goto exit; } ok: /* * Consider: * 1. may_open() truncates a file * 2. a rw->ro mount transition occurs * 3. nameidata_to_filp() fails due to * the ro mount. * That would be inconsistent, and should * be avoided. Taking this mnt write here * ensures that (2) can not occur. */ /*檢測是否截斷文件標志*/ will_write = open_will_write_to_fs(flag, nd.path.dentry->d_inode); if (will_write) { /*要截斷的話就要獲取寫權限*/ error = mnt_want_write(nd.path.mnt); if (error) { goto exit; } } //may_open執行權限檢測、文件打開和truncate的操作 error = may_open(&nd.path, acc_mode, flag); if (error) { if (will_write) { mnt_drop_write(nd.path.mnt); } goto exit; } filp = nameidata_to_filp(&nd, open_flag); if (IS_ERR(filp)) { ima_counts_put(&nd.path, acc_mode & (MAY_READ | MAY_WRITE | MAY_EXEC)); } /* * It is now safe to drop the mnt write * because the filp has had a write taken * on its behalf. */ //安全的放棄寫權限 if (will_write) { mnt_drop_write(nd.path.mnt); } if (nd.root.mnt) { path_put(&nd.root); } return filp; exit_mutex_unlock: mutex_unlock(&dir->d_inode->i_mutex); exit_dput: path_put_conditional(&path, &nd); exit: if (!IS_ERR(nd.intent.open.file)) { release_open_intent(&nd); } exit_parent: if (nd.root.mnt) { path_put(&nd.root); } path_put(&nd.path); return ERR_PTR(error); do_link: //允許遍歷連接文件,則手工找到連接文件對應的文件 error = -ELOOP; if (flag & O_NOFOLLOW) { //不允許遍歷連接文件,返回錯誤 goto exit_dput; } /* * This is subtle. Instead of calling do_follow_link() we do the * thing by hands. The reason is that this way we have zero link_count * and path_walk() (called from ->follow_link) honoring LOOKUP_PARENT. * After that we have the parent and last component, i.e. * we are in the same situation as after the first path_walk(). * Well, almost - if the last component is normal we get its copy * stored in nd->last.name and we will have to putname() it when we * are done. Procfs-like symlinks just set LAST_BIND. */ /* 以下是手工找到鏈接文件對應的文件dentry結構代碼 */ //設置查找LOOKUP_PARENT標志 nd.flags |= LOOKUP_PARENT; //判斷操作是否安全 error = security_inode_follow_link(path.dentry, &nd); if (error) { goto exit_dput; } //處理符號鏈接 error = __do_follow_link(&path, &nd); if (error) { /* Does someone understand code flow here? Or it is only * me so stupid? Anathema to whoever designed this non-sense * with "intent.open". */ release_open_intent(&nd); if (nd.root.mnt) { path_put(&nd.root); } return ERR_PTR(error); } nd.flags &= ~LOOKUP_PARENT; //檢查最后一段文件或目錄名的屬性情況 if (nd.last_type == LAST_BIND) { goto ok; } error = -EISDIR; if (nd.last_type != LAST_NORM) { goto exit; } if (nd.last.name[nd.last.len]) { __putname(nd.last.name); goto exit; } error = -ELOOP; //出現回環標志: 循環超過32次 if (count++==32) { __putname(nd.last.name); goto exit; } dir = nd.path.dentry; mutex_lock(&dir->d_inode->i_mutex); //更新路徑的掛接點和dentry path.dentry = lookup_hash(&nd); path.mnt = nd.path.mnt; __putname(nd.last.name); goto do_last; }
接下來,將控制權返回用戶進程,返回文件描述符之前,fd_install必須將file實例放置到進程task_struct的files_fd數組中
3. 讀取和寫入
在文件成功打開后,進程將使用內核提供的read或write系統調用,來讀取或修改文件的數據,入口例程是sys_read、sys_write
讀寫數據涉及到一個復雜的緩沖區和緩存系統,這些用於提供系統性能
回想我們之前學習的文件系統相關數據結構,這是共享子樹的基礎,如果"MS_SHARED"、"MS_PRIVATE"、"MS_SLAVE"、"MS_UNBINDABLE"其中某個標志傳遞到mount()系統調用,則do_mount將調用do_change_type改變給定裝載的類型
5. 標准函數
VFS層提供的有用資源是用於讀寫數據的標准函數,這些操作對所有文件系統來說,在一定程度上都是相同的,如果數據所在的塊是已知的,如果數數據所在的塊是已知的,則首先查詢頁緩存,如果數據並未保存在其中,則向對應的塊設備發出讀請求,如果對每個文件系統都需要實現這些操作,則會導致代碼大量復制,Linux內核應該極力避免這種情況的發生
0x1: 通用讀取例程
幾乎所有的文件系統都使用庫程序generic_file_read來讀取數據,它同步地讀取數據(從外部調用者的角度來看是同步的),即它保證在函數返回到調用者時,所需數據已經在內存中,在實現中,實際讀取操作委托給一個異步例程,然后等待該例程返回
\linux-2.6.32.63\fs\read_write.c
ssize_t do_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos) { struct iovec iov = { .iov_base = buf, .iov_len = len }; struct kiocb kiocb; ssize_t ret; //init_sync_kiocb初始化一個kiocb實例,用於控制異步輸入/輸出操作 init_sync_kiocb(&kiocb, filp); kiocb.ki_pos = *ppos; kiocb.ki_left = len; //實際共走委托給特定於文件系統的異步讀取操作,保存在struct file_operations的aio_read成員中,該例程是異步的,因此在例程返回到調用者時,無法保證數據已經讀取完畢 for (;;) { ret = filp->f_op->aio_read(&kiocb, &iov, 1, kiocb.ki_pos); //返回值-EIOCBRETRY表示讀請求正在排隊,尚未處理(底層設備會對請求進行重排優化,並cache請求) if (ret != -EIOCBRETRY) break; //在請求隊列正在排隊的情況下,wait_on_retry_sync_kiocb將一直等待,直至數據進入內存,在等待時,進程進入睡眠狀態,使得其他進程可以利用CPU wait_on_retry_sync_kiocb(&kiocb); } if (-EIOCBQUEUED == ret) ret = wait_on_sync_kiocb(&kiocb); *ppos = kiocb.ki_pos; return ret; }
1. 異步讀取
\linux-2.6.32.63\mm\filemap.c
ssize_t generic_file_aio_read(struct kiocb *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos) { struct file *filp = iocb->ki_filp; ssize_t retval; unsigned long seg; size_t count; loff_t *ppos = &iocb->ki_pos; count = 0; /* generic_segment_checks確認讀請求包含的參數有效之后,有兩種不同的讀模式需要區分 1. 如果設置了O_DIRECT標志,則數據直接讀取,不使用頁緩存,這個時候必須用generic_file_direct_IO 2. 否則調用do_generic_file_read,將對文件的讀操作轉換為對映射的讀操作 */ retval = generic_segment_checks(iov, &nr_segs, &count, VERIFY_WRITE); if (retval) return retval; /* coalesce the iovecs and go direct-to-BIO for O_DIRECT */ if (filp->f_flags & O_DIRECT) { loff_t size; struct address_space *mapping; struct inode *inode; mapping = filp->f_mapping; inode = mapping->host; if (!count) goto out; /* skip atime */ size = i_size_read(inode); if (pos < size) { retval = filemap_write_and_wait_range(mapping, pos, pos + iov_length(iov, nr_segs) - 1); if (!retval) { retval = mapping->a_ops->direct_IO(READ, iocb, iov, pos, nr_segs); } if (retval > 0) *ppos = pos + retval; if (retval) { file_accessed(filp); goto out; } } } for (seg = 0; seg < nr_segs; seg++) { read_descriptor_t desc; desc.written = 0; desc.arg.buf = iov[seg].iov_base; desc.count = iov[seg].iov_len; if (desc.count == 0) continue; desc.error = 0; do_generic_file_read(filp, ppos, &desc, file_read_actor); retval += desc.written; if (desc.error) { retval = retval ?: desc.error; break; } if (desc.count > 0) break; } out: return retval; } EXPORT_SYMBOL(generic_file_aio_read);
2. 從映射讀取
/source/mm/filemap.c
void do_generic_mapping_read(struct address_space *mapping, struct file_ra_state *ra, struct file *filp, loff_t *ppos, read_descriptor_t *desc, read_actor_t actor) { struct inode *inode = mapping->host; pgoff_t index; pgoff_t last_index; pgoff_t prev_index; unsigned long offset; /* offset into pagecache page */ unsigned int prev_offset; int error; index = *ppos >> PAGE_CACHE_SHIFT; prev_index = ra->prev_pos >> PAGE_CACHE_SHIFT; prev_offset = ra->prev_pos & (PAGE_CACHE_SIZE-1); last_index = (*ppos + desc->count + PAGE_CACHE_SIZE-1) >> PAGE_CACHE_SHIFT; offset = *ppos & ~PAGE_CACHE_MASK; /* 該函數使用映射機制,將文件中需要讀取的部分映射到內存頁中,它由一個大的無限循環組成,持續向內存頁讀入數據,直至所有文件數據(不在任何緩存中的文件數據)都傳輸到內存中 */ for (;;) { struct page *page; pgoff_t end_index; loff_t isize; unsigned long nr, ret; cond_resched(); find_page: //1. find_get_page檢查頁是否已經包含在頁緩存中 page = find_get_page(mapping, index); if (!page) { //如果沒喲iuze調用page_cache_sync_readahead發出一個同步預讀請求 page_cache_sync_readahead(mapping, ra, filp, index, last_index - index); //2. 由於預讀機制在很大程度上能夠保證數據現在已經進入緩存,因此再次調用find_get_page查找該頁這次仍然有一定的幾率失敗,那么必須直接進行讀取操作 page = find_get_page(mapping, index); if (unlikely(page == NULL)) goto no_cached_page; } //3. 如果設置了頁標志PG_readahead(內核用PageReadahead檢查),內核必須調用page_cache_async_readahead啟動一個異步預讀操作 if (PageReadahead(page)) { //這里內核並不等待預讀操作結束,只要有空閑時間,就會執行讀操作 page_cache_async_readahead(mapping, ra, filp, page, index, last_index - index); } //4. 雖然頁在緩存中,但其數據未必是最新的,這需要使用Page_Uptodate檢查 if (!PageUptodate(page)) goto page_not_up_to_date; page_ok: isize = i_size_read(inode); end_index = (isize - 1) >> PAGE_CACHE_SHIFT; if (unlikely(!isize || index > end_index)) { page_cache_release(page); goto out; } nr = PAGE_CACHE_SIZE; if (index == end_index) { nr = ((isize - 1) & ~PAGE_CACHE_MASK) + 1; if (nr <= offset) { page_cache_release(page); goto out; } } nr = nr - offset; if (mapping_writably_mapped(mapping)) flush_dcache_page(page); if (prev_index != index || offset != prev_offset) //對頁的訪問必須用mark_page_accessed標記,在需要從物理內存換出數據時,需要判斷頁的活動程度,這個標記很重要 mark_page_accessed(page); prev_index = index; //actor例程(file_read_actor)將適當的頁映射到用戶地址空間 ret = actor(desc, page, offset, nr); offset += ret; index += offset >> PAGE_CACHE_SHIFT; offset &= ~PAGE_CACHE_MASK; prev_offset = offset; page_cache_release(page); if (ret == nr && desc->count) continue; goto out; .. } EXPORT_SYMBOL(do_generic_mapping_read);
0x2: 失效機制
內存映射通常調用由VFS層提供的filemap_fault標准例程來讀取未保存在緩存中的頁
/source/mm/filemap.c
int filemap_fault(struct vm_area_struct *vma, struct vm_fault *vmf) { int error; struct file *file = vma->vm_file; struct address_space *mapping = file->f_mapping; struct file_ra_state *ra = &file->f_ra; struct inode *inode = mapping->host; pgoff_t offset = vmf->pgoff; struct page *page; pgoff_t size; int ret = 0; size = (i_size_read(inode) + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT; if (offset >= size) return VM_FAULT_SIGBUS; /* * Do we have something in the page cache already? */ page = find_get_page(mapping, offset); if (likely(page)) { /* * We found the page, so try async readahead before * waiting for the lock. */ do_async_mmap_readahead(vma, ra, file, page, offset); lock_page(page); /* Did it get truncated? */ if (unlikely(page->mapping != mapping)) { unlock_page(page); put_page(page); goto no_cached_page; } } else { /* No page in the page cache at all */ do_sync_mmap_readahead(vma, ra, file, offset); count_vm_event(PGMAJFAULT); ret = VM_FAULT_MAJOR; retry_find: //代碼從對find_lock_page的第一次調用開始,重試查找頁的操作 page = find_lock_page(mapping, offset); if (!page) goto no_cached_page; } /* * We have a locked page in the page cache, now we need to check * that it's up-to-date. If not, it is going to be due to an error. */ if (unlikely(!PageUptodate(page))) goto page_not_uptodate; /* * Found the page and have a reference on it. * We must recheck i_size under page lock. */ size = (i_size_read(inode) + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT; if (unlikely(offset >= size)) { unlock_page(page); page_cache_release(page); return VM_FAULT_SIGBUS; } ra->prev_pos = (loff_t)offset << PAGE_CACHE_SHIFT; vmf->page = page; return ret | VM_FAULT_LOCKED; no_cached_page: /* * We're only likely to ever get here if MADV_RANDOM is in * effect. */ error = page_cache_read(file, offset); /* * The page we want has now been added to the page cache. * In the unlikely event that someone removed it in the * meantime, we'll just come back here and read it again. */ if (error >= 0) goto retry_find; /* * An error return from page_cache_read can result if the * system is low on memory, or a problem occurs while trying * to schedule I/O. */ if (error == -ENOMEM) return VM_FAULT_OOM; return VM_FAULT_SIGBUS; page_not_uptodate: /* * Umm, take care of errors if the page isn't up-to-date. * Try to re-read it _once_. We do this synchronously, * because there really aren't any performance issues here * and we need to check for errors. */ ClearPageError(page); error = mapping->a_ops->readpage(file, page); if (!error) { wait_on_page_locked(page); if (!PageUptodate(page)) error = -EIO; } page_cache_release(page); if (!error || error == AOP_TRUNCATED_PAGE) goto retry_find; /* Things didn't work out. Return zero to tell the mm layer so. */ shrink_readahead_size_eio(file, ra); return VM_FAULT_SIGBUS; } EXPORT_SYMBOL(filemap_fault);
可以看到,Linux VFS的文件讀取總體原理如下
1. 從外部調用者來看,讀寫一定是同步的 2. 在VFS內部,使用異步+睡眠等待的形式進行異步讀取 3. 根據O_DIRECT標志位決定 1) 采用直接讀取(向底層硬件設備發起讀取請求) 2) 從映射中讀取
0x3: 權限檢查
vfs_permission是VFS層的標准函數,用於檢查是否允許以指定的某種權限訪問給定的inode,該權限可以是
1. MAY_READ 2. MAY_WRITE 3. MAY_EXEC
/source/fs/namei.c
int vfs_permission(struct nameidata *nd, int mask) { return permission(nd->path.dentry->d_inode, mask, nd); } int permission(struct inode *inode, int mask, struct nameidata *nd) { int retval, submask; struct vfsmount *mnt = NULL; if (nd) mnt = nd->path.mnt; if (mask & MAY_WRITE) { umode_t mode = inode->i_mode; //禁止以寫模式訪問只讀文件系統 if (IS_RDONLY(inode) && (S_ISREG(mode) || S_ISDIR(mode) || S_ISLNK(mode))) return -EROFS; //禁止以寫模式訪問不可修改的文件 if (IS_IMMUTABLE(inode)) return -EACCES; } if ((mask & MAY_EXEC) && S_ISREG(inode->i_mode)) { if (mnt && (mnt->mnt_flags & MNT_NOEXEC)) return -EACCES; } //通常的permission例程並不理解MAY_APPEND submask = mask & ~MAY_APPEND; //接下來,實際工作委托給特定於文件系統的權限檢查例程 if (inode->i_op && inode->i_op->permission) { retval = inode->i_op->permission(inode, submask, nd); if (!retval) { if ((mask & MAY_EXEC) && S_ISREG(inode->i_mode) && !(inode->i_mode & S_IXUGO)) return -EACCES; } } else { retval = generic_permission(inode, submask, NULL); } if (retval) return retval; retval = devcgroup_inode_permission(inode, mask); if (retval) return retval; //LSM Hook Point return security_inode_permission(inode, mask, nd); }
/source/fs/namei.c
generic_permission不僅需要將所述的inode和請求的權限作為參數,還需要一個回調函數check_acl,用於檢查ACL
int generic_permission(struct inode *inode, int mask, int (*check_acl)(struct inode *inode, int mask)) { int ret; /* * Do the basic POSIX ACL permission checks. 首先,內核需要判斷應該使用用戶、組、還是其他人的inode權限 1. 如果當前進程的文件系統UID與inode的UID相同,則需要使用對所有者設置的權限 2. 如果inode的GID包含在當前進程所屬組的列表中,那么需要使用組權限 3. 如果並非上述兩種情況,則需要"其他用戶"對inode的權限 */ ret = acl_permission_check(inode, mask, check_acl); if (ret != -EACCES) return ret; //如果DAC檢查失敗,並不意味着禁止所要求的操作,因為對能力的檢查可能允許該操作 /* * Read/write DACs are always overridable. * Executable DACs are overridable if at least one exec bit is set. */ if (!(mask & MAY_EXEC) || execute_ok(inode)) /* 如果進程有CAP_DAC_OVERRIDE能力,那么對以下情形,都可以授予所請求的權限 1. 讀或寫訪問,沒有請求執行訪問 2. 設置了3個可能的執行位中的一個或多個 3. inode表示目錄 */ if (capable(CAP_DAC_OVERRIDE)) return 0; /* * Searching includes executable on directories, else just read. */ mask &= MAY_READ | MAY_WRITE | MAY_EXEC; if (mask == MAY_READ || (S_ISDIR(inode->i_mode) && !(mask & MAY_WRITE))) /* 另一個發揮作用的能力是CAP_DAC_READ_SEARCH,該能力允許在讀取文件和搜索目錄時,撤銷DAC權限的設置,如果有該能力,那么對以下情形,可以允許訪問 1. 請求讀操作 2. 所述inode是一個目錄,沒有請求寫訪問 */ if (capable(CAP_DAC_READ_SEARCH)) return 0; return -EACCES; }
/source/fs/namei.c
static int acl_permission_check(struct inode *inode, int mask, int (*check_acl)(struct inode *inode, int mask)) { umode_t mode = inode->i_mode; mask &= MAY_READ | MAY_WRITE | MAY_EXEC; //如果current_fsuid與文件的UID相同,則將mode值右移6位,使得對應於"所有者"的權限位移動到最低位上 if (current_fsuid() == inode->i_uid) mode >>= 6; else { if (IS_POSIXACL(inode) && (mode & S_IRWXG) && check_acl) { int error = check_acl(inode, mask); if (error != -EAGAIN) return error; } //檢查所屬的所有組 if (in_group_p(inode->i_gid)) mode >>= 3; } //如果UID和GID的檢查都失敗,那么不需要將mode值做移位操作,因為對應於"其他用戶"的權限位本來就在最低位上 /* If the DACs are ok we don't need any capability check. 對選定的權限位進行DAC(自主訪問控制 discretionary access control) */ if ((mask & ~mode) == 0) return 0; return -EACCES; }
對Linux permission檢查邏輯梳理如下
1. ACL檢查 1) UID 2) GID 3) OTHER 2. 如果所述inode有一個相關的ACL,並且向generic_permission傳遞了一個用於ACL的權限檢查回調函數,那么在將當前進程的fsuid與所述文件的UID比較之后,將調用該回調函數,如果給出了ACL回調函數,那么DAC檢查是可以跳過的,因為ACL檢查中包含了標准的DAC檢查,否則,將直接返回ACL檢查的結果 3. 即使ACL拒絕了所要求的訪問,進程能力設置仍然可能允許該操作
Copyright (c) 2015 LittleHann All rights reserved