到底什么是挂载?_文件系统

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

到底什么是挂载?_文件系统 中有专门的命令来挂载文件系统,mount device dir到底什么是挂载?_操作系统_03 为要挂载的设备文件名,到底什么是挂载?_缓存_04

每个设备都有一个设备号来标识,设备号可以分为两部分,一部分叫做主设备号 到底什么是挂载?_linux_05 到底什么是挂载?_linux_06,它用来标识某一类型的设备,比如说磁盘。另一部分叫做次设备号 到底什么是挂载?_文件系统_07 到底什么是挂载?_linux_06,它来标识某一具体设备,比如说磁盘上的某一具体分区。

当文件系统挂载到某个目录后,我们就可以通过这个目录来访问该文件系统,很多地方就只是简单的这样讲了一下,但其实这只能说是挂载的作用,那到底什么是挂载,要想解决这个问题还是只能从源码着手。下面我将根据 到底什么是挂载?_文件系统 到底什么是挂载?_文件系统_10

数据结构

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
};

所谓的内存中的超级块和 到底什么是挂载?_设备号_11

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

可以看出在 到底什么是挂载?_文件系统 到底什么是挂载?_文件系统_10 里面内存中最多同时存在 8 个超级块和 32 个文件的 到底什么是挂载?_设备号_11。这个缓存与我们平时所说的缓存差不多,当系统要想获取一个 到底什么是挂载?_设备号_11 时,会先在 到底什么是挂载?_文件系统_16 中寻找有没有该 到底什么是挂载?_设备号_11,如果有的话就直接返回,如果没有,就从设备上将 到底什么是挂载?_设备号_11 读到 到底什么是挂载?_文件系统_16

相关操作

在看 到底什么是挂载?_缓存_20

static struct super_block * read_super(int dev);

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

struct super_block * get_super(int dev);

根据设备号 到底什么是挂载?_linux_21

void put_super(int dev);

释放指定的设备超级块,也就是将超级块的 到底什么是挂载?_设备号_22

struct m_inode * namei(const char * pathname);

到底什么是挂载?_linux_23 函数根据路径 到底什么是挂载?_linux_24 获取末尾文件的 到底什么是挂载?_设备号_11,这个函数应该是有印象的吧,在 到底什么是挂载?_设备号_26 中也有类似的函数。如果不太清楚的话,我这里举个例子:如果参数路径是 /a/b/c,调用 到底什么是挂载?_linux_23 之后就会返回文件 到底什么是挂载?_文件系统_28到底什么是挂载?_设备号_11

挂载

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

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卸载文件系统时释放
}

其流程图为:

到底什么是挂载?_设备号_30

上图为 到底什么是挂载?_缓存_20 的实现过程,除了各种检查之外,到底什么是挂载?_缓存_20

  1. 将要挂载的文件系统的超级块读到内存里的超级块槽
  2. 设置该超级块的 到底什么是挂载?_设备号_33 字段为 挂载点的 到底什么是挂载?_缓存_34,表示文件系统挂载到这个目录上,设置挂载点 到底什么是挂载?_缓存_34到底什么是挂载?_linux_36

另外再解释几点:

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

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

这个问题我在 到底什么是挂载?_设备号_26 文件系统里面也详细说过,到底什么是挂载?_文件系统 里也类似。这里我们假设 到底什么是挂载?_linux_41 文件都是目录文件,到底什么是挂载?_文件系统_38 是一个普通文件。首先根目录文件就是一个个目录项,在其中寻找文件名为 到底什么是挂载?_linux_41 的目录项,从中获取 到底什么是挂载?_linux_41 目录文件的 到底什么是挂载?_linux_45,根据 到底什么是挂载?_linux_45 的索引字段找到 到底什么是挂载?_linux_41 目录文件的数据,也是一个个目录项,在其中寻找文件名为 到底什么是挂载?_文件系统_38 的目录项,从中获取普通文件 到底什么是挂载?_文件系统_38到底什么是挂载?_缓存_50

上述所说的获取某个 到底什么是挂载?_设备号_11,使用的函数是,到底什么是挂载?_操作系统_52,其意为从设备 到底什么是挂载?_linux_21 中获取编号为 到底什么是挂载?_缓存_54到底什么是挂载?_设备号_11。这个函数就会判断编号为 到底什么是挂载?_缓存_54到底什么是挂载?_设备号_11

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
	}
}

其完整的流程图如下:

到底什么是挂载?_文件系统_58

这是我根据赵炯画的图改编,因为没有详细讲述 到底什么是挂载?_文件系统_59

如果在 到底什么是挂载?_文件系统_16 中找到相应的 到底什么是挂载?_设备号_11,就判断 到底什么是挂载?_缓存_62,如果为真,表示该 $inode $表示的目录文件上面挂载的有文件系统。此时这个目录应该表示被挂载的文件系统的根目录,所以设置 到底什么是挂载?_缓存_63到底什么是挂载?_缓存_64,原目录就被隐藏掉了。举个例子再说明一下,假如调用 到底什么是挂载?_操作系统_65,本来我是要获取 1 号设备的第 99 个 到底什么是挂载?_设备号_11,然后发现这个 到底什么是挂载?_设备号_11 指向的目录上面挂载的有 2 号设备的文件系统,那么我们就去寻找 2 号设备的根目录 到底什么是挂载?_设备号_11 然后返回。所以看起来调用 到底什么是挂载?_操作系统_65 实则调用的 到底什么是挂载?_缓存_70,这也就是为什么说将文件系统挂载到某个目录之后,这个目录就被屏蔽了的原因所在

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

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;
}

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

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

到底什么是挂载?_缓存_76