Linux VFS文件系统之rootfs注册与挂载

------------------------------------------------
#纯属个人理解,如有问题敬请谅解!
#kernel version: 2.6.26
#Author: andy wang
-------------------------------------------------

一: 序言

    文件系统在linux这个“阿房宫”中扮演着重要的角色,它与许多其他子系统紧密联系;关系错中复杂,但是要想对linux有一个深入的理解,文件系统这关是必须要过的。
    在linux中用虚拟文件系统(VFS)来为linux文件系统提供一个统一的模型,VFS是一个纯软件机制;它为文件系统的操作提供了一个统一的接口)(文件read,write……);是linux实现’一切皆文件’的口号实现的基础。

二:rootfs注册

    既然是一棵树那么根就是它存在的基础, 所以要研究VFS我们必须从文件系统的根入手;那么VFS的根什么咋个建立起来的呢? 以及VFS这颗大树是如何发展起来的呢?要弄清楚这么都必须先从rootfs入手,接下来我们就具体看一下rootfs文件系统是咋个建立起来的。
    在linux kernel初始化阶段会调用int __init init_rootfs(void)向内核注册rootfs文件系统,下面看看rootfs怎样注册到内核中的:

 int __init init_rootfs(void)                                                     
{                                                                                
        int err;                                                                 
                                                                                 
        err = bdi_init(&ramfs_backing_dev_info);                                 
        if (err)                                                                 
                return err;                                                      
                                                                                 
        err = register_filesystem(&rootfs_fs_type);                              
        if (err)                                                                 
                bdi_destroy(&ramfs_backing_dev_info);                            
                                                                                 
        return err;                                                              
}    
    这个函数比较简单,函数被宏__init修饰,说明该函数被linux链接脚本链接到.init段中,在系统初始化的时候调用,而在调用完之后会将其占用的内存释放掉,在kernel中定义的一些修饰宏还是挺有意思的,以后有时间得专门写个文档..好了我们来看看代码, 其中核心的函数为register_filesystem();这个函数将结构file_system_type 注册在内核一个单链表中,每个文件系统必须调用此函数来注册到内核.    

三: rootfs挂载

     此时该文件系统已经成功注册到内核中,在后面的挂载文件系统中需要再次遍历链表取出对于的file_system_type结构进行挂载。接下来该分析rootfs挂载过程。挂载rootfs首先会调用do_kern_mount()函数,具体代码段为:

struct vfsmount * do_kern_mount(const char *fstype, int flags, const char *name, void *data){

         struct file_system_type *type = get_fs_type(fstype);

         struct vfsmount *mnt;

         if (!type)

                   return ERR_PTR(-ENODEV);

         mnt = vfs_kern_mount(type, flags, name, data);//挂载的核心函数

         …………….

         return mnt;

}
 
根据传入文件系统的名字fstype,调用get_fs_type()去匹配刚才注册的文件系统,如果未找到返回失败.接下来会调用挂载文件系统的核心函数vfs_kern_mount();

struct vfsmount * vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data){

         ………….. 

         mnt = alloc_vfsmnt(name);//在cache中分配一个vfsmount,并对其进行初始化

         if (!mnt)

                   goto out;

         ………..

         error = type->get_sb(type, flags, name, data, mnt);//分配文件系统超级块的回调函数
         if (error < 0)
                   goto out_free_secdata;
         BUG_ON(!mnt->mnt_sb);
         error = security_sb_kern_mount(mnt->mnt_sb, secdata);

         if (error)
                  goto out_sb;
         mnt->mnt_mountpoint = mnt->mnt_root;

         mnt->mnt_parent = mnt;

         up_write(&mnt->mnt_sb->s_umount);

         free_secdata(secdata);

         return mnt;

         ……….

}

    在alloc_vfsmnt()中,会在cache中动态分配并并初始化一个vfsmount, 由内核动态分配一个mnt_id,再初始化一些链表头和vfsmount名字。接下来会调用一个重要的回调函数get_sb为文件系统建立一个超级块。

在rootfs中回调函数为rootfs_get_sb();

static int rootfs_get_sb(struct file_system_type *fs_type,int flags, const char *dev_name, void *data, struct vfsmount *mnt)
{
     return get_sb_nodev(fs_type, flags|MS_NOUSER, data, ramfs_fill_super, mnt);
}

    一般的伪文件系统或叫内存文件系统分配超级块都是调用get_sb_nodev()或者get_sb_single(); 而实际文件系统如磁盘文件系统,在分配超级块的时候则调用get_sb_bdev()需要在具体块设备上建立超级块。因为rootfs是内存文件系统所以调用get_sb_nodev();下面跟一下这个代码的实现:

int get_sb_nodev(struct file_system_type *fs_type,int flags, void *data,int (*fill_super)(struct super_block *, void *, int), struct vfsmount *mnt)

{
         int error;
         struct super_block *s = sget(fs_type, NULL, set_anon_super, NULL);//在内存中分配一个超级块
 
         if (IS_ERR(s))
                   return PTR_ERR(s);
         s->s_flags = flags;
         error = fill_super(s, data, flags & MS_SILENT ? 1 : 0);//填充超级块的回调函数

         if (error) {
                   up_write(&s->s_umount);
                   deactivate_super(s);//kill超级块,会调用fs->kill_sb(s)
                   return error;

         }

         s->s_flags |= MS_ACTIVE;

         return simple_set_mnt(mnt, s);//关联超级块和vfsmount

}

 

在这个函数中重点关注一下回调函数fill_super();这个函数会初始化超级块的部分成员,并建立VFS的根目录,也就是我们上面图中的 ”/” 。下面是rootfs fill_super的程序片段:

static int ramfs_fill_super(struct super_block * sb, void * data, int silent){

         struct inode * inode;

         struct dentry * root;

         sb->s_maxbytes = MAX_LFS_FILESIZE;  //文件的最大值

         sb->s_blocksize = PAGE_CACHE_SIZE;  //以字节为单位块的大小

         sb->s_blocksize_bits = PAGE_CACHE_SHIFT;  //以位为单位块的大小

         sb->s_magic = RAMFS_MAGIC;  //文件系统的魔数

         sb->s_op = &ramfs_ops;  //超级块的方法 ,在处理inode的时候会有用

         sb->s_time_gran = 1;

         inode = ramfs_get_inode(sb, S_IFDIR | 0755, 0); //建立根目录索引节点

         if (!inode)

                   return -ENOMEM;

         root = d_alloc_root(inode); //建立根目录目录对象

         if (!root) {

                   iput(inode);

                   return -ENOMEM;

         }

         sb->s_root = root;  //超级块的s_root指向刚建立的根目录对象

         return 0;

}

    在linux文件系统的中目录也看成文件, 在建立一个文件的时候都会在目录高速缓存中建立一个目录项对象,并建立一个索引节点与之关联。注意一个目录项对象只能关联唯一一个索引节点,而一个索引节点却可以对应多个目录项对象.比如我们常见的在linux 下的硬链接就是一个索引节点对于多个目录项对象。
    目录项对象的存在主要是为了我们进行路径的查找, 但是我们最终的目标是要找到目录项对象关联的索引节点,只有找到一个文件的索引节点才能找到指向该文件的操作方法。
    下面来看看文件索引节点的创建和初始化,这是一个比较中要的函数,因为一个文件的属性和操作方法都是在这个函数里面定义的。
    下面是rootfs的索引节点创建函数, 实际的磁盘文件系统(如EXT2)索引节点的创建更为复杂。程序片段为:

struct inode *ramfs_get_inode(struct super_block *sb, int mode, dev_t dev){

         struct inode * inode = new_inode(sb);//在索引节点高速缓存中创建一个inode 

         if (inode) {

                   inode->i_mode = mode;  //文件的类型

                   inode->i_uid = current->fsuid;

                   inode->i_gid = current->fsgid;

                   inode->i_blocks = 0;  //文件的块数

                   inode->i_mapping->a_ops = &ramfs_aops;

                   inode->i_mapping->backing_dev_info = &ramfs_backing_dev_info;

                   mapping_set_gfp_mask(inode->i_mapping, GFP_HIGHUSER);

                   inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME;

                   switch (mode & S_IFMT) {  //判断文件类型

                   default:

                         init_special_inode(inode, mode, dev); //特殊文件;如:字符~块设备文件,FIFO,SOCKET文件

                          break;

                   case S_IFREG: //普通文件

                            inode->i_op = &ramfs_file_inode_operations;  //索引节点的操作方法

                            inode->i_fop = &ramfs_file_operations;  //缺省普通文件的操作方法

                            break;

                   case S_IFDIR:  //目录文件

                            inode->i_op = &ramfs_dir_inode_operations; 

                            inode->i_fop = &simple_dir_operations;  //目录文件的操作方法 

                            /* directory inodes start off with i_nlink == 2 (for "." entry) */

                            inc_nlink(inode);

                            break;

                   case S_IFLNK:  //符号链接

                            inode->i_op = &page_symlink_inode_operations;

                            break;
                   }
         }

         return inode; //返回创建的inode与对应的目录项对象关联

}

在new_inode函数中就是分配并且初始化一个inode,需要特别关注的是在alloc_inode()函数中的一段代码:

if (sb->s_op->alloc_inode)

          inode = sb->s_op->alloc_inode(sb);
else
          inode = (struct inode *) kmem_cache_alloc(inode_cachep, GFP_KERNEL);

如果在超级块方法中定义了alloc_inode函数,在inode时就需要用此函数来分配,在这种情况下struct inode可能被包含在一个更大的结构中被分配,在rootfs中并没有此定义,在以后分析其他文件系统中会看到它的用处。否则就会在cache中分配一个inode结构.

接下来我们来看看inode操作方法和文件操作方法的定义,init_special_inode()都做了些啥子,这些函数将会为我们揭开文件的操作的神秘的面纱 ;先来看看特殊文件的操作方法是咋个定义的:

void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev){

         inode->i_mode = mode;

         if (S_ISCHR(mode)) {          //字符设备文件

                   inode->i_fop = &def_chr_fops;   //默认字符设操作方法

                   inode->i_rdev = rdev;

         } else if (S_ISBLK(mode)) {          //块设备文件

                   inode->i_fop = &def_blk_fops;     //默认块设备操作方法

                   inode->i_rdev = rdev;

         } else if (S_ISFIFO(mode))   //FIFO文件

                   inode->i_fop = &def_fifo_fops;    //默认FIFO文件操作方法

         else if (S_ISSOCK(mode))  //SOCKET文件

                   inode->i_fop = &bad_sock_fops;  //默认SOCKET文件的操作方法

         else
                   printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o)\n",

                          mode);

}
 
看了这个函数对于我们理解linux设备文件可能会有很大的帮助,这个在以后分析设备驱动模型的时候再写吧。

对于像rootfs,ramfs,tmpfs等内存文件系统,文件其实就是放在内存中的,对于文件的操作其实就是对内存的读写操作,这个又是一个很大的话题了,设计到linux内存管理等复杂东东了;等研究透linux内存管理再来研究这部分吧。

在建立好inode之后当然还需要创建rootfs根目录的目录项对象,不然VFS是找不到刚才建立的inode的,那还咋个操作文件呢,当然是空话了!!!       

来看看VFS根目录是咋个创建的:

struct dentry * d_alloc_root(struct inode * root_inode){

         struct dentry *res = NULL; 

         if (root_inode) {

                   static const struct qstr name = { .name = "/", .len = 1 }; 

                   res = d_alloc(NULL, &name);

                   if (res) {

                            res->d_sb = root_inode->i_sb; //指向该文件系统的超级块

                            res->d_parent = res;  //根目录的父亲当然是它自己了

                            d_instantiate(res, root_inode); //关联 dentry 和 inode

                   }

         }

         return res;

}

 这里的name=”/” 就是我们根目录的名字了,它就是VFS树根的名字;然后在目录项高速缓存中建立一个dentry ,最后一件重要的事情就是关联dentry和inode,看一下代码片断:

 void d_instantiate(struct dentry *entry, struct inode * inode){

BUG_ON(!list_empty(&entry->d_alias)); 

         spin_lock(&dcache_lock);

         if (inode)

                   list_add(&entry->d_alias, &inode->i_dentry);  //将dentry加到inde链表中

         entry->d_inode = inode;  //关联inode

         fsnotify_d_instantiate(entry, inode);

         spin_unlock(&dcache_lock);

         security_d_instantiate(entry, inode);

}

BUG_ON(!list_empty(&entry->d_alias));可以证明我们刚才说的 “一个目录项对象只能关联一个索引节点”

list_add(&entry->d_alias, &inode->i_dentry); 此可以证明刚才说的”一个索引节点可以关联多个目录项对象”   

最后sb->s_root = root;将超级块的根目录指向刚才建立的根目录, 至此ramfs_fill_super函数就完成了它的使命。不过我们会发现直到这里我们都没有看到最初建立的vfsmount ,不要着急下面我们会看到它了。

在执行完fill_super()回调函数以后, vfsmount就登场了,看下面的代码:

int simple_set_mnt(struct vfsmount *mnt, struct super_block *sb){

         mnt->mnt_sb = sb;  //对 mnt_sb超级块指针附值了

         mnt->mnt_root = dget(sb->s_root); // 对mnt_root指向的根目录附值

         return 0;

}

最后回到vfs_kern_mount()函数  还需要对vfsmount进行一些初始化:

mnt->mnt_mountpoint = mnt->mnt_root;   //文件系统挂载点目录,其实就是刚才建立的”/”目录

mnt->mnt_parent = mnt; //父对象是自己                 

到此rootfs挂载就算完成了,VFS的根就这样建立起来啦, 让我们看看内存中数据结构的关系图,“理理更清晰,洗洗更健康“。 

当然rootfs是一个特殊的文件系统,做了上面的工作是不够的,我们还需要建立一个命名空间,它主要是要将do_kern_mount()中建立起来的mnt和dentry记录在init进程中,这样在linux下fork出来的所有进程都会继承这样的属性。比如所有进程的根目录都是”/” .

{       ……….

         ns->root = mnt;

         mnt->mnt_ns = ns; 

         init_task.nsproxy->mnt_ns = ns;

         get_mnt_ns(ns);

         root.mnt = ns->root;

         root.dentry = ns->root->mnt_root; 

         set_fs_pwd(current->fs, &root);   //设置进程的当前目录

         set_fs_root(current->fs, &root);   //设置进程的根目录

}
 
四: 总结 

    本文介绍了rootfs的注册和挂载,可以说rootfs是VFS的基础, 有了rootfs文件系统VFS这颗树才可以发展壮大.下文会介绍VFS是如何发展的,既目录和文件的建立。

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值