LINUX文件系统挂载浅析

本文深入解析了Linux文件系统,介绍了文件系统的基本概念,如VFS(虚拟文件系统)如何统一不同类型的文件系统。文章详细阐述了inode、dentry、superblock等核心概念及其关系,并探讨了文件路径查找过程。挂载部分讲解了挂载树、挂载点、挂载命名空间以及挂载传播机制。最后,提到了挂载命名空间在隔离文件系统中的作用。
摘要由CSDN通过智能技术生成

LINUX文件系统挂载浅析

一、文件系统

首先我们来解释一下“文件系统”的概念。根据百度百科的定义,文件系统是操作系统用于明确存储设备(常见的是磁盘,也有基于NAND Flash的固态硬盘)或分区上的文件的方法和数据结构。根据我的理解,可以简单同一个文件系统上的文件都是“在一块”的。比如我们有两个EXT4类型的分区:分区A和分区B,他们就是两个独立的文件系统,其文件系统类型相同,都是EXT4

1.1 VFS

在LINUX的世界里,“文件”是一个很宽泛的概念,正所谓“一切皆文件”。但是在LINUX中有很多种类型的文件系统,不同的文件系统的实现和操作方法是不同的,内核是如何将他们统一起来的呢?这就离不开我们的VFSVirtual File System,虚拟文件系统),它通过为文件系统提供了一个统一的模型和接口,使得各种文件系统的调用变得统一起来。可以理解为VFS为文件系统做了一层抽象,根据底层具体文件系统类型的不同,VFS会调用不同的函数,从而使得文件系统的定义变得灵活起来。

1.2 文件

想要理解LINUX内核对于文件操作的实现,我们需要先理清楚inodedentrysuperblockvfsmount等概念以及他们之间的关系。

INODE

内核里面,万物皆文件,而inode正是用来表示文件的。inode可以说包含了系统操作文件所需要的一切信息,以磁盘文件为例,它记录着文件在磁盘上存储的具体位置、文件的操作方法等,我们简单说一下其中的一些属性。

struct inode {

	const struct inode_operations	*i_op;
	struct super_block	*i_sb;
	/* Stat data, not accessed from path walking */
	unsigned long		i_ino;
    ......
    struct list_head	i_sb_list;
	struct list_head	i_wb_list;	/* backing dev writeback list */
	struct list_head	i_lru;		/* inode LRU list */
	union {
		struct hlist_head	i_dentry;
		struct rcu_head		i_rcu;
	};
	atomic_t		i_count;
	atomic_t		i_dio_count;
	atomic_t		i_writecount;

	union {
		const struct file_operations	*i_fop;	/* former ->i_op->default_file_ops */
		void (*free_inode)(struct inode *);
	};

	void			*i_private; /* fs or device private pointer */
} __randomize_layout;
  • i_inoinode号。每个文件都有一个独一无二的编号用于识别该文件,这就是inode号。ino的具体生成是由具体的文件系统类型决定和实现的,以nsfs(网络命名空间文件系统)为例,它是在__ns_get_path()中生成inode的,并将网络命名空间的inum作为inode的编号。
  • i_sbinode所在文件系统的超级块。超级块是用来记录文件系统信息的数据结构,例如分区的大小、最大文件长度等关键信息。除此之外,其还定义了用于操作inode的函数的指针。每一个文件系统都有一个与之对应的超级块。
  • i_sb_list:这个链表用于将同一个超级块的所有inode连起来,即我们可以遍历某个文件系统中所有的inode。该链表的表头存储在超级块结构体(struct super_block)的s_inodes字段。
  • i_countinode的引用计数,该值归0后可能会导致inode的释放。
  • i_private:私有数据,不同的文件系统类型用于存储不同的数据。

DENTRY

内核里面万物皆文件,那么目录也不例外,也是一种类型的文件,使用inode表示。相比于表示文件的inode,表示目录的inode的数据段中存储的不是指向文件的信息,而是其一个inode列表,表示其中包含的文件或者目录。那么内核是如何根据一个文件路径来找到那个文件的inode的呢?首先我们来看一下简单的文件路径查找,以文件/usr/bin/busybox的查找过程为例,其过程大概如下:

在这里插入图片描述

为了提高查找的效率,在路径查找过程中,每涉及到一个inode,就会为其创建一个dentry实例,这样在下一次的查找时就不需要再从块设备上进行查找,比较块设备的查找是很慢的。

从上面的模式中,我们大概可以理解了我们平时所使用的软链接和硬链接是如何实现的:软链接的inode类型是软链接,而数据段中存储的是其指向的路径;硬链接是在所在目录的inode的数据段中的文件列表中增加了个项目,只是其inode号指向的是一个已有的inode,这也是为啥硬链接无法跨文件系统,因为inode号是和文件系统相关联的,因此其只能表达当前文件系统。

二、挂载

说到文件系统,就离不开mount(挂载),只有将文件系统挂载到当前的挂载命名空间中,文件系统才能被使用,所以每一个挂载了的文件系统都有一个以上与之对应的挂载项(同一个文件系统可以mount多次),这个我们可以在系统中通过mount命令看到。

2.1 挂载树

如果不考虑文件中的软(硬)链接,那么文件目录是通过一个完美的树状结构来组织的。文件系统是挂载到其他文件或者文件夹上的,因此所有的挂载项也是一个树状结构,而这个树状结构的根就是根挂载,也就是根文件系统所在的挂载项。

我们在进行mount操作的时候,需要选择一个目录或文件来作为挂载点,这个挂载点所在的文件系统的挂载项即为新挂载项的父挂载。每个挂载项都有一个唯一的父挂载(根挂载除外),一个挂载项可以有多个子挂载,所有的挂载就组织成了一个树状结构。如果一个挂载项没有父挂载,那么他就是根挂载。

2.2 数据结构

内核中主要使用四个结构体来组织挂载:mountvfsmountsuperblockmountpoint。其中,mount为挂载信息的主要结构体,其用来组织mount之间的逻辑关系、引用计数等基本信息;vfsmount用来衔接mountvfs层之间的关系;superblock超级块主要是针对文件系统,存储了文件系统的基本信息,比如文件系统大小、类型等;mountpoint用来代表挂载点。

mount

mount结构体的定义如下所示:

struct mount {
	struct hlist_node mnt_hash;
	struct mount *mnt_parent;		//父挂载
	struct dentry *mnt_mountpoint;	//挂载点
	struct vfsmount mnt;			//vfsmount
	union {
		struct rcu_head mnt_rcu;
		struct llist_node mnt_llist;
	};
#ifdef CONFIG_SMP
	struct mnt_pcp __percpu mnt_pcp;
#else
	int mnt_count;
	int mnt_writers;
#endif
	struct list_head mnt_mounts;	//子挂载链表头指针
	struct list_head mnt_child;		//子挂载链表
	......
} __randomize_layout;

所有的mount变量都存储在一个全局HASH表里,以便于能够根据挂载点,快速地找到挂载点上面的挂载项,HASH查找函数如下所示:

static inline struct hlist_head *m_hash(struct vfsmount *mnt, struct dentry *dentry)
{
	unsigned long tmp = ((unsigned long)mnt / L1_CACHE_BYTES);
	tmp += ((unsigned long)dentry / L1_CACHE_BYTES);
	tmp = tmp + (tmp >> m_hash_shift);
	return &mount_hashtable[tmp & m_hash_mask];
}

可以看出,挂载项是根据挂载点dentry及其所在的文件系统的vfsmount来进行HASH查找的。

vfsmount

vfsmount是用来衔接vfsmount的。在早期的linux系统中是没有mount结构体的,mount结构体的属性都是在vfsmount里的,后来才被分离出去的。

struct vfsmount {
	struct dentry *mnt_root;	//当前所挂载的文件系统的根,这是一个相对的根目录。
	struct super_block *mnt_sb;	//当前挂载的文件系统的超级块
	int mnt_flags;				//挂载标识
} __randomize_layout;

由于vfsmount是定义在mount结构体里面的,因此我们是可以根据vfsmount的地址取得其所属的mount的地址的:

static inline struct mount *real_mount(struct vfsmount *mnt)
{
	return container_of(mnt, struct mount, mnt);
}

mountpoint

该结构体是为了能够快速找到同一个挂载点上的所有挂载项。同一个目录上,我们是可以进行多次挂载的,后挂载的文件系统会将先挂载的给覆盖。从其定义来看,其主要作用为维护了一个链表,用来组织同一个挂载点上的所有挂载项。所有的mountpoint变量都存储在一个全局HASH表里。

struct mountpoint {
	struct hlist_node m_hash;	
	struct dentry *m_dentry;	//挂载点
	struct hlist_head m_list;	//哈希链表,存储挂载点上所有的挂载项
	int m_count;
};

2.3 挂载过程

总的来说,一个新的文件系统的挂载过程可以分为以下三步:

  1. 挂载点查找。在进行挂载时,内核要根据用户输入的路径找到挂载点的dentry。在路径查找过程中,如果路径中间的某个文件夹是符号链接或者挂载点,那么内核会进行follow操作。所谓的follow操作,就是,如果当前文件夹是个软链接,那么就用软链接的目标dentry作为找到的dentry;如果当前文件夹上面挂载了文件系统,就使用所挂载的文件系统的相对根目录的dentry作为当前找到的dentry。如果当前文件夹是路径的最后一项,那么不会进行软链接的follow,但是会进行挂载项的follow。以挂载路径/mnt/test/sda为例,这里我们假设:(1)这个路径上已经挂载了一个磁盘sda1,下面我们要在此在这个路径上挂载磁盘sda2;(2)/mnt/test是一个符号链接,其指向/test目录。其路径查找逻辑如下图所示:

在这里插入图片描述

从上图中我们可以看出,路径查找时,首先会根据符号链接文件/mnt/test/的信息,进行follow link的操作,找到/testdentry;然后,再检查到/testdentry上面有个挂载项,所以就进行了follow mount的操作,返回了所挂载的文件系统的相对根目录的dentry。从这里我们可以看出来,inode号并不是全局唯一的,而是在当前文件系统中是唯一的,所以确定一个文件需要文件系统和inode两个条件。同时我们也可以看出来,当多次在同一个文件或者目录上进行挂载操作,那么后一个文件系统会挂载到前一个文件系统的相对根目录的dentry上。

  1. mount结构体构建。当我们进行一个mount命令将某个块设备挂载到某个文件夹上时,内核函数调用的过程为:ksys_mountdo_mountdo_new_mountvfs_kern_mountdo_add_mount,其中mount结构体的构建是在vfs_kern_mount函数中进行的,该函数会根据文件系统的类型、挂载参数等信息来构造文件系统的相对根dentry以及mount等变量。

  2. 挂载项的组织。在初始化好mount变量后,下面要对其进行组织,包括:(1)将该mount变量添加到全局hash表中,并将mount变量的挂载点设置为步骤1中找到的挂载点dentry;(2)组织挂载树,将当前的挂载项的父挂载设置为挂载点所在文件系统的挂载项;(3)处理挂载项的传播。

2.4 挂载命名空间

对于LINUX的命名空间,我们应该并不陌生,它是用来对内核中的某些内容进行隔离的存在,常见的有用来隔离网络环境的网络命名空间、用来隔离进程的进程命名空间等,而挂载命名空间正是用来隔离挂载项的,从某种意义上也可以理解为其是用来隔离文件系统的。

在创建新的挂载命名空间时,会将老的挂载命名空间中的挂载树拷贝一份到当前挂载命名空间。这个拷贝操作是必须的,因为如果当前挂载命名空间中没有挂载项,也就是没有文件系统,那么新创建的挂载命名空间中的进程将无法运行,因为对于进程的运行,文件系统是必须的。

2.5 挂载传播

各个挂载点之间不是相互独立的,而是存在着一定的关联关系,也就是传播关系。想要理解这种传播关系,我们就要先理解peer grouppropagation type的概念。propagation type就是传播类型,其包括了四种:

  1. shared:共享传播。peer group可以理解为一个共享组,当几个mount项属于同一个peer group时,挂载信息会在这几个mount项之间传播。例如,我们在其中一个mount项中添加子mount,那么其他属于同一个peer groupmount项下面也会出现相同的挂载。当一个挂载项是通过bind的方式挂载的,那么它与其源挂载属于同一个共享组。这种传播方式是通过挂载树来传播的,例如我们将一个挂载项卸载了,那么该卸载消息会通过挂载树向上传递,如果其某个父挂载有共享挂载,那么消息会传递到所有的共享挂载中。
  2. private:私有挂载。与共享挂载相反,该挂载不与任何挂载共享,不属于任何共享组。即使通过bind的方式挂载,也不进行共享。
  3. slave:单向模式,或者主从模式。如果一个挂载是单向模式,那么共享组中的信息会传递到该挂载,该挂载的信息不会传递到共享组。
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值