由之前的分析中我们知道,挂载根文件系统后,系统里存在根文件系统的超级块和一个根节点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时,调用的就是新文件系统的操作函数集了。这就实现了跨文件系统的操作。