文件系統基礎知識
轉載:http://edsionte.com/techblog/archives/1974
1. 文件類型
普通文件(regular file):包括文本文件和二進制文件。通過open或creat系統調用函數可以創建一個普通文件。
目錄文件(directory file):也稱爲目錄,一個目錄的內容即爲該目錄下的文件和其他子目錄。通過mkdir命令可創建一個目錄。通過ls -l 某個目錄文件時,可以看到該文件的屬性的第一項爲d,即表示目錄文件。
設備文件(device file):Linux將硬件設備也當作文件來處理,通過mknod命令可以創建一個設備文件。通常設備文件分爲字設備文件(character special file),比如鍵盤和鼠標等;還有塊設備文件(block special file),比如硬盤和光驅等。
鏈接文件(symbolic link):也稱爲符號鏈接,通過ln命令可以創建一個符號鏈接。符號鏈接文件本身就是一個文件,這種文件的內容則是另一個文件(即源文件)的路徑。對符號鏈接進行讀寫等基本操作時,系統會自動將其轉化爲對源文件的相應操作。通過ls -l 某個符號鏈接文件時,可以看到文件屬性的第一項爲l,即表示目錄文件。
管道文件(pipe):用於進程間通信,也稱爲FIFO文件。通過系統調用pipe可以創建一個管道。管道文件在使用上和普通文件沒有什麼太大的區別,只不過這種文件不像普通文件那樣存儲與磁盤上,而是存儲在內存中。
套接字文件(socket):主要用於網絡通信,也可以用於一臺主機上的進程間通信。
2. Linux文件結構
Linux採用樹型結構將所有文件組織起來,也就是說,每一個Linux系統就只對應一個這樣的樹型結構。windows操作系統也採用樹型結構,但卻是每個分區對應一個樹型結構。下面對Linux根目錄下的一些常見子目錄進行說明:
/bin:存放二進制的可執行的命令文件,我們通常所使用的命令就是來自這裏。比如ls,cat等。
/sbin:存放系統管理命令的目錄,即爲使用前要加sudo的那些命令,比如fdisk等。
/mnt:存放用戶臨時安裝其他文件系統的目錄。
/dev:存放設備文件的目錄。
/etc:存放系統管理和配置文件的目錄。
/home:系統中所有用戶主目錄的彙總。通過/home/username可以訪問某個用戶的主目錄。
/tmp:公共的臨時文件目錄。相對的,每個用戶在其主目錄下也有私有的臨時文件目錄。
/lib:標準程序設計庫,又叫做動態鏈接共享庫。
/proc:這個目錄存在與內存中,不佔用磁盤空間。該目錄存放的是對內存的一次映像,我們每打印一次/proc目錄,顯示的就是當前內存的狀態。
虛擬文件系統
Linux可支持數十種文件系統,不同的文件系統可以同時共存於一個系統之中。這些不同類型的文件系統並不是各自封閉的,而很可能會進行文件複製和移動等。比如,我的PC中裝有雙系統(比如ubuntu+winXP),現在我要將XP系統E盤中的test.doc文件拷貝到ubuntu系統中的主目錄下。我們知道,XP的文件系統類型是FAT,而Linux主目錄的文件系統類型是ext3,我們如何使用一種統一的“語言”來支持這種不同文件系統上文件的移動和複製?虛擬文件系統(Virtual FileSystem,VFS)就扮演的這樣的角色,即支持跨越不同文件系統或存儲設備的文件操作。
VFS是在各種具體的文件系統之上建立了一個抽象層,它屏蔽了不同文件系統間的差異。它之所以可以將各種文件系統納入其中,是因爲它提供了一個通用的文件系統模型。在內核中,這個通用的模型具體的表現爲一組統一的抽象接口和數據結構。每個文件系統都必須實現這些統一接口,並在組織結構上與該模型保持一致。
VFS相當於一個萬能插銷座,具體的文件系統相當於插銷座內部的電路實現。不管是兩頭插銷還是三頭插銷,甚至未來出現的單頭插銷都可以使用這個萬能的插座,使用根本不必考慮這個插座內部是如何實現的。
關於VFS更加全面的解說可以看這裏。
VFS中的數據結構
上面說到,VFS是通用的文件系統模型。那麼,這個通用性具體如何實現?VFS主要通過一組數據結構來描述文件對象。其中有四個基本的結構體:
超級塊(struct super_block):它描述一個已安裝了的文件系統。
索引結點(struct inode):它描述一個文件。
目錄項(strcut dentry):它描述文件系統的層次結構。一個完整路徑的每個組成部分都是一個目錄項。比如打開/home/edsionte/code/hello.c時,內核分別爲“/”,"home/","edsionte/","code/","hello.c"創建相應的目錄項。
文件(struct file):它描述一個已被進程打開的文件。
VFS採用面向對象的思想,在上述每一個結構體中即包含描述每個文件對象屬性的數據,又包含對這些數據進行操作的函數指針結構體。也就是說,上述四個基本的結構體中,每一個結構體中又嵌套了一個子結構體,這個子結構體包含了對父結構體進行各種操作的函數指針。
上述文字是對VFS主要的幾個結構體大致的說明。下文中,將從代碼的角度分析這些結構體。
VFS中的基本數據結構
轉載:http://edsionte.com/techblog/archives/1984
本文涉及VFS中的數據結構有:struct super_block;struct inode;struct dentry;struct file;
Linux中的VFS(關於VFS更加全面的解說可以看這裏)以一組通用的數據結構來描述各種文件系統。這些數據結構分別是超級塊、索引結點、目錄項和文件。下面將分別對這些結構進行說明。
超級塊結構體
超級塊結構代表一個已經安裝了的文件系統,其存儲該文件系統的有關信息。對於一個基於磁盤的文件系統來說,這類對象存放於磁盤的特定扇區中;對於非基於磁盤的文件系統,它會在該文件系統的使用現場創建超級塊結構並存放在內存中。
在yoursource/include/linux/fs.h中有這個結構體的定義。下面對該結構的部分字段進行說明:
01 |
1318 struct super_block { |
02 |
1319 struct list_head s_list; |
04 |
1321 unsigned char s_dirt; |
05 |
1322 unsigned char s_blocksize_bits; |
06 |
1323 unsigned long s_blocksize; |
07 |
1324 loff_t s_maxbytes;
|
08 |
1325 struct file_system_type *s_type; |
09 |
1326 const struct super_operations *s_op; |
11 |
1330 unsigned long s_flags; |
12 |
1332 struct dentry *s_root; |
14 |
1342 struct list_head s_inodes; |
15 |
1347 struct list_head s_files; |
16 |
1353 struct block_device *s_bdev;
1354 struct list_head s_instances
|
17 |
1357 struct quota_info s_dquot; |
s_list:所有的超級塊形成一個雙聯表,s_list.prev和s_list.next分別指向與當前超級塊相鄰的前一個元素和後一個元素。通常我們通過list_entry宏來獲取s_list所在超級塊結構體的地址。超級塊鏈表的頭結點是變量super_blocks;
s_dev:超級塊所描述的文件系統所在設備的設備號。比如,ext2文件系統所在設備爲磁盤,則該設備號即爲該磁盤在系統中的設備號;
s_dirt:超級塊在內存中被修改後,該標誌爲1,則修改後的超級塊必須寫回磁盤。
s_dirty:所有髒inode鏈接在一起所形成的指針;
s_blocksize:以字節爲單位表示塊的大小。系統對文件的存取操作是以塊爲單位的,該值即代表這個塊的具體大小;
s_blocksize_bits:以位來表示塊的大小。比如,一個塊的大小爲1024字節,則該值爲10;
s_maxbytes:該超級塊所屬文件系統中文件的最大長度;
s_type:指向具體的文件系統類型;
s_op:指向對超級塊操作的函數指針結構體;
s_flags:安裝文件系統時的標誌,記錄比如只讀或可擦寫等這樣的標誌;
s_root:該文件系統根目錄的目錄項結構指針。利用該根目錄項,可以訪問到這個文件系統中的任何一個文件;
s_count:對該超級塊的引用計數;
s_inodes:該文件系統中所有的索引結點形成一個雙聯表,該字段存放這個鏈表的頭結點;
s_files:該文件系統中所有已被打開的文件形成一個雙聯表,該字段存放這個鏈表的頭結點;
s_instances:某個具體類型的文件系統的所有超級塊會組成一個雙聯表。這個鏈表的頭結點爲super_block,頭結點定義在該文件系統對應的file_system_type結構體中;請參考“Filesystem Type Registration”部分。每一個SuperBlock必然屬於一種文件系統類型,某一個文件系統類型可能對應多個SuperBlock,這些SuperBlock就通過s_instances形成雙向鏈表,而這個雙向鏈表的頭結點就保存在file_system_type結構體中的“struct
list_head fs_supers”。
s_id[32]:文件系統的名稱。比如ext3文件系統,該值爲“ext3”;
結構體中的每一個field就像一個個的options, 每一個option都有自己對應的值,要搞清楚每一個field的含義以及所有可能的值及其作用,需要耐心細緻的去慢慢研究體會。
在這裏要注意區分file_system_type,要關注Filesystem Type Registration部分,這部分關於於各種特定的文件系統是如何註冊到系統中的,是各種文件系統的開始和源頭。
超級塊操作結構體
從上面的字段說明可以知道,超級塊中有一個s_op字段,它指向與該超級塊相關的操作。該字段的類型爲struct super_operations,在yoursource/include/linux/fs.h中有這個結構體的定義。下面對該結構的部分字段進行說明:
01 |
1560 struct super_operations {
|
02 |
1561 struct inode *(*alloc_inode)( struct super_block *sb); |
03 |
1562 void (*destroy_inode)( struct inode *);
1563 void (*read_inode)(sturct inode *);
|
05 |
1564 void (*dirty_inode) ( struct inode *); |
06 |
1565 int (*write_inode) ( struct inode *, struct writeback_control
*wbc); |
07 |
1568 void (*put_super) ( struct super_block *); |
08 |
1569 void (*write_super) ( struct super_block *); |
這裏區分兩個概念: inode對象和特定文件系統中的inode, inode對象就是VFS中定義的這個inode對象,運行時保存在內存中,而特定文件系統的inode就是磁盤中某個文件對應的inode。
alloc_inode:創建和初始化一個新的索引結點對象(object),包括特定文件系統所需數據空間(參考inode結構體);
destroy_inode:釋放指定的索引結點對象(object);
clear_inode : 當磁盤中的inode被destroyed的時候,該函數用來執行特定文件系統響應的操作來destory 磁盤中的inode。
read_inode:從磁盤中讀取相應的數據填充到inode結構體各個fields中, inode對象中的i_ino field指向磁盤中要被讀出來的特定文件系統的inode;
put_super:釋放指定的超級塊,文件系統被卸載時使用;
write_super:如果該超級塊被修改,即s_dirt爲1時,則要將超級塊寫回磁盤中的文件系統superblock,同時還要將s_dirt重設爲0;
write_inode:將指定的inode對象寫回磁盤中特定文件系統的inode中,用於指定inode的更新;
drop_inode:釋放指定的inode,與write_inode成對出現;
當文件系統需要對其所對應的超級塊進行操作時,就應該使用超級塊操作類中的具體函數。比如,定義sb爲指向某個超級塊的指針,如果該超級塊需要將自己寫回磁盤,則應該這麼調用:
sb->s_op->write_super(sb);
可以看到,雖然write_super函數是由sb所指的超級塊所調用的,但是仍然將sb傳遞給write_super函數。
從上面的superBlock的操作函數看,主要用於操作inode和superblock, inode主要分爲操作inode對象,以及更新讀、寫、刪除磁盤中的inode; superblock主要分爲釋放superblock對象、更新磁盤中superblock, 掛載、卸載文件系統。但注意這裏雖然有在內存中創建inode對象的alloc_inode,但並沒有在磁盤中創建inode的操作,在磁盤中創建inode的操作在inode對象的i_op結構體中的create函數。
索引結點結構體
索引結點結構體用來描述存放在磁盤上的文件信息。每當內核對磁盤上的文件進行操作時,就會將該文件的信息填充到一個索引結點,可以代表一個目錄文件,可以代表一個普通的文件,也可以代表管道或者設備文件等這樣的特殊文件。因此,在索引結點結構中,會包含針對這些特殊文件的一些屬性。該結構體定義於在yoursource/include/linux/fs.h中有這個結構體的定義。下面對該結構的部分字段進行說明:
02 |
726 struct hlist_node i_hash; |
03 |
727 struct list_head i_list; |
04 |
729 struct list_head i_dentry; |
05 |
730 unsigned long i_ino; |
07 |
732 unsigned int i_nlink; |
11 |
736 unsigned int i_blkbits; |
15 |
742 struct timespec i_atime; |
16 |
743 struct timespec i_mtime; |
17 |
744 struct timespec i_ctime; |
18 |
745 blkcnt_t i_blocks; |
19 |
746 unsigned short i_bytes; |
22 |
751 const struct inode_operations *i_op; |
23 |
752 const struct file_operations *i_fop; |
24 |
753 struct super_block *i_sb; |
25 |
760 struct list_head i_devices; |
27 |
762 struct pipe_inode_info *i_pipe; |
28 |
763 struct block_device *i_bdev; |
29 |
764 struct cdev *i_cdev; |
i_hash:爲了提高查找正在被使用的inode的效率,每一個inode都會有一個hash值,所有hash值相同的inode形成一個雙鏈表。該字段包含prev和next兩個指針,分別指向上述鏈表的前一個元素和後一個元素;
i_list:VFS中使用四個鏈表來管理不同狀態的inode結點。inode_unused將當前未使用的inode鏈接起來,inode_in_use將當前正在被使用的inode鏈接起來,超級塊中的s_dirty將所有髒inode鏈接起來,i_hash將所有hash值相同的inode鏈接起來。i_list中包含prev和next兩個指針,分別指向與當前inode處於同一個狀態鏈表的前後兩個元素。
i_sb_list:每個文件系統中的inode都會形成一個雙聯表,這個雙鏈表的頭結點存放在超級塊的s_inodes中。而該字段中的prev和next指針分別指向在雙鏈表中與其相鄰的前後兩個元素;
i_dentry:所有引用該inode的目錄項將形成一個雙聯表,該字段即爲這個雙聯表的頭結點;
i_ino:索引結點號。通過“ls -i”命令可以查看文件的索引節點號;
i_count:引用計數;
i_nlink:硬鏈接數。當該inode描述一個目錄時,這個值至少爲2,因爲任何一個目錄至少包含“.”和".."這兩個目錄;
i_uid:inode所屬文件的擁有者的id,通過ls -n可查看擁有者id;
i_gid:inode所屬文件所在組的id,通過ls -n可查看組id;
i_rdev:如果該inode描述的是一個設備文件,此值爲設備號;
i_blkbits:以位爲單位的塊大小;
i_atime:文件最近一次被訪問的時間。通過ls -lu 可查看該時間;
i_mtime:文件最近一次被修改的時間,這裏的修改指文件內容被修改。通過ls -l 可查看該時間;
i_ctime:文件最近一次被修改的時間,這裏的修改除了指文件內容被修改外,更強調的是文件的屬性被修改。通過ls -lc可查看該時間;
i_blocks:文件使用塊的個數,通過ls -s可以查看該某個文件的塊使用數目;這是該文件佔用塊的個數?從而說明了文件的大小?
i_mode:文件的訪問權限;
i_op:指向索引結點操作結構體的指針;
i_fop:指向文件操作結構體的指針,這個字段用來初始化文件結構體(struct file)中的f_op字段;
i_sb:指向inode所屬文件系統的超級塊的指針;
i_pipe:如果inode所代表的文件是一個管道,則使用該字段;
i_bdev:如果inode所代表的文件是一個塊設備,則使用該字段;
i_cdev:如果inode所代表的文件是一個字符設備,則使用該字段;
索引結點操作結構體
在inode結構體中,有一個i_op字段,該字段指向與索引結點相關的操作。這個字段的類型爲struct inode_operations,在yoursource/include/linux/fs.h中有這個結構體的定義。下面只列出部分字段的說明。
01 |
1516 struct inode_operations {
|
02 |
1517 int (*create) ( struct inode *, struct
dentry *, int , struct nameidata *); |
03 |
1518 struct dentry * (*lookup) ( struct inode *, struct
dentry *, struct nameidata *); |
05 |
1519 int (*link) ( struct dentry *, struct inode
*, struct dentry *); |
06 |
1520 int (*unlink) ( struct inode *, struct
dentry *); |
07 |
1521 int (*symlink) ( struct inode *, struct dentry
*, const char *); |
08 |
1522 int (*mkdir) ( struct inode *, struct dentry
*, int ); |
09 |
1523 int (*rmdir) ( struct inode *, struct dentry
*); |
10 |
1524 int (*mknod) ( struct inode *, struct dentry
*, int ,dev_t); |
create:如果索引節點操作體所屬的inode是目錄文件(即是個目錄),那麼當在該目錄下創建或打開一個文件時,內核必須爲這個文件創建一個inode。VFS通過調用該inode的i_op->create()函數來完成上述新inode的創建。該函數的第一個參數爲該目錄的inode,第二個參數爲要打開新文件的dentry,第三個參數是對該文件的訪問權限。如果該inode是普通文件,那麼該inode永遠都不會調用這個create函數;
lookup:在目錄dir中根據dentry中的文件名查找該文件的inode;
link:用於在指定目錄下創建一個硬鏈接。這個link函數最終會被系統調用link()調用。該函數的第一個參數是原始文件的dentry,第二個參數即爲上述指定目錄的inode,第三個參數是鏈接文件的dentry。
unlink:在某個目錄下刪除指定的硬鏈接。這個unlink函數最終會被系統調用unlink()調用。 第一個參數即爲上述硬鏈接所在目錄的inode,第二個參數爲要刪除文件的dentry。
symlink:在某個目錄下新建一個軟連接。
mkdir:在指定的目錄下創建一個子目錄,當前目錄的inode會調用i_op->mkdir()。該函數會被系統調用mkdir()調用。第一個參數即爲指定目錄的inode,第二個參數爲子目錄的dentry,第三個參數爲子目錄權限;
rmdir:從inode所描述的目錄中刪除一個指定的子目錄時,該函數會被系統調用rmdir()最終調用;
mknod:在指定的目錄下創建一個特殊文件,比如管道、設備文件或套接字等。
About the field i_fop(file的操作結構體) from Understanding the linux kernel P473:
![]()
上面的意思是說每一個特定的文件系統都包含一套它自己的文件操作函數,比如讀、寫一個文件。當Kernel把一個inode從磁盤load到內存中時,內核把這些文件操作方法保存在一個結構體file_operations指針中,該指針就保存在inode對象的i_fop中。當一個進程打開一個文件時,VFS用保存在inode對象中的i_fop初始化file對象的f_op。當然如果有必要,VFS可能後來會修改file結構體中的f_op。綜上所述,一般來講,file對象中的f_op來自inode對象的i_fop,而inode對象的i_fop來自特定文件系統的操作函數集。
關於鏈接
UNIX文件系統提供了一種將不同文件鏈接至同一個文件的機制,我們稱這種機制爲鏈接。它可以使得單個程序對同一文件使用不同的名字。這樣的好處是文件系統只存在一個文件的副本。系統簡單地通過在目錄中建立一個新的登記項來實現這種連接。該登記項具有一個新的文件名和要連接文件的inode號(inode與原文件相同)。不論一個文件有多少硬鏈接,在磁盤上只有一個描述它的inode,只要該文件的鏈接數不爲0,該文件就保持存在。硬鏈接不能對目錄建立硬鏈接!
硬連接是直接建立在節點表上的(inode),建立硬連接指向一個文件的時候,會更新節點表上面的計數值。舉個例子,一個文件被連接了兩次(硬連接),這個文件的計數值是3,而無論通過3個文件名中的任何一個訪問,效果都是完全一樣的,但是如果刪除其中任意一個,都只是把計數值減1,不會刪除實際的內容的,(任何存在的文件本身就算是一個硬連接)只有計數值變成0也就是沒有任何硬連接指向的時候纔會真實的刪除內容。
軟鏈接(symbolic link) ln-s
我們把符號鏈接稱爲軟鏈接,它是指向另一個文件的特殊文件,這種文件的數據部分僅包含它所要鏈接文件的路徑名。軟鏈接是爲了克服硬鏈接的不足而引入的,軟鏈接不直接使用inode號作爲文件指針,而是使用文件路徑名作爲指針(軟鏈接:文件名 + 數據部分-->目標文件的路徑名)。軟件有自己的inode,並在磁盤上有一小片空間存放路徑名。因此,軟鏈接能夠跨文件系統,也可以和目錄鏈接!其二,軟鏈接可以對一個不存在的文件名進行鏈接,但直到這個名字對應的文件被創建後,才能打開其鏈接。
目錄項結構體
爲了方便對目標文件的快速查找,VFS引入了目錄項。這裏是一個dentry cache的概念。目標文件路徑中的每一項都代表一個目錄項,比如/home/test.c中,/,home,test.c都分別是一個目錄項。這些目錄項都屬於路徑的一部分,並且每個目錄項都與其對應的inode相聯繫。如果VFS得到了某個dentry,那麼也就隨之得到了這個目錄項所對應文件的inode,這樣就可以對這個inode所對應的文件進行相應操作。所以,依次沿着目標文件路徑中各部分的目錄項進行搜索,最終則可找到目標文件的inode。
與超級塊和索引結點不同的是,目錄項在磁盤上並沒有對應的實體文件,它會在需要時候現場被創建。因此,在目錄項結構體中並沒有髒數據字段,因爲目錄項並不會涉及重寫到磁盤。
dentry對象主要包含兩方面的信息: 1. 對應的文件或目錄的名字及其對應的inode對象;2. 描述目錄樹,方便快速查找文件或目錄。剩下的主要是dentry操作結構體以及一些維護dentry的信息。
目錄項由struct dentry描述,在yoursource/include/linux/dcache.h中有這個結構的定義。下面只對部分字段進行說明。
03 |
91 unsigned int d_flags; |
06 |
94 struct inode *d_inode; |
09 |
101 struct dentry *d_parent; |
10 |
102 struct qstr d_name; |
12 |
104 struct list_head d_lru; |
14 |
109 struct list_head d_child; |
15 |
110 struct rcu_head d_rcu; |
17 |
112 struct list_head d_subdirs; |
18 |
113 struct list_head d_alias; |
19 |
115 const struct dentry_operations *d_op; |
20 |
116 struct super_block *d_sb; |
d_count:引用計數;
d_inode:與該目錄項相關聯的inode對象;
d_hash:內核使用哈希表對所有dentry進行管理,該字段使得當前dentry處於哈希表的某個衝突鏈表當中;
d_parent:指向父目錄的目錄項;
d_name:目錄項的名稱;
d_subdirs:如果當前目錄項是一個目錄,那麼該目錄下所有的子目錄形成一個鏈表。該字段是這個鏈表的表頭;
d_child:如果當前目錄項是一個目錄,那麼該目錄項通過這個字段加入到父目錄的d_subdirs鏈表當中。這個字段中的next和prev指針分別指向父目錄中的另外兩個子目錄;
d_alias:一個inode可能對應多個目錄項,所有的目錄項形成一個鏈表。inode結構中的i_dentry即爲這個鏈表的頭結點。當前目錄項以這個字段處於i_dentry鏈表中。該字段中的prev和next指針分別指向與該目錄項同inode的其他兩個(如果有的話)目錄項;
d_op:指向目錄項操作結構體的指針;
d_sb:指向該目錄項所屬的文件系統對應的超級塊;
目錄項操作結構體
1 |
134 struct dentry_operations {
|
2 |
135 int (*d_revalidate)( struct dentry *, struct nameidata
*); |
3 |
136 int (*d_hash) ( struct dentry *, struct qstr
*); |
4 |
137 int (*d_compare) ( struct dentry *, struct qstr
*, struct qstr *); |
5 |
138 int (*d_delete)( struct dentry *); |
6 |
139 void (*d_release)( struct dentry *); |
7 |
140 void (*d_iput)( struct dentry *, struct
inode *); |
8 |
141 char *(*d_dname)( struct dentry *, char *, int ); |
文件結構體
VFS使用struct file來描述一個已經被進程打開的文件。文件對象是VFS與應用層進程進行一切交互的接口。與上述三個結構體不同,文件結構體是進程直接處理的對象。因此,在該結構體中你可以看到我們熟悉的一些文件屬性信息。文件對象是已打開文件在內存中的表示,因此,它在磁盤上並沒有與之對應的數據。也就是說,文件對象只存在於內存中,所以這個結構也就不涉及髒數據字段和是否需要寫回磁盤,因爲文件對象的作用是VFS與應用層進程進行交互的。
![]()
由上述可知,每當一個進程打開一個文件時,內存中有會有相應的file結構體。因此,當一個文件被多個進程打開時,這個文件就會有多個對應的文件結構體。但是,這些文件結構體對應的索引結點inode和目錄項dentry卻是唯一的只有一個。在yoursource/include/linux/fs.h中有這個結構體的定義。下面只列出部分字段的說明。
04 |
915 struct list_head fu_list; |
05 |
916 struct rcu_head fu_rcuhead; |
07 |
918 struct path f_path; |
08 |
919#define f_dentry f_path.dentry |
09 |
920#define f_vfsmnt f_path.mnt |
10 |
921 const struct file_operations *f_op; |
11 |
922 spinlock_t f_lock; |
13 |
924 int f_sb_list_cpu; |
15 |
926 atomic_long_t f_count; |
16 |
927 unsigned int f_flags; |
19 |
930 struct fown_struct f_owner; |
20 |
935#ifdef CONFIG_SECURITY |
24 |
939 void *private_data; |
[GX] 注意到file 結構體中並沒有文件名和對應的inode信息,這兩方面的信息獨立出來放在了dentry結構體中(file中有一個指針f_dentry指向這個dentry),在dentry結構體中有d_name來存儲文件的名字,dentry結構體中還有指向inoded對象的指針d_inode,在inode結構體中有一個field叫i_ino存儲着inode
number。所以file結構體間接的存儲着其對應的inode的inode number以及該file的name。而處在file結構體與inode number中間的dentry只是爲了加快lookup的速度而引入的一種dentry cache機制。本質上file對象、dentry對象、inode對象都是一種cache機制,如果沒有這三個在內存中的對象,VFS在接收到進程的系統調用,根據系統調用中所給的文件路徑名去訪問磁盤中的文件時,可以用最笨最傻的方法去訪問硬盤(實際上應該是特定的文件系統),即根據目錄文件的inode的數據塊中存儲的子目錄名或文件名及其inode
number(子目錄名或文件名和inode number組成了目錄文件數據塊中的一條entry)找到文件最終對應的inode number,但是顯然這樣的效率是極其低下的,但第一次打開一個從未打開的文件應該就是這樣的最笨最傻的方法來實現的,只是做完了後會保存到dentry cache和inode cache中,下次再使用這個文件就不用最笨最傻的方法找這個文件了。所以爲了提高效率,把上述最笨最傻的查找過程中用到的信息歸納總結成file、dentry、inode這三個對象保存在內存中cache起來。文件路徑中只爲最後一個component創建file對象,無論最後一個component是文件還是目錄,其他的component只創建dentry對象和inode對象,並把dentry對象插入dentry
cache中,把inode對象插入inode cache中,方便下次快速查找。file/dentry/inode三個對象中並沒有該文件在block deivce中哪個位置的信息,需要依賴底層的特定filesystem根據file name到block device中找,每種filesystem有各自的實現方式。因此VFS採用的是特定文件系統提供的接口來實現VFS的功能,進而實現對硬盤的訪問。
關於dentry主要有兩方面的作用:<1>將文件名與inode對象關聯起來 <2>保存從根目錄"/"到最後一個component的目錄拓撲結構,以加快下次查詢的速度。
fu_list:每個文件系統中以被打開的文件都會形成一個雙聯表,這個雙聯表的頭結點存放在超級塊的s_files字段中。該字段的prev和next指針分別指向在鏈表中與當前文件結構體相鄰的前後兩個元素;
f_dentry:與該文件對應的dentry;
f_vfsmnt:該文件所在文系統的安裝點,與f_dentry相結合可以得到該文件的絕對路徑;
f_op:指向與該文件相關的操作的結構體;
f_count:該文件的引用計數;
f_flags:進程打開該文件時候的標誌,比如以只讀,可讀寫等方式打開該文件;
f_mode:該文件的訪問權限;
f_pos:當前該文件的偏移量,讀寫操作均從該偏移量開始;
f_security:指向文件安全數據結構struct file_security_struct的指針;
文件操作結構體
VFS使用struct file_operations來描述與文件相關的操作集合,file對象中的f_op字段就指向這種結構體類型。在這個操作結構體中,有許多我們所熟悉的函數指針,大多數與文件相關的系統調用最終會調用這裏的函數。當一個進程打開某個文件時,該文件結構體中的f_op字段是通過該文件inode對象中的i_fop字段來初始化的,而通常inode對象中的i_fop來自特定文件系統的文件操作函數集。在yoursource/include/linux/fs.h 中有這個結構體的定義。下面只列出部分字段的說明。
01 |
1488 struct file_operations {
|
02 |
1489 struct module *owner; |
03 |
1490 loff_t (*llseek) ( struct file *, loff_t, int ); |
04 |
1491 ssize_t (*read) ( struct file *, char __user *, size_t ,
loff_t *); |
05 |
1492 ssize_t (*write) ( struct file *, const char __user
*, size_t , loff_t *); |
07 |
1499 int (*mmap) ( struct file *, struct vm_area_struct
*); |
08 |
1500 int (*open) ( struct inode *, struct
file *); |
09 |
1502 int (*release) ( struct inode *, struct file
*); |
owner:用於指定擁有這個文件操作結構體的模塊,通常取THIS_MODULE;
llseek:用於設置文件的偏移量。第一個參數指明要操作的文件,第二個參數爲偏移量,第三個參數爲開始偏移的位置(可取SEEK_SET,SEEK_CUR和SEEK_END之一)。
read:從文件中讀數據。第一個參數爲源文件,第二個參數爲目的字符串,第三個參數指明欲讀數據的總字節數,第四個參數指明從源文件的某個偏移量處開始讀數據。由系統調用read()調用;
write:往文件裏寫數據。第一個參數爲目的文件,第二個參數源字符串,第三個參數指明欲寫數據的總字節數,第四個參數指明從目的文件的某個偏移量出開始寫數據。由系統調用write()調用;
mmap:將指定文件映射到指定的地址空間上。由系統調用mmap()調用;
open:打開指定文件,並且將這個文件和指定的索引結點關聯起來。由系統調用open()調用;
release:釋放以打開的文件,當打開文件的引用計數(f_count)爲0時,該函數被調用;
與文件系統相關的數據結構
轉載: http://edsionte.com/techblog/archives/2033
本文涉及VFS中數據結構有:struct file_system_type; struct vfsmount; struct fs_struct; struct files_struct; struct nameidata; struct qstr;
VFS中使用file_system_type結構來描述一個具體的文件系統類型。也就是說,Linux支持的所有文件系統類型都分別唯一的對應一個file_system_type結構體。如果某個具體文件類型的文件系統被安裝到了系統中,那麼系統就會創建一個vfsmount結構。這個結構用來描述一種文件系統類型的一個安裝實例。
struct file_system_type
該結構位於yoursource/include/linux/fs.h中,下面對該結構中部分字段進行說明:
01 |
1736 struct file_system_type {
|
04 |
1739 int (*get_sb) ( struct file_system_type *, int , |
05 |
1740 const char *, void *, struct vfsmount
*); |
06 |
1741 void (*kill_sb) ( struct super_block *); |
07 |
1742 struct module *owner; |
08 |
1743 struct file_system_type * next; |
09 |
1744 struct list_head fs_supers; |
11 |
1746 struct lock_class_key s_lock_key; |
12 |
1747 struct lock_class_key s_umount_key; |
13 |
1748 struct lock_class_key s_vfs_rename_key; |
15 |
1750 struct lock_class_key i_lock_key; |
16 |
1751 struct lock_class_key i_mutex_key; |
17 |
1752 struct lock_class_key i_mutex_dir_key; |
18 |
1753 struct lock_class_key i_alloc_sem_key; |
name:文件系統的名字,不能爲空;
get_sb:在安裝文件系統時,調用此指針所指函數以在磁盤中獲取超級塊;
kill_sb:卸載文件文件系統時候,調用此指針所指函數以進行一些清理工作;
owner:如果一個文件系統以模塊的形式加載到內核,則該字段用來說明哪個模塊擁有這個結構。一般爲THIS_MODULE;
next:所有的文件系統類型結構形成一個鏈表,該鏈表的頭指針爲全局變量file_systems(struct file_system_type *file_systems)。這個字段指向鏈表中下一個文件系統類型結構;
fs_supers:同一個文件系統類型下的所有超級塊形成一個雙聯表,這個字段是這個雙聯表的頭結點。超級塊之間通過s_instances字段相互鏈接,見super_block對象中的s_instances;
struct vfsmount
在解釋這個結構的相關字段之前,我們很有必要了解一些概念。當我們安裝linux後,硬盤上就有了一個分區爲ext3(或ext4)文件系統,這個文件系統被稱之爲根文件系統。系統安裝完畢後,如果要繼續安裝其他文件系統,就需要掛載(mount)。具體的,將要安裝的文件系統的根目錄掛載到根文件系統的某個子目錄上。這樣,新安裝的文件系統的根目錄就是父文件系統下的某個子目錄,我們將這個子目錄稱爲安裝點(mount point)。
Linux支持同一個文件系統掛載在不同的mount point上,但是這些不同的mount point只對應唯一一個super_block。同事同一個mount_point也支持掛載多個文件系統,最新掛載的文件系統會隱藏之前掛載的文件系統。因此掛載的文件系統就形成了一個層級結構:一個文件系統的mount point可能是第二個文件系統的一個目錄,第二個文件系統反過來掛載在第三個文件系統的目錄下。顯然跟蹤記錄每一個掛載的文件系統很快就變成一場噩夢,對每一個掛載操作,kernel必須在內存中記錄其掛載點和掛載flag,以及掛載的文件系統之間的關係,這些信息就保存在vfsmount結構中。因此super_block側重於描述單個文件系統的信息,而vfsmount側重於描述文件系統的mount
point信息以及與其他掛載的文件系統之間的目錄關係。
該結構存儲在yoursource/include/linux/mount.h中,下面對該結構的部分字段進行說明:
02 |
50 struct list_head mnt_hash; |
03 |
51 struct vfsmount *mnt_parent; |
04 |
52 struct dentry *mnt_mountpoint; |
05 |
53 struct dentry *mnt_root; |
06 |
54 struct super_block *mnt_sb; |
07 |
55 struct list_head mnt_mounts; |
08 |
56 struct list_head mnt_child; |
10 |
63 const char *mnt_devname; |
11 |
64 struct list_head mnt_list; |
15 |
78 atomic_t mnt_count; |
16 |
79 int mnt_expiry_mark; |
mnt_hash:內核通過哈希表對vfsmount進行管理,當前vfsmount結構通過該字段鏈入相應哈希值對應的鏈表當中;
mnt_parent:指向父文件系統對應的vfsmount結構;
mnt_mountpoint:指向該文件系統安裝點對應的dentry;
mnt_root:該文件系統對應的設備根目錄的dentry;(兩者是否指向同一個dentry?mnt_root是指向該文件系統設備根目錄的dentry,具體這個dentry是什麼?)
mnt_sb:指向該文件系統對應的超級塊,一個vfsmount對應一個super_block; 但一個super_blokc可能有多個mount point,linux支持將一個文件系統掛載在多個mount point上。
mnt_child:同一個父文件系統中的所有子文件系統通過該字段鏈接成雙聯表;
mnt_mounts:該字段是上述子文件系統形成的鏈表的頭結點;
mnt_list:所有已安裝的文件系統的vfsmount結構通過該字段鏈接在一起;
與路徑查找有關的輔助結構
我們在使用open系統調用時,給該函數的第一個參數傳遞的是文件路徑名。open函數對文件的操作最終會轉化爲對該文件inode的操作。VFS爲了識別目標文件,會沿着路徑逐層查找。因此,VFS中引入了nameidata結構。nameidata結構用於在路徑查找過程中記錄中間信息和查找結果。該結構體定義在yoursource/include/linux/namei.h中。
05 |
22 unsigned int flags; |
07 |
24 unsigned int depth;
|
08 |
25 char * saved_names[MAX_NESTED_LINKS + 1]; |
12 |
29 struct open_intent open; |
4 |
36 const unsigned char * name; |
與進程有關的數據結構
struct fs_struct
每個進程都有自己的根目錄和當前的工作目錄,內核使用struct fs_struct來記錄這些信息,進程描述符中的fs字段便是指向該進程的fs_struct結構。該結構定義於yoursource/include/linux/fs_struct.h結構中。
6 |
11 struct path root, pwd; |
users:用戶數量;
lock:保護該結構的自旋鎖;
umask:打開文件時設置的文件訪問權限;
paroot:進程的根目錄;
pwd:進程的當前工作目錄;
struct files_struct
一個進程可能打開多個文件。所有被某個進程已打開的文件使用struct files_struct來記錄。進程描述符的files字段便指向該進程的files_struct結構。該結構定義於yoursource/include/linux/fdtable.h中。
01 |
44 struct files_struct { |
03 |
49 struct fdtable *fdt; |
04 |
50 struct fdtable fdtab; |
05 |
54 spinlock_t file_lock ____cacheline_aligned_in_smp; |
07 |
56 struct embedded_fd_set close_on_exec_init; |
08 |
57 struct embedded_fd_set open_fds_init; |
09 |
58 struct file * fd_array[NR_OPEN_DEFAULT]; |
count:引用計數;
fdtab:內核專門使用fdtable結構(該結構也稱爲文件描述符表)來描述文件描述符。該字段爲初始的文件描述符表;
fdt:指向fdtab描述符表;
next_fd:最近關閉的文件描述符中數值最小的下一個可用的文件描述符;
close_on_exec_init:執行exec()時需要關閉的文件描述符;
open_fds_init:當前已經打開的文件描述符;
fd_array[NR_OPEN_DEFAULT]:文件對象的初始化數組;
我們都知道open函數正確執行後會返回一個整形的文件描述符,其實這個整數便是fd_arrary數組的下標。我們所說的標準輸入文件、標準輸出文件和標準錯誤文件分別是這個數組的0、1和2號元素。
通常在x86體系架構中NR_OPEN_DEFAULT的大小爲32,難道一個進程最多只可以打開32個文件嗎?當然不是,我們在上述的字段描述中,對fdtab和fd_array都用“初始化”來修飾。也就是說,當一個進程打開的文件數目超過32的時候,內核就分配一個更大的文件對象指針數組(fd_array中的文件對象指針也會被“複製”新的數組中),以便可以存放更多打開文件所對應的file結構體的指針。
上述字段已經說過,內核通過專門的struct fdtable來描述文件描述符表。該結構定義在yoursource/include/linux/file.h中。
2 |
33 unsigned int max_fds; |
4 |
35 fd_set *close_on_exec;
|
7 |
38 struct fdtable *next; |
max_fds:當前文件對象數組中元素的個數;
fd:指向當前文件對象數組,初始指向fd_arrary;
close_on_exec:執行exec時要關閉的文件描述符,初始指向close_on_exec_init;
open_fds:當前已經打開的文件描述符,初始指向open_fds_init;
下面這張圖是VFS中各種數據結構之間的關係圖,從上至下包括描述進程的task_struct對象, task_struct對象中包含該進程打開的所有文件的files_struct指針files, 表示進程打開的單個文件的file對象, file對象中包含文件操作結構體f_op以及指向該文件的dentry對象的f_dentry, dentry對象中包含了文件的名字和目錄樹形結構以及該文件對應的inode對象, inode對象包含了inode操作函數結構體,file操作函數結構體,指向文件數據存儲位置的inode
number(假如該文件是目錄文件,那麼目錄文件數據存儲塊中的內容爲一個個的entry,每一個entry是該目錄下的子目錄或者文件對應的inode number),inode對象還包含了該文件拷貝到內存page cache的指針i_mapping, i_mapping是一個address_space結構體的指針,這個address_space名字起的比較糟糕,容易讓人產生誤解。address_space中包含了一個page_tree指向page樹, address還包含一個操作結構體a_ops。
![]()
轉載: http://edsionte.com/techblog/archives/2054
學習了VFS的基本原理,我們很有必要對這些理論知識進行驗證和實踐。本文所分析的幾個小程序將更具體、直觀的展現VFS中一些數據結構之間的邏輯關係。
1.打印超級塊和索引結點
通過前面的分析我們知道,系統中所有的超級塊都內嵌一個struct list_head類型的字段s_list。通過該字段將系統中所有的超級塊鏈接成一個雙聯表。因此,如果我們知道這個雙聯表的頭指針以及瞭解相關遍歷宏的使用方法,那麼我們就可以遍歷整個系統中的所有超級塊了。
爲了解釋方便,我們將內嵌的list_head結構體稱爲內部結構體;將super_block結構體稱爲外部結構體;
具體的,我們可以通過 list_for_each宏來遍歷一個list_head類型的雙聯表。該宏的定義如下:
1 |
364 #define list_for_each(pos, head) \ |
2 |
365 for (pos = (head)->next; prefetch(pos->next), pos != (head); \ |
使用該宏時,需要向head參數中傳遞要遍歷雙聯表的頭指針;而每次遍歷得到的list_head類型的結點地址會保存在pos這個參數中。不過,這個宏只能遍歷內嵌於超級塊中的那個list_head結構的鏈表,並不能得到正在被遍歷的那個超級塊的地址(也就是指向當前正被遍歷的超級塊的指針)。也就是說,每次遍歷時並不能得到超級塊中的其他字段。因此,還應該使用 list_entry宏。該宏通過指向list_head結點的地址來得到外部超級塊的首地址。
1 |
345 #define list_entry(ptr, type, member) \ |
2 |
346 container_of(ptr, type, member) |
這個宏的第一個參數是指向內部結構體list_head的指針,第二個參數是外部結構體的類型,第三個參數是list_head類型的變量在外部結構體中的名稱。這個宏最後會返回指向當前外部結構體的指針。比如,在super_block結構體中,list_head結構類型的字段名稱爲s_list,因此可以如下使用該宏:
1 |
sb = list_entry(pos, struct super_block, s_list); |
對於超級塊形成的雙聯表來說,它的頭指針是super_blocks。但是很遺憾,super_blocks這個變量並沒有被導出。所謂導出,就是通過EXPORT_SYMBOL將某個函數或者變量對全部內核代碼公開。也就是說,使用 EXPORT_SYMBOL可以將一個函數以符號的方式導出給其他模塊使用 。爲了解決這個問題,我們可以在包含super_blocks的這個文件中將這個變量導出,並且重新編譯內核。對於我們這裏的這個小程序而言,這樣做有些不值得。幸好,在/proc/kallsyms文件中,記錄了內核中所有符號以及符號的地址。因此,在該文件中查找相應符號就可以得到其地址。
我們使用下述兩條命令:
1 |
edsionte@edsionte-desktop:~/code/vfs/print_sb$grep super_blocks /proc/kallsyms |
2 |
c0772a30 D super_blocks |
3 |
edsionte@edsionte-desktop:~/code/vfs/print_sb$grep " sb_lock"
/proc/kallsyms |
就可以得到super_blocks變量的地址。另外,sb_lock超級塊對應的自旋鎖。
上述都準備好後,我們就可以進行遍歷了。關鍵代碼如下:
01 |
#define SUPER_BLOCKS_ADDRESS 0xc0772a30 |
02 |
#define SB_LOCK_ADDRESS 0xc08c9d60 |
04 |
static
int __init my_init( void ) |
06 |
struct
super_block *sb; |
07 |
struct
list_head *pos; |
08 |
struct
list_head *linode; |
10 |
unsigned long long count = 0;
|
12 |
printk( "print some fields of super blocks:\n" ); |
13 |
spin_lock((spinlock_t *)SB_LOCK_ADDRESS); |
14 |
list_for_each(pos, ( struct list_head *)SUPER_BLOCKS_ADDRESS){ |
16 |
sb = list_entry(pos, struct super_block, s_list); |
17 |
printk( "dev_t:%d,%d " ,MAJOR(sb->s_dev),MINOR(sb->s_dev)); |
18 |
printk( "fs_name:%s\n" ,sb->s_type->name); |
22 |
spin_unlock((spinlock_t *)SB_LOCK_ADDRESS); |
23 |
printk( "the number of inodes:%llu\n" , sizeof ( struct inode)*count); |
另外,需要注意的是,每次重啓電腦後,都要重新查找上述兩個變量的地址。
對於一個超級塊中所有的inode,有專門一個鏈表將所有的inode鏈接起來。這個鏈表的頭結點是超級塊中的s_inode字段。而inode之間是其內部的i_sb_list字段進行鏈接的。瞭解了這些,我們可以在上述程序的基礎上,再打印每個超級塊中的所有inode:
01 |
list_for_each(pos, ( struct list_head *)SUPER_BLOCKS_ADDRESS){ |
02 |
sb = list_entry(pos, struct super_block, s_list); |
03 |
printk( "dev_t:%d,%d " ,MAJOR(sb->s_dev),MINOR(sb->s_dev)); |
04 |
printk( "fs_name:%s\n" ,sb->s_type->name); |
05 |
list_for_each(linode, &sb->s_inodes){ |
06 |
pinode = list_entry(linode, struct inode, i_sb_list); |
08 |
printk( "%lu\t" ,pinode->i_ino); |
在上面代碼的基礎上,我們再加深一步。一個索引結點可能對應若干個dentry,這些dentry自身通過其內部的d_alias鏈接在一起;而整個鏈表的頭結點是inode中的i_dentry字段。因此,根據上面的方法,我們可以在遍歷每個inode的同時,繼續遍歷這個inode對應的所有dentry。部分代碼如下:
01 |
list_for_each(pos, ( struct list_head *)SUPER_BLOCKS_ADDRESS){ |
02 |
sb = list_entry(pos, struct super_block, s_list); |
03 |
printk( "dev_t:%d,%d " ,MAJOR(sb->s_dev),MINOR(sb->s_dev)); |
04 |
printk( "fs_name:%s\n" ,sb->s_type->name); |
05 |
list_for_each(linode, &sb->s_inodes){ |
06 |
pinode = list_entry(linode, struct inode, i_sb_list); |
08 |
printk( "%lu[" ,pinode->i_ino); |
09 |
list_for_each(ldentry, &pinode->i_dentry){ |
10 |
pdentry = list_entry(ldentry, struct dentry, d_alias); |
11 |
printk( "%s->" ,pdentry->d_name.name); |
上出程序的完整的代碼在這裏。
2.打印文件類型結構體
同樣的道理,通過下述的代碼可以打印file_system_type結構體。
01 |
#define FILE_SYSTEM_ADDRESS 0xc08ca3a4 /* grep file_systems /proc/kallsyms */ |
02 |
#define FILE_SYSTEM_LOCK_ADDRESS 0xc0772de0 /* grep file_systems_lock /proc/kallsyms */ |
04 |
static
int __init printfs_init( void ) |
06 |
struct file_system_type **pos; |
08 |
printk( "\n\nprint file_system_type:\n" ); |
10 |
read_lock((rwlock_t *)FILE_SYSTEM_LOCK_ADDRESS); |
11 |
pos = ( struct file_system_type **)FILE_SYSTEM_ADDRESS; |
14 |
printk( "name: %s\n" ,(*pos)->name); |
15 |
pos = &((*pos)->next); |
18 |
read_unlock((rwlock_t *)FILE_SYSTEM_LOCK_ADDRESS); |
更多的打印信息可以按照上述方法繼續添加。開始吧!