文件系统在linux这个“阿房宫”中扮演着重要的角色,它与许多其他子系统紧密联系;关系错中复杂,但是要想对linux有一个深入的理解,文件系统这关是必须要过的。
在linux中用虚拟文件系统(VFS)来为linux文件系统提供一个统一的模型,VFS是一个纯软件机制;它为文件系统的操作提供了一个统一的接口)(文件read,write……);是linux实现’一切皆文件’的口号实现的基础。
顺着上面的程序流程图,我们来具体跟一下源代码; 挂载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是如何发展的,既目录和文件的建立。