揭开虚拟文件系统的云雾之多文件系统是如何运作的(基于linux1.2.13)

由之前的分析中我们知道,挂载根文件系统后,系统里存在根文件系统的超级块和一个根节点inode。并设置了init进程的工作目录和当前目录为根节点。
我们知道文件操作是从open开始的,open就是根据文件路径找到对应的inode。并返回一个fd,后续的文件操作就可以通过fd找到inode,执行读写操作。所以我们就以open函数为例。分析多文件系统的运作。看看虚拟文件系统在抹平各个文件系统的差异后,又是如何决定使用哪个文件系统的。open函数的执行过程之前在这篇文章已经分析过,但是这篇文章里只是分析了某个文件系统中open函数的的调用过程。问题是,操作系统是如何知道应该使用哪个文件系统的呢?
这就是这篇文章的内容,让我们开始分析。阅读下面的内容之前,最好想看一下open函数执行过程的那篇文章。这里不分析open函数的过程了。我们看到open函数的执行过程中,最后通过lookup函数找到文件对应的inode节点。这就是魔法的开始,我们直接从这开始分析。lookup的函数核心代码是

return dir->i_op->lookup(dir,name,len,result);

dir->i_op->lookup函数的值是根文件系统中定义,我们假设根文件系统是ext,我们来看看ext中lookup的实现。

int ext_lookup(struct inode * dir,const char * name, int len,
	struct inode ** result)
{
	int ino;
	struct ext_dir_entry * de;
	struct buffer_head * bh;

	*result = NULL;
	if (!dir)
		return -ENOENT;
	if (!S_ISDIR(dir->i_mode)) {
		iput(dir);
		return -ENOENT;
	}
	// 找到文件或目录对应的inode
	if (!(bh = ext_find_entry(dir,name,len,&de,NULL,NULL))) {
		iput(dir);
		return -ENOENT;
	}
	ino = de->inode;
	brelse(bh);
	// 跨文件系统实现的关键代码
	if (!(*result = iget(dir->i_sb,ino))) {
		iput(dir);
		return -EACCES;
	}
	iput(dir);
	return 0;
}

就两个函数ext_find_entry和iget。ext_find_entry的实现就是从硬盘中读取目录里的内容,然后找到文件对应的inode号。再根据inode号,调用iget函数把他从硬盘中读进来。我们去看iget的实现,这是实现跨文件系统的关键。看到这个我们就知道操作系统是如何协调多个文件系统运作的了。

extern inline struct inode * iget(struct super_block * sb,int nr)
{
	return __iget(sb,nr,1);
}

iget是对__iget函数的封装。在看这个函数之前,我们要先看一个东西,那就是在根文件系统中挂载其他文件系统的实现。在根文件系统中挂载其他文件系统是通过sys_mount函数实现的。这个函数调用了do_mount函数实现挂载。挂载文件系统主要有三个参数

需要挂载的设备  挂载点  文件系统类型

下面看看sys_mount的主要代码。假设参数是

/dev/sda1 /hello ext2
retval = namei(dev_name,&inode);
dev = inode->i_rdev;
retval = do_mount(dev,dir_name,t,flags,(void *) page);

首先通过文件名(/dev/sda1)找到对应的inode节点。然后从inode节点中得到设备号。然后调用do_mount,下面看看该函数的代码。

static int do_mount(dev_t dev, const char * dir, char * type, int flags, void * data)
{
	struct inode * dir_i;
	struct super_block * sb;
	int error;
	// 找到挂载点的inode,存在dir_i中
	error = namei(dir,&dir_i);
	if (error)
		return error;
	// 已经挂载了其他文件系统(需要调re_mount)或者该inode正在被使用
	if (dir_i->i_count != 1 || dir_i->i_mount) {
		iput(dir_i);
		return -EBUSY;
	}
	// 不是目录不能挂载
	if (!S_ISDIR(dir_i->i_mode)) {
		iput(dir_i);
		return -ENOTDIR;
	}
	if (!fs_may_mount(dev)) {
		iput(dir_i);
		return -EBUSY;
	}
	// 读取超级块和根节点
	sb = read_super(dev,type,flags,data,0);
	if (!sb) {
		iput(dir_i);
		return -EINVAL;
	}
	if (sb->s_covered) {
		iput(dir_i);
		return -EBUSY;
	}
	// 挂载点和新文件系统互相关联
	sb->s_covered = dir_i;
	dir_i->i_mount = sb->s_mounted;
	return 0;		/* we don't iput(dir_i) - see umount */
}

主要的逻辑是读取新文件系统的超级块和根节点,然后和挂载点进行关联。这里还是的新文件系统假设是ext,那么read_super的具体代码在ext_read_super,这里就不贴了。其中读取根节点的时候会调用iget函数。即上面我们看到的iget,实际上调了__iget。下面我们来看看这个__iget函数到底做了什么。

struct inode * __iget(struct super_block * sb, int nr, int crossmntp)
{
	static struct wait_queue * update_wait = NULL;
	struct inode_hash_entry * h;
	struct inode * inode;
	struct inode * empty = NULL;

	if (!sb)
		panic("VFS: iget with sb==NULL");
	// 根据设备号和inode号获取哈希表位置
	h = hash(sb->s_dev, nr);
repeat:
	for (inode = h->inode; inode ; inode = inode->i_hash_next)
		// 设备相等并且inode号相等
		if (inode->i_dev == sb->s_dev && inode->i_ino == nr)
			goto found_it;
	
	if (!empty) {
		h->updating++;
		// 获取一个空闲inode
		empty = get_empty_inode();
		if (!--h->updating)
			wake_up(&update_wait);
		if (empty)
			goto repeat;
		return (NULL);
	}
	inode = empty;
	inode->i_sb = sb;
	inode->i_dev = sb->s_dev;
	inode->i_ino = nr;
	inode->i_flags = sb->s_flags;
	put_last_free(inode);
	insert_inode_hash(inode);
	read_inode(inode);
	goto return_it;

found_it:
	// 找到了,该inode还没有被引用则引用数减一,如果被引用了说明之前就减过一了
	if (!inode->i_count)
		nr_free_inodes--;
	// inode引用数加一
	inode->i_count++;
	// 可能被锁,需要阻塞
	wait_on_inode(inode);
	// 唤醒后发现被改了,重新找
	if (inode->i_dev != sb->s_dev || inode->i_ino != nr) {
		printk("Whee.. inode changed from under us. Tell Linus\n");
		iput(inode);
		goto repeat;
	}
	// 跨文件系统的实现
	if (crossmntp && inode->i_mount) {
		struct inode * tmp = inode->i_mount;
		tmp->i_count++;
		iput(inode);
		inode = tmp;
		wait_on_inode(inode);
	}
	if (empty)
		iput(empty);

return_it:
	while (h->updating)
		sleep_on(&update_wait);
	return inode;
}

操作系统用了一个哈希表(链式地址法解决冲突)缓存了inode节点。因为这里读取的是根节点,所以哈希表里没有这个inode。这时候会走到if (!empty)这个地址。分配一个新的inode,把数据从硬盘读取进来,然后插入到哈希表(这时候挂载点和新文件系统的根节点对应的inode都在哈希表里了)。返回。我们回到最开始的地方,即第一次调用__iget。假设我们当前读取/hello/1.txt的内容。当操作系统通过根文件系统ext的ext_lookup函数查找hello对应的inode时,ext_find_entry函数返回了hello对应的inode号。然后通过iget函数读取inode节点的内容时,会调用到__iget函数,调用__iget的时候,因为可以从哈希表里找到了该inode,所以直接执行到found_it那里。

// 跨文件系统的实现
	if (crossmntp && inode->i_mount) {
		struct inode * tmp = inode->i_mount;
		tmp->i_count++;
		iput(inode);
		inode = tmp;
		wait_on_inode(inode);
	}

以上代码判断出hello这个目录挂载了一个inode(即新文件系统的根节点),然后返回新文件系统对应的根节点。所以我们访问/hello的时候,得到的是新文件系统的根节点,我们知道inode里保存了他的操作函数集。后面通过lookup查找hello里的1.txt时,调用的就是新文件系统的操作函数集了。这就实现了跨文件系统的操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值