前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >揭开虚拟文件系统的云雾之多文件系统是如何运作的(基于linux1.2.13)

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

作者头像
theanarkh
发布2020-01-02 11:53:47
6140
发布2020-01-02 11:53:47
举报
文章被收录于专栏:原创分享

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

代码语言:javascript
复制
return dir->i_op->lookup(dir,name,len,result);

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

代码语言:javascript
复制
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的实现,这是实现跨文件系统的关键。看到这个我们就知道操作系统是如何协调多个文件系统运作的了。

代码语言:javascript
复制
extern inline struct inode * iget(struct super_block * sb,int nr)
{
    return __iget(sb,nr,1);
}

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

代码语言:javascript
复制
需要挂载的设备  挂载点  文件系统类型

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

代码语言:javascript
复制
/dev/sda1 /hello ext2
代码语言:javascript
复制
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,下面看看该函数的代码。

代码语言:javascript
复制
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函数到底做了什么。

代码语言:javascript
复制
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那里。

代码语言:javascript
复制
// 跨文件系统的实现
    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时,调用的就是新文件系统的操作函数集了。这就实现了跨文件系统的操作。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-12-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档