• 首发公号:Rand_cs

L i n u x Linux Linux 的文件系统中,有个很重要的概念就是挂载,挂载大家应该都很熟悉,除了根文件系统,其他所有文件系统都要先挂载到根文件系统中的某个目录之后才能访问。

所谓的根文件系统就是系统启动的时候安装的第一个文件系统,它也是内核映像所在的文件系统。而 挂载到某个目录某个目录 就是所谓的挂载点。

L i n u x Linux Linux 中有专门的命令来挂载文件系统,mount device dir d e v i c e device device 为要挂载的设备文件名, d i r dir dir 为挂载点。这里所说的设备不是真的指单个实体设备,而是其上的逻辑设备,比如说一个磁盘上的不同分区都可以看作是不同的设备。

每个设备都有一个设备号来标识,设备号可以分为两部分,一部分叫做主设备号 M a j o r Major Major N u m b e r Number Number,它用来标识某一类型的设备,比如说磁盘。另一部分叫做次设备号 M i n o r Minor Minor N u m b e r Number Number,它来标识某一具体设备,比如说磁盘上的某一具体分区。

当文件系统挂载到某个目录后,我们就可以通过这个目录来访问该文件系统,很多地方就只是简单的这样讲了一下,但其实这只能说是挂载的作用,那到底什么是挂载,要想解决这个问题还是只能从源码着手。下面我将根据 L i n u x Linux Linux 0.11 0.11 0.11 的代码来讲述挂载,涉及的东西比较多,我们只讨论相关的部分。

数据结构

m_inode,内存中的 inode

struct m_inode {
    /********略*********/
	unsigned short i_mode;   //文件类型和属性
	unsigned char i_mount;   //是否有文件系统挂载到这儿
    unsigned short i_zone[9]; //索引取,如果是块/字符设备文件i_zone[0]是设备号
    /********略*********/
};

super_block,内存中的超级块

struct super_block {
    /********略*********/
	unsigned short s_magic;    //文件系统魔数
    /********略*********/
	unsigned short s_dev;      //设备号
    /********略*********/
    struct m_inode * s_isup;   //被挂载的文件系统的根目录inode
	struct m_inode * s_imount; //该文件系统被安装到此inode
};

所谓的内存中的超级块和 i n o d e inode inode 指的是两者在内存中的缓存:

struct super_block super_block[NR_SUPER];
#define NR_SUPER 8
struct m_inode inode_table[NR_INODE];
#define NR_INODE 32

可以看出在 L i n u x Linux Linux 0.11 0.11 0.11 里面内存中最多同时存在 8 个超级块和 32 个文件的 i n o d e inode inode。这个缓存与我们平时所说的缓存差不多,当系统要想获取一个 i n o d e inode inode 时,会先在 i n o d e _ t a b l e inode\_table inode_table 中寻找有没有该 i n o d e inode inode,如果有的话就直接返回,如果没有,就从设备上将 i n o d e inode inode 读到 i n o d e _ t a b l e inode\_table inode_table 之后再返回。

相关操作

在看 m o u n t mount mount 挂载之前,先来看看一些操作函数。

static struct super_block * read_super(int dev);

如果缓存区中没有该设备的超级块,则先找一个空闲的超级块槽,然后从设备上读取超级块到找到的空闲超级块槽。如果该设备的超级块已经在缓存区中且数据有效,则直接返回该超级块的指针。

struct super_block * get_super(int dev);

根据设备号 d e v dev dev 从超级块数组当中获取超级块。

void put_super(int dev);

释放指定的设备超级块,也就是将超级块的 s _ d e v s\_dev s_dev 字段清 0,如此使得该超级块槽空闲出来。

struct m_inode * namei(const char * pathname);

n a m e i namei namei 函数根据路径 p a t h n a m e pathname pathname 获取末尾文件的 i n o d e inode inode,这个函数应该是有印象的吧,在 x v 6 xv6 xv6 中也有类似的函数。如果不太清楚的话,我这里举个例子:如果参数路径是 /a/b/c,调用 n a m e i namei namei 之后就会返回文件 c c c i n o d e inode inode

挂载

挂载的实现还是挺简单的,来看代码

int sys_mount(char * dev_name, char * dir_name, int rw_flag) //将名称为dev_name的设备上的文件系统挂载到目录dir_name上
{
	struct m_inode * dev_i, * dir_i;
	struct super_block * sb;
	int dev;

	if (!(dev_i=namei(dev_name))) //没有找到该设备
		return -ENOENT;
	dev = dev_i->i_zone[0];
	if (!S_ISBLK(dev_i->i_mode)) { //不是块设备
		iput(dev_i);
		return -EPERM;
	}
	iput(dev_i);   //释放该设备的inode
	if (!(dir_i=namei(dir_name)))  //解析获取挂载点的inode
		return -ENOENT;
	if (dir_i->i_count != 1 || dir_i->i_num == ROOT_INO) { //如果挂载点的引用数不等于1获取挂载点为根目录
		iput(dir_i);
		return -EBUSY;
	}
	if (!S_ISDIR(dir_i->i_mode)) {  //挂载点不是目录
		iput(dir_i);
		return -EPERM;
	}
	if (!(sb=read_super(dev))) {  //将设备上的文件系统的超级块读取到内存中
		iput(dir_i);
		return -EBUSY;
	}
	if (sb->s_imount) {  //如果该文件系统已挂载
		iput(dir_i);
		return -EBUSY;
	}
	if (dir_i->i_mount) {  //如果挂载点已经挂载了其他文件系统
		iput(dir_i);
		return -EPERM;
	}
	sb->s_imount=dir_i;   //将挂载点的inode记录到设备的超级块中
	dir_i->i_mount=1;     //表示该挂载点已经挂载了该文件系统
	dir_i->i_dirt=1;		/* NOTE! we don't iput(dir_i) */ //我们不会释放挂载点的inode
	return 0;			/* we do that in umount */ //我们在umount卸载文件系统时释放
}

其流程图为:

上图为 m o u n t mount mount 的实现过程,除了各种检查之外, m o u n t mount mount 实际上只做了两件事:

  1. 将要挂载的文件系统的超级块读到内存里的超级块槽
  2. 设置该超级块的 s b → s _ i m o u n t sb \rightarrow s\_imount sbs_imount 字段为 挂载点的 i n o d e inode inode,表示文件系统挂载到这个目录上,设置挂载点 i n o d e inode inode i _ m o u n t i\_mount i_mount 字段为 1 表示该目录上挂载的有文件系统

另外再解释几点:

  1. 字符设备,提供连续的数据流,应用程序可以顺序读取数据,通常不支持随机存取,像前面提到过的键盘串口都属于字符设备。块设备,应用程序能够随机访问设备的数据,程序可以自行确定数据的位置,比如说硬盘就是典型的块设备。很明显地文件系统只能存放在块设备上
  2. 挂载点只能是个目录文件,文件系统都是挂载到目录上的
  3. 挂载文件系统时,挂载点这个目录文件只能在当前引用,也就是说在挂载文件系统的时候,还有其他地方正在使用挂载点目录的话就不对

这就是挂载的本质,有没有感觉简单的同时又还是模模糊糊的?为什么将文件系统挂载到某个目录之后,这个目录就能表示被挂载的文件系统。解决这个问题还是要再来捋捋文件系统是如何寻找一个文件的,也就是 n a m e i namei namei 函数,比如说给定一个路径 /a/b,这是一个绝对路径,如何从最开始的根目录寻到文件 b b b 的呢?

这个问题我在 x v 6 xv6 xv6 文件系统里面也详细说过, L i n u x Linux Linux 里也类似。这里我们假设 a a a 文件都是目录文件, b b b 是一个普通文件。首先根目录文件就是一个个目录项,在其中寻找文件名为 a a a 的目录项,从中获取 a a a 目录文件的 i n o d e _ a inode\_a inode_a,根据 i n o d e _ a inode\_a inode_a 的索引字段找到 a a a 目录文件的数据,也是一个个目录项,在其中寻找文件名为 b b b 的目录项,从中获取普通文件 b b b i n o d e _ b inode\_b inode_b 然后返回。

上述所说的获取某个 i n o d e inode inode,使用的函数是, i g e t ( d e v , n r ) iget(dev, nr) iget(dev,nr),其意为从设备 d e v dev dev 中获取编号为 n r nr nr i n o d e inode inode。这个函数就会判断编号为 n r nr nr i n o d e inode inode 上是否挂载的有文件系统,来看相关代码:

struct m_inode * iget(int dev, int nr){
    /*********略**********/
    inode = inode_table;    //从inode表中第一个元素开始
	while (inode < NR_INODE+inode_table) {  //扫描内存里缓存的inode表
		if (inode->i_dev != dev || inode->i_num != nr) { //如果设备号对不上或者inode编号对不上
			inode++;   //下一个
			continue;
		}
		wait_on_inode(inode);   //等待该inode解锁
		if (inode->i_dev != dev || inode->i_num != nr) { //因为等待过程中inode可能会发生变化,所以再次判断
			inode = inode_table;
			continue;
		}
		inode->i_count++;  //找到了该inode,将其引用数加1
		if (inode->i_mount) {   //如果该inode上挂载的有文件系统
			int i;

			for (i = 0 ; i<NR_SUPER ; i++)   //在内存中缓存的超级块中寻找挂载点为当前inode的超级块
				if (super_block[i].s_imount==inode)  //找到了,break
					break;
			if (i >= NR_SUPER) {  //没找到,返回
				printk("Mounted inode hasn't got sb\n");
				if (empty) 
					iput(empty);
				return inode;
			}
			iput(inode);   //释放当前inode
			dev = super_block[i].s_dev;  //将设备号重新设置为被挂载的文件系统所在的设备号
			nr = ROOT_INO;    //将要寻找的inode编号重新设置为根目录的inode编号
			inode = inode_table;   //从内存的inode表第一个元素重新开始寻找inode
			continue;
		}
		if (empty)  //释放临时找的空闲inode
			iput(empty);  
		return inode;   //返回获取到的inode
	}
}

其完整的流程图如下:

这是我根据赵炯画的图改编,因为没有详细讲述 i g e t iget iget 的代码,所以主要关注虚线方框里面的就行。

如果在 i n o d e _ t a b l e inode\_table inode_table 中找到相应的 i n o d e inode inode,就判断 i n o d e → i _ m o u n t = = 1 inode \rightarrow i\_mount == 1 inodei_mount==1,如果为真,表示该 $inode $表示的目录文件上面挂载的有文件系统。此时这个目录应该表示被挂载的文件系统的根目录,所以设置 d e v = 超 级 块 表 示 的 设 备 dev = 超级块表示的设备 dev= n r = 1 nr = 1 nr=1,原目录就被隐藏掉了。举个例子再说明一下,假如调用 i g e t ( 1 , 99 ) iget(1, 99) iget(1,99),本来我是要获取 1 号设备的第 99 个 i n o d e inode inode,然后发现这个 i n o d e inode inode 指向的目录上面挂载的有 2 号设备的文件系统,那么我们就去寻找 2 号设备的根目录 i n o d e inode inode 然后返回。所以看起来调用 i g e t ( 1 , 99 ) iget(1, 99) iget(1,99) 实则调用的 i g e t ( 2 , 1 ) iget(2, 1) iget(2,1),这也就是为什么说将文件系统挂载到某个目录之后,这个目录就被屏蔽了的原因所在

到此,对文件系统的挂载应该有个很清晰的认识呢,最后来看看文件系统的卸载,基本上就是挂载的逆操作,来简单看看:

int sys_umount(char * dev_name)
{
	struct m_inode * inode;
	struct super_block * sb;
	int dev;

	if (!(inode=namei(dev_name)))  //解析获取设备文件的inode
		return -ENOENT;
	dev = inode->i_zone[0];   //对于块/字符设备,设备号记录在i_zone[0]
	if (!S_ISBLK(inode->i_mode)) {  //如果不是块设备
		iput(inode);
		return -ENOTBLK;
	}
	iput(inode);  //释放设备文件的inode
	if (dev==ROOT_DEV)  //如果要卸载的是根文件系统
		return -EBUSY;
	if (!(sb=get_super(dev)) || !(sb->s_imount)) //如果没有获取到设备的超级块或者如果挂载点为空
		return -ENOENT;
	if (!sb->s_imount->i_mount) //如果挂载点的挂载标识为空
		printk("Mounted inode has i_mount=0\n");
    //检查是否有进程在使用将要卸载文件系统上的文件
	for (inode=inode_table+0 ; inode<inode_table+NR_INODE ; inode++) 
		if (inode->i_dev==dev && inode->i_count)
				return -EBUSY;
	sb->s_imount->i_mount=0;  //挂载标识设为0
	iput(sb->s_imount);   //释放挂载点的inode
	sb->s_imount = NULL;  //超级块的挂载点字段设为空
	iput(sb->s_isup);    //释放被卸载的文件系统的根目录inode
	sb->s_isup = NULL;   //根目录inode字段清0
	put_super(dev);    //释放设备超级块
	sync_dev(dev);     //更新的信息同步到设备
	return 0;
}

文件系统的卸载主要就是释放超级块,然后将一些字段值复原,具体见上面注释就不细说了。

好了本文关于文件系统的挂载就这么多,所以回到开头什么是挂载,但从实现上来说,就是将超级块加载到内存里面,因为超级块就是一个文件系统的元信息集合,超级块就能代表一个文件系统,所以将超级块加载到内存里面,我们就可以认为挂载了相应的文件系统。当然挂载这个机制不可能就只是靠超级块是否在内存里面来决定实现,还需要其他的函数来辅助,就比如说获取 i n o d e inode inode i g e t iget iget 函数,这个函数就会来判断当前获取的这个 i n o d e inode inode 是否为挂载点,如果是,那就需要屏蔽当前这个 i n o d e inode inode 指向的目录文件,然后将其替换为被挂载的文件系统的根目录。这些总总加起来应该才算是挂载这个机制的实现,而不是说单靠一个 m o u n t mount mount 函数就实现了挂载的机制。

O K OK OK 本文就到这里了,有什么问题还请批评指正,也欢迎大家来同我探讨交流一起学习一起进步。

  • 首发公号:Rand_cs
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐