Linux内核vfs层优化之实现文件系统umount状态读写文件

        近期发现不少机器异常重启(linux系统),一般重启都是发生内核crash了,会在/var/crash目录生成vmcore等crash日志,实际却没有,查看机器硬件日志,没有报错。硬件没有问题又不是内核crash,难不成是软件主动重启了?会不会应用层某种业务因为某种资源获取不到,主动发起了重启,就像执行了reboot重启命令一样,或者内核驱动因为检测到了未知异常,主动执行了重启函数?虽然这种可能性不大,但是暂时没有其他头绪,先从这个角度排查一下。

     这两种重启最终都会执行到内核kernel_restart()->kernel_restart_prepare()函数,然后从reboot_notifier_list链表中取出之前注册过的重启消息通知函数,而注册重启消息通知函数执行 register_reboot_notifier()即可。我们可以提前执行register_reboot_notifier()注册一个重启消息通知函数,比如reboot_notify_function()。当系统主动reboot重启后,将自动执行reboot_notify_function() 函数,在该函数将重启前进程信息、内核dmesg打印保存起来,重启后根据这些日志分析重启原因。那将这些日志保存到什么地方?可以保存到一片特定内存,可以裸的保存到一片特定的磁盘分区,可以保存到磁盘文件系统,前两种改动比较大,不适宜线上服务器调试。而保存到磁盘文件系统相对来说改动最小,把这些代码编译成ko驱动,insmod加载一下,等下次异常重启,自动抓取重启前的日志,对正常运行的服务器没有影响,改动最小,还不用修改编译内核,不用重启系统,调试方便

     新的问题来了,如果主动重启是内核或者应用层执行了reboot函数发起的,内核中直接调用filp_open/vfs_write/vfs_read即可读写文件,实现起来没难度。但是如果重启是应用层某处执行了类似reboot命令发起的,会先杀死应用层所有进程,卸载所有文件系统,最后reboot系统调用执行到内核中,由于此时文件系统已经全部umount了,直接执行filp_open文件会返回失败!因为filp_open打开文件是依赖根文件系统正常mount的。有些系统根文件系统会remount成只读,其它文件系统全部umount,此时也不能在根文件系统写文件,其它文件系统umount了也不行。

        这就引出了本次讨论的主题,如何在文件系统umount状态读写文件?摸索了几天,发现是可以实现的,核心代码只有几十行,挺有意思的,实现的代价还不大。现在就介绍一下这个实现过程,同时还会涉及到内核open文件过程、文件系统mount过程、/dev/sda块设备节点生成过程,涉及的内核函数只列出核心源码,删除了部分代码,方便理解。内核源码基版本3.10.96

1 文件open的内核源码实现

在讲解之前,先根据示意图从整体理解一下文件系统的一个大体组成。

如图所示,这是一个根文件系统的目录结构的简单演示,由根目录”/”指向”home”、”etc”等目录或者文件。但是对于内核来说,文件系统偏上层是以文件或者目录的dentry结构构成彼此的连接。如下图所示,

根目录”/”的dentry指向”home”、”etc”等目录或者文件的dentry,这里假设文件或者目录已经open过,相关文件系统数据结构保存在内存中。一个文件或者目录都有一个dentry结构,在文件或者目录搜索时,由文件系统的树状结构,搜索对应文件或者目录的dentry。下文讲解open过程,会多次提到dentry结构,下文如遇到dentry用法不理解的时候,可以再看看这个图。struct dentry结构的关键成员如下:

  1. struct dentry {
  2.     //如果设置了DCACHE_MOUNTED 标志位,表示该dentry是一个挂载点
  3.     unsigned int d_flags;      
  4.     //每个dentry结构都通过d_hash链入hashdentry_hashtable的某个队列
  5.     struct hlist_bl_node d_hash;    /* lookup hash list */
  6.     //指向父目录dentry
  7.     struct dentry *d_parent;   
  8.     //该目录或者文件的hash值,有时每个dentryd_name->name"/",这是因为该dentry是个挂载点
  9.     struct qstr d_name;
  10.     //就是对应的inode结构,一个dentry只对应一个inode
  11.     struct inode *d_inode;
  12.     //目录或者文件名字
  13.     unsigned char d_iname[DNAME_INLINE_LEN];
  14.     //子目录或者子文件的dentry通过d_child链入父目录dentryd_subdirs
  15.     struct list_head d_child;
  16.     struct list_head d_subdirs;
  17. };

现在开始正式讲解文件的open过程内核流程。假设本次要保存的日志路径是/home/reboot_info,系统启动过程执行了mount  –t  ext4  /dev/sda3   /home/,显然磁盘分区sda3挂载到了/home目录,reboot_info实际是在sda3磁盘ext4文件系统的根目录。open ”/home/reboot_info”的内核函数流程是,

filp_open->file_open_name->do_filp_open->path_openat,应用层调用open函数,内核的实现流程与此类似。path_openat函数核心源码如下:

  1. static struct file *path_openat(int dfd, struct filename *pathname,
  2.         struct nameidata *nd, const struct open_flags *op, int flags)
  3. {
  4.     struct file *file;
  5.     struct path path;
  6.     file = get_empty_filp();
  7.     // pathname->name 是文件路径,即”/home/reboot_info”
  8.     path_init(dfd, pathname->name, flags | LOOKUP_PARENT, nd, &base);
  9.     link_path_walk(pathname->name, nd);
  10.     do_last(nd, &path, file, op, &opened, pathname);
  11. }

get_empty_filp()得到一个空的struct file结构,每次open文件对应一个。path_init()主要是初始化struct nameidata *nd,这个nd贯穿open过程,它保存了每次搜索的文件或者目录的参数,核心成员如下:

  1. struct nameidata {
  2.     struct path path;
  3.     struct qstr last;
  4.     struct path root;
  5.     struct inode    *inode; /* path.dentry.d_inode */
  6. }

该结构的主要成员定义如下:

  1. //包含本次搜索的文件或者目录名称、hash值、名字字符串长度
  2. struct qstr {
  3.     union {
  4.         struct {
  5.             HASH_LEN_DECLARE;
  6.         };
  7.         u64 hash_len;
  8.     };
  9.     const unsigned char *name;
  10. };
  11. struct path {
  12.     //当前文件系统的vfsmount结构,
  13.     struct vfsmount *mnt;
  14.     //本次搜索到的目录或者文件的dentry
  15.     struct dentry *dentry;
  16. };
  17. struct vfsmount {
  18.     //文件系统的根目录dentry
  19.     struct dentry *mnt_root;
  20.     //文件系统super_block结构
  21.     struct super_block *mnt_sb;
  22.     int mnt_flags;
  23. };

struct nameidata *nd结构各个成员的设置见path_init()函数,下来就介绍path_openat()函数中执行的第一个重点函数--- path_init()

  1. static int path_init(int dfd, const char *name, unsigned int flags,
  2.              struct nameidata *nd, struct file **fp)
  3. {
  4.     //open文件的路径”/home/reboot_info”第一个字符是’/’,表示从根文件系统根目录开始搜索
  5.     if (*name=='/')
  6.     {
  7.         //该函数核心操作是nd->root = current->fs->root,设置为当前根文件系统的struct path *root
  8.         set_root(nd);
  9.         //nd->path保存nd->root的值
  10.         nd->path = nd->root;
  11.     }
  12.     //设置nd->inode为根文件系统根目录的inode结构
  13.     nd->inode = nd->path.dentry->d_inode;
  14. }

      path_init()函数已经去除不相关代码。本次open的文件路径是”/home/reboot_info”,name指向这个字符串,这表示从根文件系统根目录开始搜索文件,set_root(nd)其实就是nd->root = current->fs->root,current->fs->root-> dentry是根文件系统根目录dentry,nd->path = nd->root赋值后,nd->path->dentry就是根文件系统根目录dentry,这点之后的代码会用到,很关键。接着回到path_openat()函数中的link_path_walk()函数,这是搜索文件的核心。

  1. static int  link_path_walk(const char *name, struct nameidata *nd)
  2. {
  3.     //name初始指向”/home/reboot_info”第一个字符’/’,循环结束指向字符’h’
  4.     while (*name=='/')
  5.         name++;
  6.    
  7.     for(;;) {
  8.         struct qstr this;
  9.         //第一次循环是计算”home”字符串的hash值存于this.hash,返回字符串长度
  10.         len = hash_name(name, &this.hash);
  11.         //指向”/home/reboot_info””home”,表示本次搜索目录或者文件名称是”home”
  12.         this.name = name;
  13.         // len本次搜索的文件或者目录名称”home”字符串长度
  14.         this.len = len;
  15.        
  16.         type = LAST_NORM;
  17.         if (likely(type == LAST_NORM)) {
  18.             struct dentry *parent = nd->path.dentry;
  19.         }
  20.        
  21.         //nd->last就是包含当前搜索的文件或者目录名字字符串的hash值结构体
  22.         nd->last = this;
  23.         nd->last_type = type;
  24.        
  25.         if (!name[len])//本案例第2次循环到这里就会执行return 0返回
  26.             return 0;
  27.         do {
  28.             len++;
  29.         } while (unlikely(name[len] == '/'));
  30.         if (!name[len])
  31.             return 0;
  32.         //name指向”/home/reboot_info”中的”reboot_info”
  33.         name += len;
  34.         //搜索文件的核心函数
  35.         walk_component(nd, &next, LOOKUP_FOLLOW);
  36.     }
  37. }

      link_path_walk()函数是一个循环,依次搜索每个文件或者目录的dentry,具体实现是:先令name指针指向待本次搜索的文件或者目录名字字符串,比如第一次循环name指向”/home/reboot_info””home”字符串(第二次循环则指向”reboot_info”字符串),接着向struct qstr this结构填充本次搜索的目录”home”的hash值、字符串长度,然后nd->last = this,这是下一步能匹配搜索”home”目录的关键。下一步执行walk_component()函数开始真正的搜索。

  1. static inline int walk_component(struct nameidata *nd, struct path *path,
  2.         int follow)
  3. {
  4.     //在系统dentry  hash链表中搜索查找本次文件或者目录的dentry
  5.     err = lookup_fast(nd, path, &inode);
  6.     if (unlikely(err)) {
  7.     //找不到就调用ext4文件系统的lookup函数查找
  8.         err = lookup_slow(nd, path);
  9.         inode = path->dentry->d_inode;
  10.     }
  11.     //核心是两个赋值,nd->path.mnt = path->mnt nd->path.dentry = path->dentry
  12.     path_to_nameidata(path, nd);
  13.     nd->inode = inode;
  14. }

   第一次执行walk_component()函数,是搜索”home”目录的dentry是否存在,也可以说搜索根文件系统根目录下是否存在home目录? 接着看lookup_fast()函数。

  1. static int  lookup_fast(struct nameidata *nd,
  2.                struct path *path, struct inode **inode)
  3. {
  4.     struct vfsmount *mnt = nd->path.mnt;
  5.     //parent指向父目录dentry ,第一次搜索home目录时,nd->path.dentry path_init()函数中赋值,是根文件系统根目录dentry
  6.     struct dentry *dentry, *parent = nd->path.dentry;
  7.    
  8.     //dentry hash链表查找”home”目录dentry,基于nd->last(包含home目录名字的hash)和父目录dentry结构,即parent
  9.     dentry = __d_lookup_rcu(parent, &nd->last, &seq, nd->inode);
  10.     if (!dentry)
  11.         goto unlazy;
  12.     path->mnt = mnt;
  13.     //dentry此时是home目录的dentry
  14.     path->dentry = dentry;
  15.        //如果dentry是挂载点,要把path->mnt path->dentry切换为挂载源头文件系统的
  16.     if (unlikely(!__follow_mount_rcu(nd, path, inode)))
  17.       goto unlazy;
  18.     return 0;
  19. }

      第一次循环执行lookup_fast()是在根文件系统根目录下查找是否有home目录,parent指向根目录dentrynd->last包含查找home目录的hash值,__d_lookup_rcu(parent, &nd->last, &seq, nd->inode)结合parentnd->last在系统dentry  hash链表查找home目录的dentry如果之前openhome目录,就会把home目录dentry添加到系统dentry hash链表,添加的唯一标识是父目录dentry+home目录的名称的hash,将来就是根据二者从dentry hash链表查找到home目录dentry,其他目录或者文件的添加和搜索同理。__d_lookup_rcu()函数返回查找的目录dentry,即home目录dentry。查到失败返回NULL,此时上一级的lookup_fast()返回错误,表示没有找到home目录,就会调用lookup_slow(),调用ext4文件系统的ext4_lookup接口,从磁盘中读取home目录的raw inode 信息,创建home目录的inodedentry,然后把home目录dentry添加到系统dentry  hash链表,为了分析方便,这里假设home目录之前open过,其dentry结构已经存在于dentry  hash链表。

      接着__d_lookup_rcu函数向下执行,__follow_mount_rcu()函数很关键,它是判断本次查找到的path->dentry 这个dentry是否有挂载属性,这是什么意思?前文说过,系统启动时执行了mount –t ext4  /dev/sda3  /homesda3磁盘分区的ext4文件系统挂载到了home目录,则home目录就是一个挂载目录,有挂载属性,关于mount的过程,稍后也会详细讲解。

  1. static bool  __follow_mount_rcu(struct nameidata *nd, struct path *path,
  2.                    struct inode **inode)
  3. {
  4.      for (;;) {
  5.         struct mount *mounted;
  6.       //判断dentry->d_flags & DCACHE_MOUNTED,该目录是否有挂载属性,是为真
  7.         if (!d_mountpoint(path->dentry))
  8.             break;
  9.       //如果path->dentry是挂载点,则找到挂载源头的sda3 ext4文件系统的信息结构
  10.         mounted = __lookup_mnt(path->mnt, path->dentry, 1);
  11.         if (!mounted)
  12.             break;
  13.       // mounted->mnt mounted->mnt.mnt_root sda3 ext4文件系统的struct vfsmount结构和根目录dentry
  14.         path->mnt = &mounted->mnt;
  15. //查找到的dentry保存到path->dentry,注意,这里的dentry已经切换为sda3 ext4文件系统的dentry结构,不再是根文件系统下home目录的dentry
  16.         path->dentry = mounted->mnt.mnt_root;
  17.         //sda3 ext4文件系统的根目录inode
  18.         *inode = path->dentry->d_inode;
  19.      }
  20.      return true;
  21. }

      __follow_mount_rcu()函数就是判断path->dentry是否是挂载目录,如果是则找到挂载源头的那个sda3 ext4文件系统的根dentry

      好了,经历了千辛万苦,终于从lookup_fast()函数返回到walk_component()函数,此时,home目录dentry已经找到了,保存在path->dentry如果home目录是挂载点,则path->dentry是挂载源头sda3 ext4文件系统的根目录dentrypath->mntsda3 ext4文件系统的struct vfsmount结构。然后执行path_to_nameidata(),其实是nd->path.mnt = path->mnt nd->path.dentry = path->dentry两个赋值。然后从walk_component()返回到link_path_walk(),此时nd->path.dentry已经更新为home目录的dentry,本例由于home是挂载点,所以是sda3 ext4文件系统的根目录dentry。之后在link_path_walk()函数开始下一轮循环,令name指针指向”/home/reboot_info””reboot_info”,然后计算”reboot_info”hash值,接着就以sda3 ext4 的根目录dentry加上”reboot_info”hash值,在系统dentry hash链表查找是否有reboot_info文件的dentry,不过这个过程是在do_last()函数完成。因为查找文件时,link_path_walk()函数会在中间return 0返回,请看该函数中的注释。不过没关系,do_last函数查找文件也是同样的道理。

  1. static int  do_last(struct nameidata *nd, struct path *path,
  2.            struct file *file, const struct open_flags *op,
  3.            int *opened, struct filename *name)
  4. {
  5.    //nd->path.dentry父目录dentry”reboot_info”hash值,搜索reboot_info文件dentry
  6.     lookup_fast(nd, path, &inode);
  7.     //如果open文件时指定O_CREAT,则在这里创建文件
  8.     lookup_open(nd, path, file, op, got_write, opened);
  9.     //这里对file->f_path.dentryfile->f_opf->f_mapping赋值
  10.     file->f_path.mnt = nd->path.mnt;
  11.     error = finish_open(file, nd->path.dentry, NULL, opened);
  12. }

do_last()已添加注释,不再讲解。为了更清晰的讲解目录挂载情况下,文件的查找,请看下方这个图:

    再啰嗦一遍,reboot_info文件的查找:首先获取根文件系统根目录dentry,然后找到其下的home目录dentry,由于sda3 ext4文件系统挂载到了home目录,所以再找到sda3 ext4文件系统的根目录dentry,最后再其下找到reboot_info文件的dentry。

      好了,文件的open过程讲解完了,到这里,我总结了一个文件或者目录open的核心环节就是:以父目录dentry+该文件或者目录名称字符串hash,在系统dentry hash链表查找该文件或者目录dentry,如果存在,则完成查找(该文件或者目录open过)。我的一个重要理解是:open一个文件不需要一定要从根文件系统根目录开始,知道了文件的父目录dentry,是不是也可以完成文件查找。这样的话,根文件系统即便卸载了,或者挂载成只读,也是可以openreadwrite文件的,有什么关系呢?

2 内核文件open的升级版本

     本文的案例,读写/home/reboot_info文件,并且有mount  –t ext4  /dev/sda3  /home,所以reboot_info文件实际是在sda3磁盘分区 ext4文件系统的根目录。按照上一节的描述,如果我提前获取sda3  ext4文件系统的根目录dentry,应该就可以读写reboot_info文件了吧?

      先做个测试,怎么获取sda3 ext4文件系统的根目录dentry呢?先创建该文件,然后这样操作,struct file *  filp= filp_open(“/home/reboot_info”, O_RDWR,0644)filp->f_path->dentry是该文件的dentryfilp->f_path->dentry->d_parent就是其父目录的dentry。因为reboot_info就在sda3磁盘ext4文件系统根目录,所以也是该ext4文件系统根目录的dentry,每一个文件或者目录dentry结构创建时,其dentry->d_parent指向其父目录dentry,另外一个获取根目录dentry方法是filp->f_path-> mnt-> mnt_root,这是直接通过文件所处文件系统的struct vfsmount结构获取。注意,current->fs->root-> mnt-> mnt_rootcurrent->fs->root-> dentry是根文件系统根目录dentry,这些关系有点绕。

        好了,现在已经获取sda3磁盘ext4文件系统根目录dentry,怎样根据这个dentry直接open文件呢?在这点我刚开始陷入了误区,想模仿link_path_walk ()函数实现一套代码,无非把struct nameidata *ndpath等成员,设置成已经获取到的sda3磁盘ext4文件系统根目录dentry,然后把搜索的文件名字字符串设置成”reboot_info”,搜索从当前目录开始,而不是”/home/reboot_info”从根文件系统根目录开始。这样改动太大了,说不好还得改内核,只能作罢。在翻看fs/open.c源码时,发现有个file_open_root()函数,定义如下:

  • struct file *file_open_root(struct dentry *dentry, struct vfsmount *mnt,
  •                 const char *filename, int flags)

分析源码后,这不就是我需要的函数呀,柳暗花明又一村!用法是

  • file_open_root(父目录dentry, 文件所在文件系统的struct vfsmount,”reboot_info”,O_RDWR)

     前两个参数就是刚才filp_open文件获取的filp->f_path->dentry->d_parent, filp->f_path-> mnt。好了。这样测试open reboot_info文件,成功返回了struct file结构,然后vfs_read/vfs_write读写成功。如图是这种方法一个示意图演示,直接获取sda3 ext4文件系统的根目录dentry,然后在该dentry下,搜索reboot_info文件的dentry

     好了初步目的实现了,但是本案例要实现的场景是文件系统umount状态下读写文件!执行umount /home命令后,再执行file_open_root()函数,返回失败,想想也正常,sda3磁盘ext4文件系统都umount了,它mount过程创建块设备bdevsuperblock、根目录dentryinode、文件系统的struct vfsmount等结构,推测都释放掉了。对了,还有这个文件系统所有文件或者目录的dentry hash链表,推测也会被释放清空,毕竟文件系统umount了。刚开心一点就碰到当头一棒!看来,如果此种情况,要想正常操作文件,得在内核中自己实现sda3块设备ext4文件系统的mount流程,这就得对内核mount的流程要熟悉!有点复杂,已经分析到这一步,咬牙坚持一下吧。

3 mout  –t ext4  /dev/sda3  /home的内核源码实现

同样的,这部分内核还是只列出关键部分。mount最后调用的系统调用进入内核空间,源码如下:

  1. fs/namespace.c
  2. SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name,
  3.         char __user *, type, unsigned long, flags, void __user *, data)
  4. {
  5.     int ret;
  6.     char *kernel_type;
  7.     struct filename *kernel_dir;
  8.     char *kernel_dev;
  9.     unsigned long data_page;
  10.     ret = copy_mount_string(type, &kernel_type);
  11.     kernel_dir = getname(dir_name);
  12.     ret = copy_mount_string(dev_name, &kernel_dev);
  13.     ret = copy_mount_options(data, &data_page);
  14.     ret = do_mount(kernel_dev, kernel_dir->name, kernel_type, flags,
  15.             (void *) data_page);
  16.     return ret;
  17. }

前几个都是拷贝字符串,最后是调用do_mount()函数,kernel_dev指向”/dev/sda3” kernel_dir->name指向”/home” kernel_type指向”ext4”字符串,这是几个mount过程的关键参数。

  1. long  do_mount(const char *dev_name, const char *dir_name,
  2.         const char *type_page, unsigned long flags, void *data_page)
  3. {
  4.     struct path path;
  5.     // dir_name指向”/home”,搜索该目录,并把该目录dentry等信息保存到path结构
  6.     retval = kern_path(dir_name, LOOKUP_FOLLOW, &path);
  7.     retval = do_new_mount(&path, type_page, flags, mnt_flags,
  8.                           dev_name, data_page);
  9.     return retval;
  10. }

   kern_path()函数搜索”/home”目录,函数调用流程是kern_path-> do_path_lookup-> filename_lookup-> path_lookupatpath_lookupat 函数与前文介绍的内核open文件的核心函数path_openat,实现流程非常相似,就是找到”/home”这个根文件系统下”home”目录dentryinode、根文件系统的struct  vfsmount等信息,然后保存到struct nameidata *nd结构,再把nd->path保存到kern_path()传递的struct path path结构,下一步mount要用到。这是path_lookupat()函数的核心源码,确实与path_openat()函数相似。

  1. static int  path_lookupat(int dfd, const char *name,
  2.                 unsigned int flags, struct nameidata *nd)
  3. {
  4.     err = path_init(dfd, name, flags | LOOKUP_PARENT, nd, &base);
  5.     err = link_path_walk(name, nd);
  6. }

     又是struct path path,这个结构已经出现多次了,open文件或者目录过程它出现频次很高。再看一下下边它的定义,其实就是保存当前搜索的文件或者目录的dentry等信息。另外,个人觉得文件系统vfsopen文件的实现流程是非常核心的,把它理解透了,vfs层的全面理解就比较容易了。

  1. struct path {
  2.     //搜索的文件或者目录所在文件系统的struct vfsmount
  3.     struct vfsmount *mnt;
  4.     //本次搜索到的目录或者文件的dentry
  5.     struct dentry *dentry;
  6. };

我们回到do_mount函数,继续看下边的do_new_mount()函数。

  1. static int  do_new_mount(struct path *path, const char *fstype, int flags,
  2.             int mnt_flags, const char *name, void *data)
  3. {
  4.     struct file_system_type *type;
  5.     struct vfsmount *mnt;
  6.     //获取sda3 ext4文件系统的struct file_system_type
  7.     type = get_fs_type(fstype);
  8.     // “/dev/sda3” 块设备ext4文件系统的探测
  9.     mnt = vfs_kern_mount(type, flags, name, data);
  10.     //建立 “/home” 目录与 sda3  ext4文件系统的联系
  11.     err = do_add_mount(real_mount(mnt), path, mnt_flags);
  12. }

首先看vfs_kern_mount()函数

  1. struct vfsmount *
  2. vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data)
  3. {
  4.     struct mount *mnt;
  5.     struct dentry *root;
  6.     //分配mnt结构
  7.     mnt = alloc_vfsmnt(name);
  8.     /*
  9.     1找到”/dev/sda3”文件的dentrymntinode结构,并由inode得到sda3块设备的bdev
  10.     2读取sda3磁盘 ext4文件系统中的超级块数据,得到ext4文件系统的root inoderoot dentrysuper_block等结构
  11.     */
  12.     root = mount_fs(type, flags, name, data);
  13.     //sda3 ext4文件系统的root dentry
  14.     mnt->mnt.mnt_root = root;
  15.     mnt->mnt.mnt_sb = root->d_sb;//超级块
  16.     mnt->mnt_mountpoint = mnt->mnt.mnt_root;//
  17.     mnt->mnt_parent = mnt;
  18.     return &mnt->mnt;
  19. }
  20. mount_fs()函数的实现:
  21. struct dentry *
  22. mount_fs(struct file_system_type *type, int flags, const char *name, void *data)
  23. {
  24.     struct dentry *root;
  25.     struct super_block *sb;
  26.    
  27.     //执行ext4_mount(),返回sda3 ext4文件系统的根目录dentry
  28.     root = type->mount(type, flags, name, data);
  29.     //超级块super_block
  30.     sb = root->d_sb;
  31.     return  root;
  32. }

ext4_mount ()函数的实现:

  1. static struct dentry *ext4_mount(struct file_system_type *fs_type, int flags,
  2.                const char *dev_name, void *data)
  3. {
  4.     return mount_bdev(fs_type, flags, dev_name, data, ext4_fill_super);
  5. }
  6. mount_bdev()函数的实现:
  7. struct dentry *mount_bdev(struct file_system_type *fs_type,
  8.     int flags, const char *dev_name, void *data,
  9.     int (*fill_super)(struct super_block *, void *, int))
  10. {
  11.     struct block_device *bdev;
  12.     struct super_block *s;
  13.    
  14.     //查找到”/dev/sda3”路径下”sda3”文件的dentryinode结构,由inode得到sda3struct block_device ,即bdev,最后执行__blkdev_get函数open  sda3这个块设备
  15.     bdev = blkdev_get_by_path(dev_name, mode, fs_type);
  16.     //创建struct  super_block结构并初始化其成员
  17.     s = sget(fs_type, test_bdev_super, set_bdev_super, flags | MS_NOSEC,bdev);
  18.     ………..
  19.     strlcpy(s->s_id, bdevname(bdev, b), sizeof(s->s_id));
  20.     sb_set_blocksize(s, block_size(bdev));
  21.     //执行ext4_fill_super函数,从sda3磁盘读取ext4文件系统超级块等ext4文件系统基本数据,填充struct super_block *s超级块
  22.     error = fill_super(s, data, flags & MS_SILENT ? 1 : 0);
  23.     bdev->bd_super = s;
  24. }

     blkdev_get_by_path()主要是探测”/dev/sda3””sda3”这个块设备,执行mount命令可以看到devtmpfs  on /dev   type  devtmpfs(……),有些linux系统是 tmpfs  on /dev   type  tmpfs(……),原理猜测都是一样的。这说明”/dev”这个目录下挂载了devtmpfs文件系统。”/dev/sda3”sda3这个块设备驱动初始化后生成的。大体过程是,先找到”/dev”这个dev目录dentry,发现dev目录下挂载了devtmpfs文件系统,就找到devtmpfs文件系统的根目录dentry。然后在devtmpfs文件系统根目录,创建sda3这个文件,当然这个文件代表了一个块设备,有主次设备号,通过它可以读写底层sda3磁盘中的数据。”/dev/sda3”设备节点的生成过程,在文章/dev 目录下设备节点生成与访问过程 内核源码详解 有详解,这个知识点也是本案例的关键环节。

  1. struct block_device *blkdev_get_by_path(const char *path, fmode_t mode,
  2.                     void *holder)
  3. {
  4.     struct  block_device *bdev;
  5.     //查找到”/dev/sda3” dentryinode,由inode->i_rdev中保存的sda3主次设备号,创建sda3块设备bdev
  6.     bdev = lookup_bdev(path);
  7.     //打开sda3这个块设备
  8.     err = blkdev_get(bdev, mode, holder);
  9.     return bdev;
  10. }
  11. struct block_device *lookup_bdev(const char *pathname)
  12. {
  13.     struct block_device *bdev;
  14.     struct inode *inode;
  15.     struct path path;
  16.     //open  ”/dev/sda3” 这个文件,得到”sda3”文件的dentry保存到path.dentry
  17.     error = kern_path(pathname, LOOKUP_FOLLOW, &path);
  18.     //”sda3”文件的inode,注意此时”sda3”文件是在devtmpfs文件系统上的
  19.     inode = path.dentry->d_inode;
  20.     // inode->i_rdevsda3块设备的主次设备号,根据这个主次设备号,创建基于sda3块设备的bdev
  21.     bdev = bd_acquire(inode);
  22.     return bdev;
  23. }

     lookup_bdev是创建sda3块设备的关键环节,首先执行kern_path()open “/dev/sda3”这个块设备,kern_path有没有觉得很熟悉?在介绍do_mount函数开头,open “/home”目录刚刚介绍过,这里不再介绍。但是需要了解到,该函数执行后,就得到了”sda3”这个文件的dentry,这个”sda3”文件是在/dev目录创建的,/dev目录挂载这devtmpfs系统,所以该文件属于devtmpfs文件系统。另外一点是,块设备跟字符设备一样,都有自己的主次设备号,作为它的唯一标识,在创建”/dev/sda3”时就包含了主次设备号,ls  -l  /dev/sda3即可看到主次设备号。主次设备号保存在”sda3”文件的inode->i_rdev中,inode通过dentry->d_inode获取,就是lookup_bdev源码inode = path.dentry->d_inode那一行。

好了,继续介绍下方的bd_acquire()函数。

  1. static struct block_device *bd_acquire(struct inode *inode)
  2. {
  3.     struct block_device *bdev;
  4.     //根据传入的块设备的主次设备号,找到或者创建块设备的bdev
  5.     bdev = bdget(inode->i_rdev);
  6.     return bdev;
  7. }
  8. //dev_t dev就是块设备的主次设备号
  9. struct block_device *bdget(dev_t dev)
  10. {
  11.     struct block_device *bdev;
  12.     struct inode *inode;
  13.     //根据dev_t dev块设备号和blockdev_superblockhashinode_hashtable
  14.     //找出该块设备的inode,没有找到就分配新的inode
  15.     inode = iget5_locked(blockdev_superblock, hash(dev),
  16.             bdev_test, bdev_set, &dev);
  17.     //块设备的bdev结构
  18.     bdev = &BDEV_I(inode)->bdev;
  19.     if (inode->i_state & I_NEW) {
  20.         bdev->bd_contains = NULL;
  21.         bdev->bd_super = NULL;
  22.         //跟块设备有关的inode,这个inodebdev组成struct bdev_inode
  23.         bdev->bd_inode = inode;
  24.         bdev->bd_block_size = (1 << inode->i_blkbits);
  25.         bdev->bd_part_count = 0;
  26.         bdev->bd_invalidated = 0;
  27.         inode->i_mode = S_IFBLK;//i_mode代表块设备
  28.         inode->i_rdev = dev;
  29.         inode->i_bdev = bdev;
  30.         inode->i_data.a_ops = &def_blk_aops;//块设备文件操作结构体
  31.         mapping_set_gfp_mask(&inode->i_data, GFP_USER);
  32.         inode->i_data.backing_dev_info = &default_backing_dev_info;
  33.         spin_lock(&bdev_lock);
  34.         //bdev添加到all_bdevs全局链表
  35.         list_add(&bdev->bd_list, &all_bdevs);
  36.         spin_unlock(&bdev_lock);
  37.     }
  38.     return bdev;
  39. }

bdev结构这里有必要介绍一下,如下:

  1. struct block_device {
  2.     //包含块设备的主次设备号
  3.     dev_t           bd_dev;
  4.     //块设备主分区mmcblk0和其他分区mmcblk0p4各有一个inode,不是同一个,bd_inode就指向块设备的inodebdget函数赋值
  5.     struct inode *      bd_inode;
  6.     //bdev文件系统的超级快,mount_bdev函数赋值
  7.     struct super_block *    bd_super;
  8.     //块设备分区的bdev->bd_contains指向块设备主分区的bdev,主分区的bdevbdev->bd_contains指向主分区bdev
  9.     struct block_device *   bd_contains;
  10.     //bd_set_size()函数中赋值,块设备大小
  11.     unsigned        bd_block_size;
  12.     //主分区register_disk()中设置1,之后才会扫描块设备分区
  13.     int         bd_invalidated;
  14.     //__blkdev_get()函数对块设备的bd_part赋值
  15.     struct hd_struct *  bd_part;
  16.     //__blkdev_get()赋值为块设备的gendisk,块设备只有一个gendisk
  17.     struct gendisk *    bd_disk;
  18. }

      块设备对应的block_device 结构,在 bdget()->iget5_locked()分配。块设备主分区sda bdev生成流程 add_disk()->register_disk()->bdget_disk()->bdget()。块设备分区(sda3) block_device生成流程 vfs_kern_mount()->mount_fs()->cramfs_mount()->mount_bdev()->blkdev_get_by_path()->lookup_bdev()->bd_acquite()->bdget()。每个块设备分区都有一个block_device结构,主块设备分区也有一个,块设备主分区和块设备分区只有gendisk结构。

  iget5_locked()函数主要功能就是根据sda3的主次设备号和块设备的超级块blockdev_superblock,找到或者创建块设备sda3inode。注意,这是块设备基于bdev文件系统的inode,每个块设备有一个,代表的是底层块设备。与前文说的”/dev/sda3””sda3”文件的inode不一样,前者基于块设备文件系统,后者基于devtmpfs文件系统。继续bdget()函数,然后由inode得到bdev,再对bdevinode初始化。

    好了,现在bd_acquire()-> bdget() 执行完成,返回到lookup_bdev函数。可以发现,lookup_bdev()主要两个功能,一个是完成”/dev/sda3”的探测,然后创建sda3块设备bdevinode。从lookup_bdev() 返回到blkdev_get_by_path(),接着会执行blkdev_get(),该函数完成最后会执行__blkdev_get(),得到块设备对应的struct gendisk *disk结构,对bdev的成员bd_diskbd_queuebd_containsbd_partbd_block_size等赋值,执行diskblock open函数,完成块设备的open。如果块设备代表整个块设备主分区,还会执执行rescan_partitions扫描块设备的各个分区,比如,sda代表的块设备主分区,其中的sda1sda2sda3分区就是在这里完成rescan

      现在再由blkdev_get_by_path()函数返回mount_bdev()函数,接着执行fill_super()也就是ex4_ fill_super()函数,该函数主要完成从sda3磁盘读取ext4文件系统超级块等ext4文件系统基本数据,填充struct super_block超级块结构,还得到该文件系统的根目录dentryinode,这点非常关键,有了这个根目录dentry,就可以访问其下的文件或者目录了

    饶了一大圈,现在沿着vfs_kern_mount->mount_fs->ext4_mount->mount_bdev返回到vfs_kern_mount函数。该函数再总结一下作用:得到sda3 对应块的设备bdev,然后得到对应的sda3 ext4文件系统的根目录dentryinodesuper_block,然后对该文件系统的struct mount *mntmnt->mnt(struct vfsmount)赋值,并返回mnt->mnt(struct  vfsmount)。如下图简单显示了sda3 ext4文件系统与根文件系统建立联系,挂载到根文件系统/home目录。

从vfs_kern_mount()函数返回到do_new_mount(),接着执行do_add_mount()

  1. // struct mount *newmnt 就是sda3 ext4文件系统的,struct path *path是挂载点”/home”目录的
  2. static int do_add_mount(struct mount *newmnt, struct path *path, int mnt_flags)
  3. {
  4.     struct mountpoint *mp;
  5.     struct mount *parent;
  6.    //基于挂载点”/home”目录得到struct mountpoint *mp,其中会把该目录dentry这样操作dentry->d_flags |= DCACHE_MOUNTED,表示home目录挂载了其他的文件系统。
  7.    mp = lock_mount(path);
  8.    //得到挂载点”/home”目录所在的文件系统的struct mount,即根文件系统的struct mount
  9.     parent = real_mount(path->mnt);
  10.     newmnt->mnt.mnt_flags = mnt_flags;
  11.     // newmnt代表sda3 磁盘ext4文件系统,parent代表根文件系统
  12.     err = graft_tree(newmnt, parent, mp);
  13. }

      graft_tree完成最后的挂载工作,由于涉及到文件系统mount 多对一的情况,是它的大体原理是,基于newmntparentmp,建立复杂的链表相互联系,由其中一个就能找到另外一个。newmntparent分别是sda3磁盘ext4文件系统的struct mount结构、根文件系统的struct mount结构,每个文件系统挂载时都有一个struct mount结构,其成员有struct  vfsmount  mntstruct vfsmount的成员struct dentry *mnt_root就是该文件系统的根目录dentrygraft_tree()将两个struct mount结构建立链表关系。想进一步了解文件系统mount过程可以查看 linux内核mount过程超复杂的do_mount()、do_loopback()、attach_recursive_mnt()、propagate_mnt()函数详解-CSDN博客

        文件系统mount成功后,等将来查找到”/home”目录时,找到”home”目录的dentry,发现其dentry->d_flags DCACHE_MOUNTED属性,此时知道根文件系统的struct mount,由于早期mount时建立了sda3 磁盘ext4文件系统的struct mount与根文件系统的struct mount的关系,自然找到sda3 ext4文件系统的struct mount,继而找到该ext4文件系统的根目录dentry

        好了,漫漫长途,终于把/dev/sda3 挂载到/home目录讲完了,说实话还是有点绕的,需要多看几次,多体会体会,理解mount的本质后,就容易理解了。

4文件系统umount状态读写文件的实现

4.1 内核中调用vfs_kern_mount()函数实现mount

       请回到第二节最后看一下,知道了mount的流程,我们之前的疑问是:怎么在reboot最后,进入内核时,sda3 ext4文件系统已经从/home目录umount的情况下,实现一个mount流程?为什么要内核里要再实现一次sda3磁盘ext4文件系统的mount流程呢?因为sda3  ext4文件系统已经umount了,而只有再mount一次,才会创建sda3块设备bdev,才会创建该文件系统的super_block、根目录dentryinode,有这些才能真正的在这个文件系统中读写文件!而这些过程经过验证,正好都是在内核mount过程的vfs_kern_mount()函数中实现。具体该怎么调用vfs_kern_mount()呢?vfs_kern_mount()mount一节有重点介绍,可以先回过头看看,它的定义如下:

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

用法是

  • struct vfsmount * sda3_vfsmount =

*vfs_kern_mount(get_fs_type(“ext4”) , MS_KERNMOUNT,/dev/sda3”,NULL)

     正常执行后,返回sda3 ext4文件系统的struct  vfsmount,其成员struct dentry *mnt_root就是该ext4文件系统root  dentry,之后调用file_open_root(sda3_vfsmount->mnt_root,sda3_vfsmount,”reboot_info”,O_RDWR)就可以实现reboot_info文件的open,然后使用该函数返回的文件struct *file,就可以调用vfs_read/vfs_write读写文件了。

    可惜只是理想很丰满,一厢情愿而已。因为有个关键点忽略了,

     vfs_kern_mount(get_fs_type(“ext4”) , MS_KERNMOUNT,”/dev/sda3”,NULL)会失败返回,因为vfs_kern_mount()函数中会首先open ”/dev/sda3”文件,函数路径是vfs_kern_mount->mount_fs->ext4_mount->mount_bdev->blkdev_get_by_path->lookup_bdev->kern_path,再看下第3节内容,而devtmpfs文件系统mount”/dev”目录,但是此时是reboot命令最后,根文件系统remount成只读,其他文件系统全部umount了,包括devtmpfs。那怪不得open ”/dev/sda3”文件会失败了!

4.2 文件系统umont状态执行vfs_kern_mount()函数

        这个难题该怎么破解?有个关键点,”/dev/sda3”open过程,因为第一个字符是’/’,这表示从根文件系统根目录开始搜索文件,而此时根文件系统已经remount成只读了,不再是正常的根文件系统。先看下第一节内核open文件执行的path_init函数,如果我们从根目录开始搜索文件,会执行nd->root = current->fs->rootcurrent->fs->root->dentry就是根文件系统的根目录dentry,之后就是在这个dentry下开始搜索”dev”目录的dentryopen的核心就是根据父目录dentry搜索子文件或者目录dentry。我们在内核里是可以修改current->fs->root->dentry为我们要搜索的文件的父目录dentry的,什么意思呢

      我们知道因为devtmpfs文件系统挂载到了/dev目录,”/dev/sda3”中的”sda3”文件实际是在devtmpfs文件系统的根目录目录下,先struct file *  filp= filp_open(”/dev/sda3”, O_RDWR,0644)filp->f_path->dentry”dev”文件的dentryfilp->f_path->dentry->d_parent是其父目录的dentry,就是devtmpfs文件系统的根目录dentry,这点第2节有讲解。filp->f_path.mnt是该文件系统的struct vfsmount结构。我们把filp->f_path.mntfilp->f_path->dentry->d_parent保存到全局变量里,struct vfsmount  devtmpfs_root_vfsmnt = filp->f_path.mntstruct dentry* devtmpfs_root_dentry=filp->f_path->dentry->d_parent

       等reboot重启最后进入内核空间,执行到我们注册的reboot消息通知函数,先 current->fs->root->dentry = devtmpfs_root_dentry修改默认的根目录dentry为原devtmpfs文件系统的根目录dentry,修改current->fs->root.mnt= &devtmpfs_root_vfsmnt 为原来devtmpfs文件系统的struct  vfsmount结构。这样设置完成后,相当于强制修改了系统的根文件系统目录。接着样执行vfs_kern_mount(get_fs_type(“ext4”) , MS_KERNMOUNT,”/sda3”,NULL)函数,注意把第三个函数参数由”/dev/sda3”改为”/sda3”,这样在open块设备”/sda3”vfs_kern_mount->mount_fs->ext4_mount->mount_bdev->blkdev_get_by_path->lookup_bdev->kern_path-> do_path_lookup-> filename_lookup->path_lookupat-> path_init,在path_init函数中会执行到nd->root = current->fs->root,因为current->fs->root已经被修改为原devtmpfs文件系统的数据,尤其nd->path.dentry =current->fs->root.dentry是原devtmpfs文件系统的根目录dentry。好了,接下来走文件搜索流程,在path_lookupat-> link_path_walk()函数中,nd->path.dentry作为父目录dentry,然后在dentry  hash链表中搜索是否有名称为”sda3”的文件dentry,找到之后貌似就可以成功返回了?这样可以成功吗?当然不是,这里边还有几个关键问题没有考虑!

       能确保此时”sda3”dentry存在于dentry hash链表吗?因为此时devtmpfs文件系统已经umount了,推测肯定是不存在了!还有一点,我们现在做的无非是构建一个”/dev/sda3”块设备已经open过的场景,然后令执行vfs_kern_mount()函数成功返回,该怎么完善的构建这个场景呢?

4.3 如何手动构建文件open过程及源码实现

前一节已经在文件系统umount状态构建了 “/dev/sda3”文件的文件系统(devtmpfs),但是这还不够,一个文件和父目录都有dentryinode结构,这些结构彼此都有联系,在第一次open “/dev/sda3”时会自动建立。这个open过程是:

path_openat->link_path_walk->walk_component->lookup_slow->__lookup_hash->lookup_dcache,接着这样执行d_alloc函数为本次open的文件“/dev/sda3”分配struct dentry结构:

  1. struct dentry *d_alloc(struct dentry * parent, const struct qstr *name)
  2. {
  3.     struct dentry *dentry = __d_alloc(parent->d_sb, name);//分配dentry结构
  4.     dentry->d_parent = parent;// dentry->d_parent指向父目录dentry
  5.     list_add(&dentry->d_child, &parent->d_subdirs);
  6. }
  7. //分配dentry并初始化dentry成员
  8. struct dentry *__d_alloc(struct super_block *sb, const struct qstr *name)
  9. {
  10.     struct dentry *dentry;
  11.     dentry = kmem_cache_alloc(dentry_cache, GFP_KERNEL);
  12.     dentry->d_name.len = name->len;
  13.     dentry->d_name.hash = name->hash;
  14.     memcpy(dname, name->name, name->len);
  15.     dentry->d_parent = dentry;
  16.     dentry->d_sb = sb;
  17.     INIT_LIST_HEAD(&dentry->d_lru);
  18.     INIT_LIST_HEAD(&dentry->d_subdirs);
  19.     INIT_HLIST_NODE(&dentry->d_u.d_alias);
  20.     INIT_LIST_HEAD(&dentry->d_child);
  21.     d_set_d_op(dentry, dentry->d_sb->s_d_op);
  22. }

     再回到__lookup_hash()函数,执行lookup_real创建该“/dev/sda3”文件的inode,注意这里需要调用devtmpfs文件系统的lookup函数,因为devtmpfs文件系统挂载到了/dev/目录。在创建完inode后,会执行d_instantiate()建立该文件dentryinode的联系, d_instantiate()函数源码如下:

  1. //建立文件dentryinode联系
  2. void  d_instantiate(struct dentry *entry, struct inode * inode)
  3. {
  4.     //如果dentry已经添加过链表中,hlist_unhashed返回假,出发BUG_ON
  5.     BUG_ON(!hlist_unhashed(&entry->d_u.d_alias));
  6.     if (inode)
  7.         spin_lock(&inode->i_lock);
  8.     // dentry->d_u.d_alias添加到inode->i_dentry链表,然后dentry->d_inode = inode,建立该dentryinode与父目录dentryinode的联系。
  9.     __d_instantiate(entry, inode);
  10.     if (inode)
  11.         spin_unlock(&inode->i_lock);
  12.     security_d_instantiate(entry, inode);
  13. }

     这些代码列出了第一次open一个文件时,创建该文件的dentryinode的过程,还有建立他们彼此的联系。我们就是要手动实现”/dev/sda3”文件open过程:创建”sda3”文件的dentryinode,建立二者的联系,还要建立他们与父目录dentry的联系。现在把我写的这个模块ko的源码较为完整的列出来,省去出错判断代码。

  1. struct dentry  devtmpfs_root_dentrysda3_dentry;
  2. struct inode  devtmpfs_root_inodesda3_inode;
  3. struct vfsmount  devtmpfs_vfsmnt;
  4. struct super_block  devtmpfs_sb;
  5. //ko模块init函数中
  6. int  capture_reboot_init()
  7. {
  8.     struct file *filp = open(/dev/sda3”,O_RDWR,0644);
  9.     //获取”/dev/sda3”文件的dentryinode
  10.      memcpy(&devtmpfs_root_dentry, filp->f_path->dentry->d_parent,sizeof(struct dentry));
  11.     memcpy(&devtmpfs_root_inode, devtmpfs_root_dentry.d_inode,sizeof(struct inode ));
  12.     //获取/dev所属devtmpfs文件系统的根dentryinode、超级块
  13.     memcpy(&sda3_dentry, filp->f_path->dentry,sizeof(struct dentry));
  14.     memcpy(&sda3_inode, sda3_dentry.d_inode,sizeof(struct inode ));
  15.     memcpy(&devtmpfs_sb, devtmpfs_root_inode.i_sb,sizeof(struct super_block));
  16.     //建立”sda3”文件dentryinodedevtmpfs文件系统根目录dentryinode,超级块关系
  17.     devtmpfs_root_dentry.d_inode= &devtmpfs_root_inode;
  18.     sda3_dentry.d_inode= &sda3_inode;
  19.     devtmpfs_vfsmnt = filp->f_path.mnt;
  20.     sda3_inode.i_sb=&devtmpfs_sb;
  21.     devtmpfs_root_inode.i_sb= devtmpfs_sb;
  22.     sda3_dentry.d_parent=& devtmpfs_root_dentry;
  23.     //”sda3”文件的inodei_bdev必须设置为NULL,这样才会创探测新的sda3 bdev块设备
  24.     sda3_inode.i_bdev = NULL;
  25.     //建立”sda3”文件dentryinode的联系,并把dentry添加到dentry hash链表
  26.     d_add(&sda3_dentry,& sda3_inode);
  27.     //初始化”sda3”文件dentry关键成员,尤其是sda3_dentry .d_u.d_alias
  28.     INIT_LIST_HEAD(&sda3_dentry .d_lru);
  29.     INIT_LIST_HEAD(&sda3_dentry .d_subdirs);
  30.     INIT_HLIST_NODE(&sda3_dentry .d_u.d_alias);
  31.     INIT_LIST_HEAD(&sda3_dentry .d_child);
  32.     filp_close(filp);
  33. }

ko模块在注册的reboot消息通知函数reboot_notify_function中,在系统发起reboot重启命令最后,进入内核空间执行

  1. int  reboot_notify_function(void)
  2. {
  3.     struct vfsmount  *vfsmnt;
  4.     struct file *filp;
  5.     loff_t pos;
  6.     char buf[100];
  7.     struct  *sda3_root_dentry;
  8.     //修改默认的根目录dentry为原devtmpfs文件系统的根目录dentry
  9.     current->fs->root->dentry = devtmpfs_root_dentry;
  10.     //修改为原devtmpfs文件系统的struct  vfsmount结构
  11.     current->fs->root.mnt= &devtmpfs_root_vfsmnt;
  12.     //模拟sda3块设备的mount过程,先模拟探测”/dev/sda3”文件,再创建sda3块设备bdev,探测该块设备中ext4文件系统,获取根目录dentryinodevfsmount,超级块等
  13.     vfsmnt = vfs_kern_mount(get_fs_type(“ext4”) , MS_KERNMOUNT,/sda3”,NULL);
  14.     sda3_root_dentry = vfsmnt->mnt_root;
  15.     //sda3_root_dentry为根目录dentryopen该目录下的reboot_info文件
  16.     filp= file_open_root(sda3_root_dentry, vfsmnt,”reboot_info”,O_RDWR);
  17.     //将重启前进程的名字和pid保存到reboot_info文件。
  18.     snprintf(buf,100,%%d\n”,current->comm,current->pid);
  19.     pos = 0;
  20.     vfs_write(filp, buf,strlen(buf)+1,&pos);
  21.     filp_close(filp);
  22. }

     以上是该模块完整的核心源码,数下来也就几十行,实现起来不算复杂。capture_reboot_init()提前获取”/dev/sda3”文件的dentryinode信息,以及该文件所在的devtmpfs文件系统的根目录dentryinodevfsmountsuperblock信息,然后把这些信息保存到全局变量中。同时伪造一个devtmpfs文件系统,把各个成员错综复杂的关系建立起来,重点是建立devtmpfs文件系统根目录dentryinode”/dev/sda3”文件dentryinode的联系,”/dev/sda3”文件的dentry添加到dentry hash链表,使将来执行vfs_kern_mount() 成功open  ”/dev/sda3”文件。

重点源码标注了黄色,有几点还是要重点强调一下。

1 sda3_inode.i_bdev = NULL 必须要设置,将来执行vfs_kern_mount->mount_fs->ext4_mount->mount_bdev->blkdev_get_by_path-> lookup_bdev-> bd_acquire函数,探测sda3块设备时,sda3_inode.i_bdev 如果不为NULL,就不会重新探测sda3块设备,具体可以看下前文讲解mount的内核实现。

  1. static struct block_device *bd_acquire(struct inode *inode)
  2. {
  3.     struct block_device *bdev;
  4.     bdev = inode->i_bdev;
  5.     //如果inode->i_bdev不为NULL,直接return
  6.     if (bdev) {
  7.         ihold(bdev->bd_inode);
  8.         spin_unlock(&bdev_lock);
  9.         return bdev;
  10.     }
  11.     //根据传入的主次设备号,找到或者创建块设备的bdev
  12.     bdev = bdget(inode->i_rdev);
  13. }

2  d_add(&sda3_dentry,& sda3_inode) 关键是将sda3_dentry添加到dentry hash链表,将来vfs_kern_mount->mount_fs->ext4_mount->mount_bdev->blkdev_get_by_path->lookup_bdev->kern_path->do_path_lookup->filename_lookup->path_lookupat->link_path_walk-> walk_component-> lookup_fastopen “/dev/sda3”时,直接执行lookup_fast()就可以找到sda3文件的dentry,不用再走lookup_slow()流程。

3  INIT_HLIST_NODE(&sda3_dentry .d_u.d_alias); 内核是通过sda3_dentry.d_u.d_alias把该dentry添加到其inode->i_dentry的链表。这里初始化sda3_dentry.d_u.d_alias NULL,,否则下来执行d_add(&sda3_dentry,& sda3_inode) 时会报内核bug,因为该函数检测到sda3_dentry.d_u.d_alias不为NULL,认为该dentry已经添加过inode->i_dentry链表,不能添加第二次。

4  vfsmnt = vfs_kern_mount(get_fs_type(“ext4”) , MS_KERNMOUNT,”/sda3”,NULL); 有两个重点工作,open “sda3”这个块设备文件节点,获取其dentryinode,这个inode就是sda3_inode,接着使用这个inode->i_bdev创建sda3块设备bdev,然后探测sda3对应的磁盘ext4文件系统,获取ext4文件系统根目录dentryinode,还有super_blockvfsmount

5    filp= file_open_root(sda3_root_dentry, vfsmnt,”reboot_info”,O_RDWR); sda3 ext4文件系统根目录dentry为父目录dentry,然后在该目录下open文件”reboot_info”

好了,这里终于相关技术难点讲解完毕。涉及的东西繁多,可能思路有点绕。文章有什么错误,请大佬多指点。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值