Linux文件系统挂载的内核实现

大纲
0x1. 分析的思路和方法
0x2. 搭建实验环境
0x3. 探索mount的过程
0x4. 对架构的思考

0x1. 分析的思路和方法

面对复杂且庞大的项目,如何高效地分析自己感兴趣的模块以达到充分理解的目的,对很多人来说都充满了挑战。我在这里提供一种思路供大家参考:

在理解整体逻辑的基础上,聚焦关键点,忽略大多数主题无关的细节。

这篇文章将会详细阐述这个思路的实践方式。

0x2. 搭建实验环境

搭建实验环境的目的,是要将Linux内核运行起来,运行起来之后我们可以输出日志信息,或人为地制造crash以让系统输出调用栈,调用栈和日志信息可以直观地反映出系统的执行流程,执行流程即是程序的整体脉络。下面你会看到,将系统运行起来是一种探索整体逻辑的有效手段。

0x21. 内核版本与运行平台

内核版本:goldfish3.4
运行平台:qemu

0x22. 文件系统类型

hellofs:https://github.com/accelazh/hellofs

hellofs是acelazh根据开源项目改造的一个文件系统,可以说是一个最小功能的文件系统。选择这个文件系统的原因,主要是因为其结构简单,可以让我们聚焦在文件系统挂载的关键流程上。

0x23. hellofs的移植

hellofs的目标平台是x86_64,而我们的运行平台是arm,进行一些简单的改造即可移植到goldfish。

0x3. 对mount的探索

0x31. 绘制UML序列图

你可能以为我在开玩笑,因为我一行代码都没有分析,上来就要绘制UML序列图!是的,我将主要通过动态的方式来绘制UML序列图,以整体上理解mount的执行流程。下面请允许我开始我的表演!

0x311. 制造crash

扫描二维码关注公众号,回复: 9568690 查看本文章

在hellofs文件系统源码中加入一个BUG(需要保证mount过程中内核发生crash),比如hellofs_fill_super(如果没有触发到那就多加几个函数),然后编译安装模块,接着开始挂载:

mount -o loop,owner,group,users -t hellofs /data/local/tmp/hello.img /sdcard/mnt

接下来内核发生了crash,从crash日志中,我们提取到如下函数调用栈:

(hellofs_fill_super [hellofs]) from (mount_bdev) (mount_bdev) from
(hellofs_mount [hellofs]) (hellofs_mount [hellofs]) from (mount_fs)
(mount_fs from (vfs_kern_mount) (vfs_kern_mount) from (do_mount)
(do_mount) from (sys_mount) (sys_mount) from (ret_fast_syscall)

0x312. 补全调用栈

因为hellofs的体量相当小,所以我在其中的每个函数入口处均加入了一个日志打印宏,再次运行之后,得到如下信息:

drivers/…/…/hellofs/super.c(58)-<hellofs_mount>:
drivers/…/…/hellofs/super.c(10)-<hellofs_fill_super>:
drivers/…/…/hellofs/inode.c(87)-<hellofs_get_hellofs_inode>:
drivers/…/…/hellofs/inode.c(22)-<hellofs_fill_inode>: hellofs is
succesfully mounted on: /dev/block/loop0

0x333. 绘制UML序列图

有了前两步铺垫,我们能够轻易得到mount的内核执行序列图:
mount的流程
根据UML序列图,我们可以清楚地看到hellofs为kernel提供了两个接口,一个是hellofs_mount,一个是hellofs_fill_super。

0x32. 理解mount的流程

整个UML序列图可以粗略地总结为如下三个要点:

  1. 用户空间执行mount命令,经由系统调用传递到内核,内核通过参数识别出需要挂载的文件系统为hellofs,hellofs的挂载接口hellofs_mount被调用;
  2. hellofs_mount调用的最终结果,是返回给内核dentry指针,dentry指针即是已经挂载好的文件系统入口;
  3. hellofs挂载的主要逻辑,实际上还是由内核自己完成,即mount_bdev函数完成了主要的挂载任务,只不过mount_bdev在完成挂载任务的过程中,仍需要hellofs执行一些协助工作,这个工作就是hellofs_fill_super。

综上步骤,我们可以再次提练挂载的关键信息:

在hellofs_fill_super函数的帮助下,内核mount_bdev函数完成了主要的挂载工作。

我们可以进一步地猜想,是不是所有类型的文件系统挂载,都是通过mount_bdev来完成的呢?为了验证这一猜想,我绘制了内核中mount_bdev的调用关系图:
mount_bdev

我们可以直观地看到,很多文件系统的挂载,都是通过mount_bdev函数来完成的。

最后只剩下两个问题:

  1. mount_bdev为什么需要hellofs_fill_super的协助
  2. mount_bdev是如何完成挂载的

关于第一个问题,通过简单的推理即可达到初步理解其原理的目的,我的推理如下:

内核需要将hellofs挂载到当前目录树下,但它并不知道hellofs的格式、属性以及函数行为,所以它需要hellofs提供一个接口,来告诉它hellofs的格式、属性和函数行为是怎样的,比如块的大小和数量。

接着,我们继续探讨第二个问题,mount_bdev究竟是如何完成挂载的呢?要搞清楚这个问题,我们必须从mount_bdev的源码着手了。

struct dentry *mount_bdev(struct file_system_type *fs_type,
    int flags, const char *dev_name, void *data,
    int (*fill_super)(struct super_block *, void *, int))
{
    struct block_device *bdev;
    struct super_block *s;
    fmode_t mode = FMODE_READ | FMODE_EXCL;
    int error = 0;

    ……
    bdev = blkdev_get_by_path(dev_name, mode, fs_type);
    ……
    s = sget(fs_type, test_bdev_super, set_bdev_super, flags | MS_NOSEC,
         bdev);
   
    char b[BDEVNAME_SIZE];

    s->s_mode = mode;
    strlcpy(s->s_id, bdevname(bdev, b), sizeof(s->s_id));
    sb_set_blocksize(s, block_size(bdev));
    error = fill_super(s, data, flags & MS_SILENT ? 1 : 0);
    if (error) {
        deactivate_locked_super(s);
        goto error;
    }

    s->s_flags |= MS_ACTIVE;
    bdev->bd_super = s;
   
    return dget(s->s_root);
    ……
}

我们将一些错误和同步处理相关的代码去掉,仅关注mount的主要逻辑,要点如下:

  1. 获取被挂载设备的指针bdev;
  2. 申请一个super_block结构体对象,并作了一些简单的初始化,比如将bdev关联到super_block;
  3. 调用fill_super进一步填充这个对象,最后返回super_block的s_root字段。

调用结束之后,我们即获取了hellofs文件系统的入口——s_root,这个入口即指向/sdcard/mnt。但故事并没有结束,因为我们还有一些疑问要解决。

到目前为止,我们知道了super_block是如何同块设备关联起来的,但是我们还没看到文件系统的入口在哪里初始化,以及文件系统的函数行为是怎么被关联到super_block的。

进一步研究,我们需要看看hellofs_fill_super的实现逻辑。

该函数的主要逻辑包括:

static int hellofs_fill_super(struct super_block *sb, void *data, int silent) {
    struct inode *root_inode;
    struct hellofs_inode *root_hellofs_inode;
    struct buffer_head *bh;
    struct hellofs_superblock *hellofs_sb;
    int ret = 0;

    bh = sb_bread(sb, HELLOFS_SUPERBLOCK_BLOCK_NO);
    BUG_ON(!bh);
    hellofs_sb = (struct hellofs_superblock *)bh->b_data;
    ……
    sb->s_magic = hellofs_sb->magic;
    sb->s_fs_info = hellofs_sb;
    sb->s_maxbytes = hellofs_sb->blocksize;
    sb->s_op = &hellofs_sb_ops;

    root_hellofs_inode = hellofs_get_hellofs_inode(sb, HELLOFS_ROOTDIR_INODE_NO);
    root_inode = new_inode(sb);
    ……
    hellofs_fill_inode(sb, root_inode, root_hellofs_inode);
    inode_init_owner(root_inode, NULL, root_inode->i_mode);

    sb->s_root = d_make_root(root_inode);
    ……
    return ret;
}
  1. 从块设备读取hellofs_superblock结构的数据,以初始化该结构体对象,最终传递给super_block结构对象;
  2. 将hellofs_superblock的操作函数赋给super_block对象;
  3. 分配并填充root_inode,赋给super_block结构体对象的s_root字段。

至此,super_block结构体对象已经成功关联了被挂载的块设备、hellofs_superblock的操作函数、以及文件系统根目录的内核表示对象root_inode。

我们将这部分内容稍微总结一下:

  1. 从设计模式的角度来看,super_block结构实际上属于Linux内核提供的文件系统适配接口,开发者将自己的super block——hellofs_superblock与super_block进行适配,以实现自己的文件系统与内核的挂接;
  2. 挂接成功之后,用户空间传递下来的所有请求,都经由super_block结构对象转发给hellofs_superblock进行处理。

从这个角度来看,我们发现mount过程的技术本质就是对super_block的各个字段进行填充。

既然super_block这么重要,我们就简单看看它的定义:

struct super_block {
    struct list_head    s_list;     /* Keep this first */
    dev_t           s_dev;      /* search index; _not_ kdev_t */
    unsigned char       s_blocksize_bits;
    unsigned long       s_blocksize;
    loff_t          s_maxbytes; /* Max file size */
    struct file_system_type *s_type;
    const struct super_operations   *s_op;
    const struct dquot_operations   *dq_op;
    const struct quotactl_ops   *s_qcop;
    const struct export_operations *s_export_op;
    unsigned long       s_magic;
    struct dentry       *s_root;
    struct rw_semaphore s_umount;
    int         s_count;
    atomic_t        s_active;
    ……
    const struct xattr_handler **s_xattr;

    struct hlist_bl_head    s_anon;     /* anonymous dentries for (nfs) exporting */
    struct list_head    s_mounts;   /* list of mounts; _not_ for fs use */
    struct block_device *s_bdev;
    struct backing_dev_info *s_bdi;
    struct mtd_info     *s_mtd;
    struct hlist_node   s_instances;
    unsigned int        s_quota_types;  /* Bitmask of supported quota types */
    struct quota_info   s_dquot;    /* Diskquota specific options */

    struct sb_writers   s_writers;

    char s_id[32];              /* Informational name */
    u8 s_uuid[16];              /* UUID */

    void            *s_fs_info; /* Filesystem private info */

    ……
    const struct dentry_operations *s_d_op; /* default d_op for dentries */

    ……
    struct list_head    s_inodes;   /* all inodes */
};

可以看到,super_block包含了一个文件系统的方方面面,包括对应的块设备、根目录、所有的inode节点、文件系统的操作函数指针等等。

最后,我们进一步总结出如下观点:

实现自定义文件系统的主要任务,就是对super_block结构进行适配。

0x4. 对架构的思考

Linux很强大的一个地方就在于它支持几乎所有文件系统,从这次mount的探索过程中,我们可以窥见其实现这种强大支持的原理:

  1. 由内核提供文件系统访问的接口,但不提供具体的实现,也不需要关心具体的实现;
  2. 不同的文件系统根据内核定义的接口自由地扩展。

进一步总结,即是面向接口编程的精髓:

高层模块定义接口,不关心具体实现;底层模块依赖于接口,根据接口完成功能的扩展。

发布了2 篇原创文章 · 获赞 3 · 访问量 583

猜你喜欢

转载自blog.csdn.net/didaliping/article/details/104558860