深入理解Linux文件系統(tǒng)之文件系統(tǒng)掛載(上)
本文轉(zhuǎn)載自微信公眾號「Linux內(nèi)核遠航者」,作者Linux內(nèi)核遠航者。轉(zhuǎn)載本文請聯(lián)系Linux內(nèi)核遠航者公眾號。
1.開場白
- 環(huán)境:
處理器架構(gòu):arm64
內(nèi)核源碼:linux-5.11
ubuntu版本:20.04.1
代碼閱讀工具:vim+ctags+cscope
我們知道,Linux系統(tǒng)中我們經(jīng)常將一個塊設(shè)備上的文件系統(tǒng)掛載到某個目錄下才能訪問這個文件系統(tǒng)下的文件,但是你有沒有思考過:為什么塊設(shè)備掛載之后才能訪問文件?掛載文件系統(tǒng)Linux內(nèi)核到底為我們做了哪些事情?是否可以不將文件系統(tǒng)掛載到具體的目錄下也能訪問?下面,本文將詳細(xì)講解Linxu系統(tǒng)中,文件系統(tǒng)掛載的奧秘。
注:本文主要講解文件系統(tǒng)掛載核心邏輯,暫不涉及掛載命名空間和綁定掛載等內(nèi)容(后面的內(nèi)容可能會涉及),且以ext2磁盤文件系統(tǒng)為例講解掛載。本專題文章分為上下兩篇,上篇主要介紹掛載全貌以及具體文件系統(tǒng)的掛載方法,下篇介紹如何通過掛載實例關(guān)聯(lián)掛載點和超級塊。
2. vfs 幾個重要對象
在這里我們不介紹整個IO棧,只說明和文件系統(tǒng)相關(guān)的vfs和具體文件系統(tǒng)層。我們知道在Linux中通過虛擬文件系統(tǒng)層VFS統(tǒng)一所有具體的文件系統(tǒng),提取所有具體文件系統(tǒng)的共性,屏蔽具體文件系統(tǒng)的差異。VFS既是向下的接口(所有文件系統(tǒng)都必須實現(xiàn)該接口),同時也是向上的接口(用戶進程通過系統(tǒng)調(diào)用最終能夠訪問文件系統(tǒng)功能)。
下面我們來看下,vfs中幾個比較重要的結(jié)構(gòu)體對象:
2.1 file_system_type
這個結(jié)構(gòu)來描述一種文件系統(tǒng)類型,一般具體文件系統(tǒng)會定義這個結(jié)構(gòu),然后注冊到系統(tǒng)中;定義了具體文件系統(tǒng)的掛載和卸載方法,文件系統(tǒng)掛載時調(diào)用其掛載方法構(gòu)建超級塊、跟dentry等實例。
文件系統(tǒng)分為以下幾種:
1)磁盤文件系統(tǒng)
文件在非易失性存儲介質(zhì)上(如硬盤,flash),掉電文件不丟失。
如ext2,ext4,xfs
2)內(nèi)存文件系統(tǒng)
文件在內(nèi)存上,掉電丟失。
如tmpfs
3)偽文件系統(tǒng)
是假的文件系統(tǒng),是利用虛擬文件系統(tǒng)的接口(可以對用戶可見如proc、sysfs,也可以對用戶不可見內(nèi)核可見如sockfs,bdev)。
如proc,sysfs,sockfs,bdev
4)網(wǎng)絡(luò)文件系統(tǒng)
這種文件系統(tǒng)允許訪問另一臺計算機上的數(shù)據(jù),該計算機通過網(wǎng)絡(luò)連接到本地計算機。
如nfs文件系統(tǒng)
結(jié)構(gòu)體定義源碼路徑:include/linux/fs.h +2226
2.2 super_block
超級塊,用于描述塊設(shè)備上的一個文件系統(tǒng)總體信息(如文件塊大小,最大文件大小,文件系統(tǒng)魔數(shù)等),一個塊設(shè)備上的文件系統(tǒng)可以被掛載多次,但是內(nèi)存中只能有個super_block來描述(至少對于磁盤文件系統(tǒng)來說)。
結(jié)構(gòu)體定義源碼路徑:include/linux/fs.h +1414
2.3 mount
掛載描述符,用于建立超級塊和掛載點等之間的聯(lián)系,描述文件系統(tǒng)的一次掛載,一個塊設(shè)備上的文件系統(tǒng)可以被掛載多次,每次掛載內(nèi)存中有一個mount對象描述。
結(jié)構(gòu)體定義源碼路徑:fs/mount.h +39
2.4 inode
索引節(jié)點對象,描述磁盤上的一個文件元數(shù)據(jù)(文件屬性、位置等),有些文件系統(tǒng)需要從塊設(shè)備上讀取磁盤上的索引節(jié)點,然后在內(nèi)存中創(chuàng)建vfs的索引節(jié)點對象,一般在文件第一次打開時創(chuàng)建。
結(jié)構(gòu)體定義源碼路徑:include/linux/fs.h +610
2.5 dentry
目錄項對象,用于描述文件的層次結(jié)構(gòu),從而構(gòu)建文件系統(tǒng)的目錄樹,文件系統(tǒng)將目錄當(dāng)作文件,目錄的數(shù)據(jù)由目錄項組成,而每個目錄項存儲一個目錄或文件的名稱和索引節(jié)點號等內(nèi)容。每當(dāng)進程訪問一個目錄項就會在內(nèi)存中創(chuàng)建目錄項對象(如ext2路徑名查找中,通過查找父目錄數(shù)據(jù)塊的目錄項,找到對應(yīng)文件/目錄的名稱,獲得inode號來找到對應(yīng)inode)。
結(jié)構(gòu)體定義源碼路徑:include/linux/dcache.h +90
2.6 file
文件對象,描述進程打開的文件,當(dāng)進程打開文件時會創(chuàng)建文件對象加入到進程的文件打開表,通過文件描述符來索引文件對象,后面讀寫等操作都通過文件描述符進行(一個文件可以被多個進程打開,會由多個文件對象加入到各個進程的文件打開表,但是inode只有一個)。
結(jié)構(gòu)體定義源碼路徑:include/linux/fs.h +915
3. 掛載總體流程
3.1系統(tǒng)調(diào)用處理
用戶執(zhí)行掛載是通過系統(tǒng)調(diào)用路徑進入內(nèi)核處理,拷貝用戶空間傳遞參數(shù)到內(nèi)核,掛載委托do_mount。
//fs/namespace.c
SYSCALL_DEFINE5(mount
參數(shù):
dev_name 要掛載的塊設(shè)備
dir_name 掛載點目錄
type 文件系統(tǒng)類型名
flags 掛載標(biāo)志
data 掛載選項
-> kernel_type = copy_mount_string(type); //拷貝文件系統(tǒng)類型名到內(nèi)核空間
-> kernel_dev = copy_mount_string(dev_name) //拷貝塊設(shè)備路徑名到內(nèi)核空間 -> options = copy_mount_options(data) //拷貝掛載選項到內(nèi)核空間
-> do_mount(kernel_dev, dir_name, kernel_type, flags, options) //掛載委托do_mount
3.2 掛載點路徑查找
掛載點路徑查找,掛載委托path_mount
do_mount
-> user_path_at(AT_FDCWD, dir_name, LOOKUP_FOLLOW, &path) //掛載點路徑查找 查找掛載點目錄的 vfsmount和dentry 存放在 path
-> path_mount(dev_name, &path, type_page, flags, data_page) //掛載委托path_mount
3.3 參數(shù)合法性檢查
參數(shù)合法性檢查, 新掛載委托do_new_mount
path_mount
-> 參數(shù)合法性檢查
-> 根據(jù)掛載標(biāo)志調(diào)用不同函數(shù)處理 這里講解是默認(rèn) do_new_mount
3.4 調(diào)用具體文件系統(tǒng)掛載方法
- do_new_mount
- -> type = get_fs_type(fstype) //根據(jù)傳遞的文件系統(tǒng)名 查找已經(jīng)注冊的文件系統(tǒng)類型
- -> fc = fs_context_for_mount(type, sb_flags) //為掛載分配文件系統(tǒng)上下文 struct fs_context
- -> alloc_fs_context
- -> 分配fs_context fc = kzalloc(sizeof(struct fs_context), GFP_KERNEL)
- -> 設(shè)置 ...
- -> fc->fs_type = get_filesystem(fs_type); //賦值相應(yīng)的文件系統(tǒng)類型
- -> init_fs_context = **fc->fs_type->init_fs_context**; //新內(nèi)核使用fs_type->init_fs_context接口 來初始化文件系統(tǒng)上下文
- if (!init_fs_context) //init_fs_context回掉 主要用于初始化
- init_fs_context = **legacy_init_fs_context**; //沒有 fs_type->init_fs_context接口
- -> init_fs_context(fc) //初始化文件系統(tǒng)上下文 (初始化一些回掉函數(shù),供后續(xù)使用)
來看下文件系統(tǒng)類型沒有實現(xiàn)init_fs_context接口的情況:
- //fs/fs_context.c
- init_fs_context = legacy_init_fs_context
- -> fc->ops = &legacy_fs_context_ops //設(shè)置文件系統(tǒng)上下午操作
- ->.get_tree = legacy_get_tree //操作方法的get_tree用于 讀取磁盤超級塊并在內(nèi)存創(chuàng)建超級塊,創(chuàng)建跟inode, 跟dentry
- -> root = fc->fs_type->mount(fc->fs_type, fc->sb_flags,
- ¦ fc->source, ctx->legacy_data) //調(diào)用文件系統(tǒng)類型的mount方法來讀取并創(chuàng)建超級塊
- -> fc->root = root //賦值創(chuàng)建好的跟dentry
- 有一些文件系統(tǒng)使用原來的接口(fs_type.mount = xxx_mount):如ext2,ext4等
- 有一些文件系統(tǒng)使用新的接口(fs_type.init_fs_context = xxx_init_fs_context):xfs, proc, sys
- 無論使用哪一種,都會在xxx_init_fs_contex中實現(xiàn) fc->ops = &xxx_context_ops 接口,后面會看的都會調(diào)用fc->ops.get_tree 來讀取創(chuàng)建超級塊實例
繼續(xù)往下走:
- do_new_mount
- -> ...
- -> fc = fs_context_for_mount(type, sb_flags) //分配 賦值文件系統(tǒng)上下文
- -> parse_monolithic_mount_data(fc, data) //調(diào)用fc->ops->parse_monolithic 解析掛載選項
- -> mount_capable(fc) //檢查是否有掛載權(quán)限
- -> vfs_get_tree(fc) //fs/super.c 掛載重點 調(diào)用fc->ops->get_tree(fc) 讀取創(chuàng)建超級塊實例
- ...
3.5 掛載實例添加到全局文件系統(tǒng)樹
- do_new_mount
- ...
- -> do_new_mount_fc(fc, path, mnt_flags) //創(chuàng)建mount實例 關(guān)聯(lián)掛載點和超級塊 添加到命名空間的掛載樹中
下面主要看下vfs_get_tree和do_new_mount_fc:
4.具體文件系統(tǒng)掛載方法
1)vfs_get_tree
- //以ext2文件系統(tǒng)為例
- vfs_get_tree //fs/namespace.c
- -> fc->ops->get_tree(fc)
- -> legacy_get_tree //上面分析過 fs_type->init_fs_context == NULL使用舊的接口(ext2為NULL)
- ->fc->fs_type->mount
- -> ext2_mount //fs/ext2/super.c 調(diào)用到具體文件系統(tǒng)的掛載方法
來看下ext2對掛載的處理:
啟動階段初始化->
- //fs/ext2/super.c
- module_init(init_ext2_fs)
- init_ext2_fs
- ->init_inodecache //創(chuàng)建ext2_inode_cache 對象緩存
- ->register_filesystem(&ext2_fs_type) //注冊ext2的文件系統(tǒng)類型
- static struct file_system_type ext2_fs_type = {
- .owner = THIS_MODULE,
- .name = "ext2",
- .mount = ext2_mount, //掛載時調(diào)用 用于讀取創(chuàng)建超級塊實例
- .kill_sb = kill_block_super, //卸載時調(diào)用 用于釋放超級塊
- .fs_flags = FS_REQUIRES_DEV, //文件系統(tǒng)標(biāo)志為 請求塊設(shè)備,文件系統(tǒng)在塊設(shè)備上
- };
掛載時調(diào)用->
- // fs/ext2/super.c
- static struct dentry *ext2_mount(struct file_system_type *fs_type,
- int flags, const char *dev_name, void *data)
- {
- return mount_bdev(fs_type, flags, dev_name, data, ext2_fill_super);
- }
ext2_mount通過調(diào)用mount_bdev來執(zhí)行實際文件系統(tǒng)的掛載工作,ext2_fill_super的一個函數(shù)指針作為參數(shù)傳遞給get_sb_bdev。該函數(shù)用于填充一個超級塊對象,如果內(nèi)存中沒有適當(dāng)?shù)某墘K對象,數(shù)據(jù)就必須從硬盤讀取。
mount_bdev是個公用的函數(shù),一般磁盤文件系統(tǒng)會使用它來根據(jù)具體文件系統(tǒng)的fill_super方法來讀取磁盤上的超級塊并在創(chuàng)建內(nèi)存超級塊。
我們來看下mount_bdev的實現(xiàn)(**它執(zhí)行完成之后會創(chuàng)建vfs的三大數(shù)據(jù)結(jié)構(gòu) super_block、根inode和根dentry **):
2)mount_bdev源碼分析
- //fs/super.c
- mount_bdev
- ->bdev = blkdev_get_by_path(dev_name, mode, fs_type) //通過要掛載的塊設(shè)備路徑名 獲得它的塊設(shè)備描述符block_device(會涉及到路徑名查找和通過設(shè)備號在bdev文件系統(tǒng)查找block_device,block_device是添加塊設(shè)備到系統(tǒng)時創(chuàng)建的)
- -> s = sget(fs_type, test_bdev_super, set_bdev_super, flags | sb_NOSEC,
- ¦bdev); //查找或創(chuàng)建vfs的超級塊 (會首先在文件系統(tǒng)類型的fs_supers鏈表查找是否已經(jīng)讀取過指定的超級塊,會對比每個超級塊的s_bdev塊設(shè)備描述符,沒有創(chuàng)建一個)
- -> if (s->s_root) { //超級塊的根dentry是否被賦值?
- ...
- } else { //沒有賦值說明時新創(chuàng)建的sb
- ...
- -> sb_set_blocksize(s, block_size(bdev)) //根據(jù)塊設(shè)備描述符設(shè)置文件系統(tǒng)塊大小
- -> fill_super(s, data, flags & sb_SILENT ? 1 : 0) //調(diào)用傳遞的具體文件系統(tǒng)的填充超級塊方法讀取填充超級塊等 如ext2_fill_super
- -> bdev->bd_super = s //塊設(shè)備bd_super指向sb
- }
- -> return dget(s->s_root) //返回文件系統(tǒng)的根dentry
可以看到mount_bdev主要是:1.根據(jù)要掛載的塊設(shè)備文件名查找到對應(yīng)的塊設(shè)備描述符(內(nèi)核后面操作塊設(shè)備都是使用塊設(shè)備描述符);2.首先在文件系統(tǒng)類型的fs_supers鏈表查找是否已經(jīng)讀取過指定的vfs超級塊,會對比每個超級塊的s_bdev塊設(shè)備描述符,沒有創(chuàng)建一個vfs超級塊; 3.新創(chuàng)建的vfs超級塊,需要調(diào)用具體文件系統(tǒng)的fill_super方法來讀取填充超級塊。
那么下面主要集中在具體文件系統(tǒng)的fill_super方法,這里是ext2_fill_super:
分析重點代碼如下:
3)ext2_fill_super源碼分析
- //fs/ext2/super.c
- static int ext2_fill_super(struct super_block *sb, void *data, int silent)
- {
- struct buffer_head * bh; //緩沖區(qū)頭 記錄讀取的磁盤超級塊
- struct ext2_sb_info * sbi; //內(nèi)存的ext2 超級塊信息
- struct ext2_super_block * es; //磁盤上的 超級塊信息
- ...
- sbi = kzalloc(sizeof(*sbi), GFP_KERNEL); //分配 內(nèi)存的ext2 超級塊信息結(jié)構(gòu)
- if (!sbi)
- goto failed;
- ...
- sb->s_fs_info = sbi; //vfs的超級塊 的s_fs_info指向內(nèi)存的ext2 超級塊信息結(jié)構(gòu)
- sbi->s_sb_block = sb_block;
- if (!(bh = sb_bread(sb, logic_sb_block))) { // 讀取磁盤上的超級塊到內(nèi)存的 使用buffer_head關(guān)聯(lián)內(nèi)存緩沖區(qū)和磁盤扇區(qū)
- ext2_msg(sb, KERN_ERR, "error: unable to read superblock");
- goto failed_sbi;
- }
- es = (struct ext2_super_block *) (((char *)bh->b_data) + offset); //轉(zhuǎn)換為struct ext2_super_block 結(jié)構(gòu)
- sbi->s_es = es; // 內(nèi)存的ext2 超級塊信息結(jié)構(gòu)的 s_es指向真正的ext2磁盤超級塊信息結(jié)構(gòu)
- sb->s_magic = le16_to_cpu(es->s_magic); //獲得文件系統(tǒng)魔數(shù) ext2為0xEF53
- if (sb->s_magic != EXT2_SUPER_MAGIC) //驗證 魔數(shù)是否正確
- goto cantfind_ext2;
- blocksize = BLOCK_SIZE << le32_to_cpu(sbi->s_es->s_log_block_size); //獲得磁盤讀取的塊大小
- /* If the blocksize doesn't match, re-read the thing.. */
- if (sb->s_blocksize != blocksize) { //塊大小不匹配 需要重新讀取超級塊
- brelse(bh);
- if (!sb_set_blocksize(sb, blocksize)) {
- ext2_msg(sb, KERN_ERR,
- "error: bad blocksize %d", blocksize);
- goto failed_sbi;
- }
- logic_sb_block = (sb_block*BLOCK_SIZE) / blocksize;
- offset = (sb_block*BLOCK_SIZE) % blocksize;
- bh = sb_bread(sb, logic_sb_block); //重新 讀取超級塊
- if(!bh) {
- ext2_msg(sb, KERN_ERR, "error: couldn't read"
- "superblock on 2nd try");
- goto failed_sbi;
- }
- es = (struct ext2_super_block *) (((char *)bh->b_data) + offset);
- sbi->s_es = es;
- if (es->s_magic != cpu_to_le16(EXT2_SUPER_MAGIC)) {
- ext2_msg(sb, KERN_ERR, "error: magic mismatch");
- goto failed_mount;
- }
- }
- sb->s_maxbytes = ext2_max_size(sb->s_blocksize_bits); //設(shè)置最大文件大小
- ...
- //讀取或設(shè)置 inode大小和第一個inode號
- if (le32_to_cpu(es->s_rev_level) == EXT2_GOOD_OLD_REV) {
- sbi->s_inode_size = EXT2_GOOD_OLD_INODE_SIZE;
- sbi->s_first_ino = EXT2_GOOD_OLD_FIRST_INO;
- } else {
- sbi->s_inode_size = le16_to_cpu(es->s_inode_size);
- sbi->s_first_ino = le32_to_cpu(es->s_first_ino);
- ...
- }
- ...
- sbi->s_blocks_per_group = le32_to_cpu(es->s_blocks_per_group); //賦值每個塊組 塊個數(shù)
- sbi->s_frags_per_group = le32_to_cpu(es->s_frags_per_group);
- sbi->s_inodes_per_group = le32_to_cpu(es->s_inodes_per_group); //賦值每個塊組 inode個數(shù)
- sbi->s_inodes_per_block = sb->s_blocksize / EXT2_INODE_SIZE(sb); //賦值每個塊 inode個數(shù)
- ...
- sbi->s_desc_per_block = sb->s_blocksize /
- sizeof (struct ext2_group_desc); //賦值每個塊 塊組描述符個數(shù)
- sbi->s_sbh = bh; //賦值讀取的超級塊緩沖區(qū)
- sbi->s_mount_state = le16_to_cpu(es->s_state); //賦值掛載狀態(tài)
- ...
- if (sb->s_magic != EXT2_SUPER_MAGIC)
- goto cantfind_ext2;
- //一些合法性檢查
- ...
- //計算塊組描述符 個數(shù)
- sbi->s_groups_count = ((le32_to_cpu(es->s_blocks_count) -
- le32_to_cpu(es->s_first_data_block) - 1)
- / EXT2_BLOCKS_PER_GROUP(sb)) + 1;
- db_count = (sbi->s_groups_count + EXT2_DESC_PER_BLOCK(sb) - 1) /
- ¦ EXT2_DESC_PER_BLOCK(sb);
- sbi->s_group_desc = kmalloc_array(db_count,
- ¦ sizeof(struct buffer_head *),
- ¦ GFP_KERNEL); //分配塊組描述符 bh數(shù)組
- for (i = 0; i < db_count; i++) { //讀取塊組描述符
- block = descriptor_loc(sb, logic_sb_block, i);
- sbi->s_group_desc[i] = sb_bread(sb, block); //讀取的 塊組描述符緩沖區(qū)保存 到sbi->s_group_desc[i]
- if (!sbi->s_group_desc[i]) {
- for (j = 0; j < i; j++)
- brelse (sbi->s_group_desc[j]);
- ext2_msg(sb, KERN_ERR,
- "error: unable to read group descriptors");
- goto failed_mount_group_desc;
- }
- }
- sb->s_op = &ext2_sops; //賦值超級塊操作
- ...
- root = ext2_iget(sb, EXT2_ROOT_INO); //讀取根inode (ext2 根根inode號為2)
- sb->s_root = d_make_root(root); //創(chuàng)建根dentry 并建立根inode和根dentry關(guān)系
- ext2_write_super(sb); //同步超級塊信息到磁盤 如掛載時間等
可以看到ext2_fill_super主要工作為:
1.讀取磁盤上的超級塊;
2.填充并關(guān)聯(lián)vfs超級塊;
3.讀取塊組描述符;
4.讀取磁盤根inode并建立vfs 根inode;
5.創(chuàng)建根dentry關(guān)聯(lián)到根inode。
下面給出ext2_fill_super之后ext2相關(guān)圖解:
有了這些信息,雖然能夠獲得塊設(shè)備上的文件系統(tǒng)全貌,內(nèi)核也能通過已經(jīng)建立好的block_device等結(jié)構(gòu)訪問塊設(shè)備,但是用戶進程不能真正意義上訪問到,用戶一般會通過open打開一個文件路徑來訪問文件,但是現(xiàn)在并沒有關(guān)聯(lián)掛載目錄的路徑,需要將文件系統(tǒng)關(guān)聯(lián)到掛載點,以至于路徑名查找的時候查找到掛載點后,在轉(zhuǎn)向文件系統(tǒng)的根目錄,而這需要通過do_new_mount_fc來去關(guān)聯(lián)并加入全局的文件系統(tǒng)樹中,下一篇我們將做詳細(xì)講解。