近期发现不少机器异常重启(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结构的关键成员如下:
- struct dentry {
- //如果设置了DCACHE_MOUNTED 标志位,表示该dentry是一个挂载点
- unsigned int d_flags;
- //每个dentry结构都通过d_hash链入hash表dentry_hashtable的某个队列
- struct hlist_bl_node d_hash; /* lookup hash list */
- //指向父目录dentry
- struct dentry *d_parent;
- //该目录或者文件的hash值,有时每个dentry的d_name->name是"/",这是因为该dentry是个挂载点
- struct qstr d_name;
- //就是对应的inode结构,一个dentry只对应一个inode
- struct inode *d_inode;
- //目录或者文件名字
- unsigned char d_iname[DNAME_INLINE_LEN];
- //子目录或者子文件的dentry通过d_child链入父目录dentry的d_subdirs
- struct list_head d_child;
- struct list_head d_subdirs;
- };
现在开始正式讲解文件的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函数核心源码如下:
- static struct file *path_openat(int dfd, struct filename *pathname,
- struct nameidata *nd, const struct open_flags *op, int flags)
- {
- struct file *file;
- struct path path;
- file = get_empty_filp();
- // pathname->name 是文件路径,即”/home/reboot_info”
- path_init(dfd, pathname->name, flags | LOOKUP_PARENT, nd, &base);
- link_path_walk(pathname->name, nd);
- do_last(nd, &path, file, op, &opened, pathname);
- }
get_empty_filp()得到一个空的struct file结构,每次open文件对应一个。path_init()主要是初始化struct nameidata *nd,这个nd贯穿open过程,它保存了每次搜索的文件或者目录的参数,核心成员如下:
- struct nameidata {
- struct path path;
- struct qstr last;
- struct path root;
- struct inode *inode; /* path.dentry.d_inode */
- }
该结构的主要成员定义如下:
- //包含本次搜索的文件或者目录名称、hash值、名字字符串长度
- struct qstr {
- union {
- struct {
- HASH_LEN_DECLARE;
- };
- u64 hash_len;
- };
- const unsigned char *name;
- };
- struct path {
- //当前文件系统的vfsmount结构,
- struct vfsmount *mnt;
- //本次搜索到的目录或者文件的dentry
- struct dentry *dentry;
- };
- struct vfsmount {
- //文件系统的根目录dentry
- struct dentry *mnt_root;
- //文件系统super_block结构
- struct super_block *mnt_sb;
- int mnt_flags;
- };
struct nameidata *nd结构各个成员的设置见path_init()函数,下来就介绍path_openat()函数中执行的第一个重点函数--- path_init()。
- static int path_init(int dfd, const char *name, unsigned int flags,
- struct nameidata *nd, struct file **fp)
- {
- //open文件的路径”/home/reboot_info”第一个字符是’/’,表示从根文件系统根目录开始搜索
- if (*name=='/')
- {
- //该函数核心操作是nd->root = current->fs->root,设置为当前根文件系统的struct path *root
- set_root(nd);
- //nd->path保存nd->root的值
- nd->path = nd->root;
- }
- //设置nd->inode为根文件系统根目录的inode结构
- nd->inode = nd->path.dentry->d_inode;
- }
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()函数,这是搜索文件的核心。
- static int link_path_walk(const char *name, struct nameidata *nd)
- {
- //name初始指向”/home/reboot_info”第一个字符’/’,循环结束指向字符’h’
- while (*name=='/')
- name++;
- for(;;) {
- struct qstr this;
- //第一次循环是计算”home”字符串的hash值存于this.hash,返回字符串长度
- len = hash_name(name, &this.hash);
- //指向”/home/reboot_info”的”home”,表示本次搜索目录或者文件名称是”home”
- this.name = name;
- // len本次搜索的文件或者目录名称”home”字符串长度
- this.len = len;
- type = LAST_NORM;
- if (likely(type == LAST_NORM)) {
- struct dentry *parent = nd->path.dentry;
- }
- //nd->last就是包含当前搜索的文件或者目录名字字符串的hash值结构体
- nd->last = this;
- nd->last_type = type;
- if (!name[len])//本案例第2次循环到这里就会执行return 0返回
- return 0;
- do {
- len++;
- } while (unlikely(name[len] == '/'));
- if (!name[len])
- return 0;
- //name指向”/home/reboot_info”中的”reboot_info”
- name += len;
- //搜索文件的核心函数
- walk_component(nd, &next, LOOKUP_FOLLOW);
- }
- }
link_path_walk()函数是一个循环,依次搜索每个文件或者目录的dentry,具体实现是:先令name指针指向待本次搜索的文件或者目录名字字符串,比如第一次循环name指向”/home/reboot_info”的”home”字符串(第二次循环则指向”reboot_info”字符串),接着向struct qstr this结构填充本次搜索的目录”home”的hash值、字符串长度,然后nd->last = this,这是下一步能匹配搜索”home”目录的关键。下一步执行walk_component()函数开始真正的搜索。
- static inline int walk_component(struct nameidata *nd, struct path *path,
- int follow)
- {
- //在系统dentry hash链表中搜索查找本次文件或者目录的dentry
- err = lookup_fast(nd, path, &inode);
- if (unlikely(err)) {
- //找不到就调用ext4文件系统的lookup函数查找
- err = lookup_slow(nd, path);
- inode = path->dentry->d_inode;
- }
- //核心是两个赋值,nd->path.mnt = path->mnt 和 nd->path.dentry = path->dentry
- path_to_nameidata(path, nd);
- nd->inode = inode;
- }
第一次执行walk_component()函数,是搜索”home”目录的dentry是否存在,也可以说搜索根文件系统根目录下是否存在home目录? 接着看lookup_fast()函数。
- static int lookup_fast(struct nameidata *nd,
- struct path *path, struct inode **inode)
- {
- struct vfsmount *mnt = nd->path.mnt;
- //parent指向父目录dentry ,第一次搜索home目录时,nd->path.dentry 在path_init()函数中赋值,是根文件系统根目录dentry。
- struct dentry *dentry, *parent = nd->path.dentry;
- //在dentry hash链表查找”home”目录dentry,基于nd->last(包含home目录名字的hash值)和父目录dentry结构,即parent
- dentry = __d_lookup_rcu(parent, &nd->last, &seq, nd->inode);
- if (!dentry)
- goto unlazy;
- path->mnt = mnt;
- //dentry此时是home目录的dentry
- path->dentry = dentry;
- //如果dentry是挂载点,要把path->mnt和 path->dentry切换为挂载源头文件系统的
- if (unlikely(!__follow_mount_rcu(nd, path, inode)))
- goto unlazy;
- return 0;
- }
第一次循环执行lookup_fast()是在根文件系统根目录下查找是否有home目录,parent指向根目录dentry,nd->last包含查找home目录的hash值,__d_lookup_rcu(parent, &nd->last, &seq, nd->inode)结合parent和nd->last在系统dentry hash链表查找home目录的dentry。如果之前open过home目录,就会把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目录的inode和dentry,然后把home目录dentry添加到系统dentry hash链表,为了分析方便,这里假设home目录之前open过,其dentry结构已经存在于dentry hash链表。
接着__d_lookup_rcu函数向下执行,__follow_mount_rcu()函数很关键,它是判断本次查找到的path->dentry 这个dentry是否有挂载属性,这是什么意思?前文说过,系统启动时执行了mount –t ext4 /dev/sda3 /home,sda3磁盘分区的ext4文件系统挂载到了home目录,则home目录就是一个挂载目录,有挂载属性,关于mount的过程,稍后也会详细讲解。
- static bool __follow_mount_rcu(struct nameidata *nd, struct path *path,
- struct inode **inode)
- {
- for (;;) {
- struct mount *mounted;
- //判断dentry->d_flags & DCACHE_MOUNTED,该目录是否有挂载属性,是为真
- if (!d_mountpoint(path->dentry))
- break;
- //如果path->dentry是挂载点,则找到挂载源头的sda3 ext4文件系统的信息结构
- mounted = __lookup_mnt(path->mnt, path->dentry, 1);
- if (!mounted)
- break;
- // mounted->mnt 和 mounted->mnt.mnt_root 是sda3 ext4文件系统的struct vfsmount结构和根目录dentry
- path->mnt = &mounted->mnt;
- //查找到的dentry保存到path->dentry,注意,这里的dentry已经切换为sda3 ext4文件系统的dentry结构,不再是根文件系统下home目录的dentry。
- path->dentry = mounted->mnt.mnt_root;
- //sda3 ext4文件系统的根目录inode
- *inode = path->dentry->d_inode;
- }
- return true;
- }
__follow_mount_rcu()函数就是判断path->dentry是否是挂载目录,如果是则找到挂载源头的那个sda3 ext4文件系统的根dentry。
好了,经历了千辛万苦,终于从lookup_fast()函数返回到walk_component()函数,此时,home目录dentry已经找到了,保存在path->dentry。如果home目录是挂载点,则path->dentry是挂载源头sda3 ext4文件系统的根目录dentry,path->mnt是sda3 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函数查找文件也是同样的道理。
- static int do_last(struct nameidata *nd, struct path *path,
- struct file *file, const struct open_flags *op,
- int *opened, struct filename *name)
- {
- //以nd->path.dentry父目录dentry和”reboot_info”hash值,搜索reboot_info文件dentry
- lookup_fast(nd, path, &inode);
- //如果open文件时指定O_CREAT,则在这里创建文件
- lookup_open(nd, path, file, op, got_write, opened);
- //这里对file->f_path.dentry、file->f_op、f->f_mapping赋值
- file->f_path.mnt = nd->path.mnt;
- error = finish_open(file, nd->path.dentry, NULL, opened);
- }
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,是不是也可以完成文件查找。这样的话,根文件系统即便卸载了,或者挂载成只读,也是可以open、read、write文件的,有什么关系呢?
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是该文件的dentry,filp->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_root和current->fs->root-> dentry是根文件系统根目录dentry,这些关系有点绕。
好了,现在已经获取sda3磁盘ext4文件系统根目录dentry,怎样根据这个dentry直接open文件呢?在这点我刚开始陷入了误区,想模仿link_path_walk ()函数实现一套代码,无非把struct nameidata *nd的path等成员,设置成已经获取到的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过程创建块设备bdev、superblock、根目录dentry和inode、文件系统的struct vfsmount等结构,推测都释放掉了。对了,还有这个文件系统所有文件或者目录的dentry hash链表,推测也会被释放清空,毕竟文件系统umount了。刚开心一点就碰到当头一棒!看来,如果此种情况,要想正常操作文件,得在内核中自己实现sda3块设备ext4文件系统的mount流程,这就得对内核mount的流程要熟悉!有点复杂,已经分析到这一步,咬牙坚持一下吧。
3 mout –t ext4 /dev/sda3 /home的内核源码实现
同样的,这部分内核还是只列出关键部分。mount最后调用的系统调用进入内核空间,源码如下:
- fs/namespace.c
- SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name,
- char __user *, type, unsigned long, flags, void __user *, data)
- {
- int ret;
- char *kernel_type;
- struct filename *kernel_dir;
- char *kernel_dev;
- unsigned long data_page;
- ret = copy_mount_string(type, &kernel_type);
- kernel_dir = getname(dir_name);
- ret = copy_mount_string(dev_name, &kernel_dev);
- ret = copy_mount_options(data, &data_page);
- ret = do_mount(kernel_dev, kernel_dir->name, kernel_type, flags,
- (void *) data_page);
- return ret;
- }
前几个都是拷贝字符串,最后是调用do_mount()函数,kernel_dev指向”/dev/sda3”, kernel_dir->name指向”/home”, kernel_type指向”ext4”字符串,这是几个mount过程的关键参数。
- long do_mount(const char *dev_name, const char *dir_name,
- const char *type_page, unsigned long flags, void *data_page)
- {
- struct path path;
- // dir_name指向”/home”,搜索该目录,并把该目录dentry等信息保存到path结构
- retval = kern_path(dir_name, LOOKUP_FOLLOW, &path);
- retval = do_new_mount(&path, type_page, flags, mnt_flags,
- dev_name, data_page);
- return retval;
- }
kern_path()函数搜索”/home”目录,函数调用流程是kern_path-> do_path_lookup-> filename_lookup-> path_lookupat,path_lookupat 函数与前文介绍的内核open文件的核心函数path_openat,实现流程非常相似,就是找到”/home”这个根文件系统下”home”目录dentry、inode、根文件系统的struct vfsmount等信息,然后保存到struct nameidata *nd结构,再把nd->path保存到kern_path()传递的struct path path结构,下一步mount要用到。这是path_lookupat()函数的核心源码,确实与path_openat()函数相似。
- static int path_lookupat(int dfd, const char *name,
- unsigned int flags, struct nameidata *nd)
- {
- err = path_init(dfd, name, flags | LOOKUP_PARENT, nd, &base);
- err = link_path_walk(name, nd);
- }
又是struct path path,这个结构已经出现多次了,open文件或者目录过程它出现频次很高。再看一下下边它的定义,其实就是保存当前搜索的文件或者目录的dentry等信息。另外,个人觉得文件系统vfs层open文件的实现流程是非常核心的,把它理解透了,vfs层的全面理解就比较容易了。
- struct path {
- //搜索的文件或者目录所在文件系统的struct vfsmount
- struct vfsmount *mnt;
- //本次搜索到的目录或者文件的dentry
- struct dentry *dentry;
- };
我们回到do_mount函数,继续看下边的do_new_mount()函数。
- static int do_new_mount(struct path *path, const char *fstype, int flags,
- int mnt_flags, const char *name, void *data)
- {
- struct file_system_type *type;
- struct vfsmount *mnt;
- //获取sda3 ext4文件系统的struct file_system_type
- type = get_fs_type(fstype);
- // “/dev/sda3” 块设备ext4文件系统的探测
- mnt = vfs_kern_mount(type, flags, name, data);
- //建立 “/home” 目录与 sda3 ext4文件系统的联系
- err = do_add_mount(real_mount(mnt), path, mnt_flags);
- }
首先看vfs_kern_mount()函数
- struct vfsmount *
- vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data)
- {
- struct mount *mnt;
- struct dentry *root;
- //分配mnt结构
- mnt = alloc_vfsmnt(name);
- /*
- 1找到”/dev/sda3”文件的dentry、mnt、inode结构,并由inode得到sda3块设备的bdev
- 2读取sda3磁盘 ext4文件系统中的超级块数据,得到ext4文件系统的root inode,root dentry,super_block等结构
- */
- root = mount_fs(type, flags, name, data);
- //sda3 的ext4文件系统的root dentry
- mnt->mnt.mnt_root = root;
- mnt->mnt.mnt_sb = root->d_sb;//超级块
- mnt->mnt_mountpoint = mnt->mnt.mnt_root;//
- mnt->mnt_parent = mnt;
- return &mnt->mnt;
- }
- 看mount_fs()函数的实现:
- struct dentry *
- mount_fs(struct file_system_type *type, int flags, const char *name, void *data)
- {
- struct dentry *root;
- struct super_block *sb;
- //执行ext4_mount(),返回sda3 ext4文件系统的根目录dentry
- root = type->mount(type, flags, name, data);
- //超级块super_block
- sb = root->d_sb;
- return root;
- }
ext4_mount ()函数的实现:
- static struct dentry *ext4_mount(struct file_system_type *fs_type, int flags,
- const char *dev_name, void *data)
- {
- return mount_bdev(fs_type, flags, dev_name, data, ext4_fill_super);
- }
- 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;
- //查找到”/dev/sda3”路径下”sda3”文件的dentry、inode结构,由inode得到sda3的struct block_device ,即bdev,最后执行__blkdev_get函数open sda3这个块设备
- bdev = blkdev_get_by_path(dev_name, mode, fs_type);
- //创建struct super_block结构并初始化其成员
- s = sget(fs_type, test_bdev_super, set_bdev_super, flags | MS_NOSEC,bdev);
- ………..
- strlcpy(s->s_id, bdevname(bdev, b), sizeof(s->s_id));
- sb_set_blocksize(s, block_size(bdev));
- //执行ext4_fill_super函数,从sda3磁盘读取ext4文件系统超级块等ext4文件系统基本数据,填充struct super_block *s超级块
- error = fill_super(s, data, flags & MS_SILENT ? 1 : 0);
- bdev->bd_super = s;
- }
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 目录下设备节点生成与访问过程 内核源码详解 有详解,这个知识点也是本案例的关键环节。
- struct block_device *blkdev_get_by_path(const char *path, fmode_t mode,
- void *holder)
- {
- struct block_device *bdev;
- //查找到”/dev/sda3” 的dentry、inode,由inode->i_rdev中保存的sda3主次设备号,创建sda3块设备bdev
- bdev = lookup_bdev(path);
- //打开sda3这个块设备
- err = blkdev_get(bdev, mode, holder);
- return bdev;
- }
- struct block_device *lookup_bdev(const char *pathname)
- {
- struct block_device *bdev;
- struct inode *inode;
- struct path path;
- //open ”/dev/sda3” 这个文件,得到”sda3”文件的dentry保存到path.dentry
- error = kern_path(pathname, LOOKUP_FOLLOW, &path);
- //”sda3”文件的inode,注意此时”sda3”文件是在devtmpfs文件系统上的
- inode = path.dentry->d_inode;
- // inode->i_rdev是sda3块设备的主次设备号,根据这个主次设备号,创建基于sda3块设备的bdev
- bdev = bd_acquire(inode);
- return bdev;
- }
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()函数。
- static struct block_device *bd_acquire(struct inode *inode)
- {
- struct block_device *bdev;
- //根据传入的块设备的主次设备号,找到或者创建块设备的bdev
- bdev = bdget(inode->i_rdev);
- return bdev;
- }
- //dev_t dev就是块设备的主次设备号
- struct block_device *bdget(dev_t dev)
- {
- struct block_device *bdev;
- struct inode *inode;
- //根据dev_t dev块设备号和blockdev_superblock从hash表inode_hashtable中
- //找出该块设备的inode,没有找到就分配新的inode
- inode = iget5_locked(blockdev_superblock, hash(dev),
- bdev_test, bdev_set, &dev);
- //块设备的bdev结构
- bdev = &BDEV_I(inode)->bdev;
- if (inode->i_state & I_NEW) {
- bdev->bd_contains = NULL;
- bdev->bd_super = NULL;
- //跟块设备有关的inode,这个inode与bdev组成struct bdev_inode
- bdev->bd_inode = inode;
- bdev->bd_block_size = (1 << inode->i_blkbits);
- bdev->bd_part_count = 0;
- bdev->bd_invalidated = 0;
- inode->i_mode = S_IFBLK;//i_mode代表块设备
- inode->i_rdev = dev;
- inode->i_bdev = bdev;
- inode->i_data.a_ops = &def_blk_aops;//块设备文件操作结构体
- mapping_set_gfp_mask(&inode->i_data, GFP_USER);
- inode->i_data.backing_dev_info = &default_backing_dev_info;
- spin_lock(&bdev_lock);
- //bdev添加到all_bdevs全局链表
- list_add(&bdev->bd_list, &all_bdevs);
- spin_unlock(&bdev_lock);
- }
- return bdev;
- }
bdev结构这里有必要介绍一下,如下:
- struct block_device {
- //包含块设备的主次设备号
- dev_t bd_dev;
- //块设备主分区mmcblk0和其他分区mmcblk0p4各有一个inode,不是同一个,bd_inode就指向块设备的inode,bdget函数赋值
- struct inode * bd_inode;
- //bdev文件系统的超级快,mount_bdev函数赋值
- struct super_block * bd_super;
- //块设备分区的bdev->bd_contains指向块设备主分区的bdev,主分区的bdev的bdev->bd_contains指向主分区bdev
- struct block_device * bd_contains;
- //bd_set_size()函数中赋值,块设备大小
- unsigned bd_block_size;
- //主分区register_disk()中设置1,之后才会扫描块设备分区
- int bd_invalidated;
- //在__blkdev_get()函数对块设备的bd_part赋值
- struct hd_struct * bd_part;
- //__blkdev_get()赋值为块设备的gendisk,块设备只有一个gendisk
- struct gendisk * bd_disk;
- }
块设备对应的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,找到或者创建块设备sda3的inode。注意,这是块设备基于bdev文件系统的inode,每个块设备有一个,代表的是底层块设备。与前文说的”/dev/sda3”中”sda3”文件的inode不一样,前者基于块设备文件系统,后者基于devtmpfs文件系统。继续bdget()函数,然后由inode得到bdev,再对bdev、inode初始化。
好了,现在bd_acquire()-> bdget() 执行完成,返回到lookup_bdev函数。可以发现,lookup_bdev()主要两个功能,一个是完成”/dev/sda3”的探测,然后创建sda3块设备bdev、inode。从lookup_bdev() 返回到blkdev_get_by_path(),接着会执行blkdev_get(),该函数完成最后会执行__blkdev_get(),得到块设备对应的struct gendisk *disk结构,对bdev的成员bd_disk、bd_queue、bd_contains、bd_part、bd_block_size等赋值,执行disk的block open函数,完成块设备的open。如果块设备代表整个块设备主分区,还会执执行rescan_partitions扫描块设备的各个分区,比如,sda代表的块设备主分区,其中的sda1、sda2、sda3分区就是在这里完成rescan。
现在再由blkdev_get_by_path()函数返回mount_bdev()函数,接着执行fill_super()也就是ex4_ fill_super()函数,该函数主要完成从sda3磁盘读取ext4文件系统超级块等ext4文件系统基本数据,填充struct super_block超级块结构,还得到该文件系统的根目录dentry、inode,这点非常关键,有了这个根目录dentry,就可以访问其下的文件或者目录了。
饶了一大圈,现在沿着vfs_kern_mount->mount_fs->ext4_mount->mount_bdev返回到vfs_kern_mount函数。该函数再总结一下作用:得到sda3 对应块的设备bdev,然后得到对应的sda3 ext4文件系统的根目录dentry和inode、super_block,然后对该文件系统的struct mount *mnt和mnt->mnt(即struct vfsmount)赋值,并返回mnt->mnt(即struct vfsmount)。如下图简单显示了sda3 ext4文件系统与根文件系统建立联系,挂载到根文件系统/home目录。
从vfs_kern_mount()函数返回到do_new_mount(),接着执行do_add_mount()。
- // struct mount *newmnt 就是sda3 ext4文件系统的,struct path *path是挂载点”/home”目录的
- static int do_add_mount(struct mount *newmnt, struct path *path, int mnt_flags)
- {
- struct mountpoint *mp;
- struct mount *parent;
- //基于挂载点”/home”目录得到struct mountpoint *mp,其中会把该目录dentry这样操作dentry->d_flags |= DCACHE_MOUNTED,表示home目录挂载了其他的文件系统。
- mp = lock_mount(path);
- //得到挂载点”/home”目录所在的文件系统的struct mount,即根文件系统的struct mount
- parent = real_mount(path->mnt);
- newmnt->mnt.mnt_flags = mnt_flags;
- // newmnt代表sda3 磁盘ext4文件系统,parent代表根文件系统
- err = graft_tree(newmnt, parent, mp);
- }
graft_tree完成最后的挂载工作,由于涉及到文件系统mount 多对一的情况,是它的大体原理是,基于newmnt、parent、mp,建立复杂的链表相互联系,由其中一个就能找到另外一个。newmnt、parent分别是sda3磁盘ext4文件系统的struct mount结构、根文件系统的struct mount结构,每个文件系统挂载时都有一个struct mount结构,其成员有struct vfsmount mnt。而struct vfsmount的成员struct dentry *mnt_root就是该文件系统的根目录dentry,graft_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、根目录dentry和inode,有这些才能真正的在这个文件系统中读写文件!而这些过程经过验证,正好都是在内核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->root,current->fs->root->dentry就是根文件系统的根目录dentry,之后就是在这个dentry下开始搜索”dev”目录的dentry。open的核心就是根据父目录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”文件的dentry,filp->f_path->dentry->d_parent是其父目录的dentry,就是devtmpfs文件系统的根目录dentry,这点第2节有讲解。filp->f_path.mnt是该文件系统的struct vfsmount结构。我们把filp->f_path.mnt和filp->f_path->dentry->d_parent保存到全局变量里,struct vfsmount devtmpfs_root_vfsmnt = filp->f_path.mnt,struct 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),但是这还不够,一个文件和父目录都有dentry、inode结构,这些结构彼此都有联系,在第一次open “/dev/sda3”时会自动建立。这个open过程是:
path_openat->link_path_walk->walk_component->lookup_slow->__lookup_hash->lookup_dcache,接着这样执行d_alloc函数为本次open的文件“/dev/sda3”分配struct dentry结构:
- struct dentry *d_alloc(struct dentry * parent, const struct qstr *name)
- {
- struct dentry *dentry = __d_alloc(parent->d_sb, name);//分配dentry结构
- dentry->d_parent = parent;// dentry->d_parent指向父目录dentry
- list_add(&dentry->d_child, &parent->d_subdirs);
- }
- //分配dentry并初始化dentry成员
- struct dentry *__d_alloc(struct super_block *sb, const struct qstr *name)
- {
- struct dentry *dentry;
- dentry = kmem_cache_alloc(dentry_cache, GFP_KERNEL);
- dentry->d_name.len = name->len;
- dentry->d_name.hash = name->hash;
- memcpy(dname, name->name, name->len);
- dentry->d_parent = dentry;
- dentry->d_sb = sb;
- INIT_LIST_HEAD(&dentry->d_lru);
- INIT_LIST_HEAD(&dentry->d_subdirs);
- INIT_HLIST_NODE(&dentry->d_u.d_alias);
- INIT_LIST_HEAD(&dentry->d_child);
- d_set_d_op(dentry, dentry->d_sb->s_d_op);
- }
再回到__lookup_hash()函数,执行lookup_real创建该“/dev/sda3”文件的inode,注意这里需要调用devtmpfs文件系统的lookup函数,因为devtmpfs文件系统挂载到了/dev/目录。在创建完inode后,会执行d_instantiate()建立该文件dentry和inode的联系, d_instantiate()函数源码如下:
- //建立文件dentry和inode联系
- void d_instantiate(struct dentry *entry, struct inode * inode)
- {
- //如果dentry已经添加过链表中,hlist_unhashed返回假,出发BUG_ON
- BUG_ON(!hlist_unhashed(&entry->d_u.d_alias));
- if (inode)
- spin_lock(&inode->i_lock);
- //令 dentry->d_u.d_alias添加到inode->i_dentry链表,然后dentry->d_inode = inode,建立该dentry、inode与父目录dentry、inode的联系。
- __d_instantiate(entry, inode);
- if (inode)
- spin_unlock(&inode->i_lock);
- security_d_instantiate(entry, inode);
- }
这些代码列出了第一次open一个文件时,创建该文件的dentry、inode的过程,还有建立他们彼此的联系。我们就是要手动实现”/dev/sda3”文件open过程:创建”sda3”文件的dentry、inode,建立二者的联系,还要建立他们与父目录dentry的联系。现在把我写的这个模块ko的源码较为完整的列出来,省去出错判断代码。
- struct dentry devtmpfs_root_dentry,sda3_dentry;
- struct inode devtmpfs_root_inode,sda3_inode;
- struct vfsmount devtmpfs_vfsmnt;
- struct super_block devtmpfs_sb;
- //在ko模块init函数中
- int capture_reboot_init()
- {
- struct file *filp = open(“/dev/sda3”,O_RDWR,0644);
- //获取”/dev/sda3”文件的dentry、inode
- memcpy(&devtmpfs_root_dentry, filp->f_path->dentry->d_parent,sizeof(struct dentry));
- memcpy(&devtmpfs_root_inode, devtmpfs_root_dentry.d_inode,sizeof(struct inode ));
- //获取/dev所属devtmpfs文件系统的根dentry和inode、超级块
- memcpy(&sda3_dentry, filp->f_path->dentry,sizeof(struct dentry));
- memcpy(&sda3_inode, sda3_dentry.d_inode,sizeof(struct inode ));
- memcpy(&devtmpfs_sb, devtmpfs_root_inode.i_sb,sizeof(struct super_block));
- //建立”sda3”文件dentry、inode与devtmpfs文件系统根目录dentry、inode,超级块关系
- devtmpfs_root_dentry.d_inode= &devtmpfs_root_inode;
- sda3_dentry.d_inode= &sda3_inode;
- devtmpfs_vfsmnt = filp->f_path.mnt;
- sda3_inode.i_sb=&devtmpfs_sb;
- devtmpfs_root_inode.i_sb= devtmpfs_sb;
- sda3_dentry.d_parent=& devtmpfs_root_dentry;
- //”sda3”文件的inode的i_bdev必须设置为NULL,这样才会创探测新的sda3 bdev块设备
- sda3_inode.i_bdev = NULL;
- //建立”sda3”文件dentry、inode的联系,并把dentry添加到dentry hash链表
- d_add(&sda3_dentry,& sda3_inode);
- //初始化”sda3”文件dentry关键成员,尤其是sda3_dentry .d_u.d_alias
- INIT_LIST_HEAD(&sda3_dentry .d_lru);
- INIT_LIST_HEAD(&sda3_dentry .d_subdirs);
- INIT_HLIST_NODE(&sda3_dentry .d_u.d_alias);
- INIT_LIST_HEAD(&sda3_dentry .d_child);
- filp_close(filp);
- }
ko模块在注册的reboot消息通知函数reboot_notify_function中,在系统发起reboot重启命令最后,进入内核空间执行
- int reboot_notify_function(void)
- {
- struct vfsmount *vfsmnt;
- struct file *filp;
- loff_t pos;
- char buf[100];
- struct *sda3_root_dentry;
- //修改默认的根目录dentry为原devtmpfs文件系统的根目录dentry
- current->fs->root->dentry = devtmpfs_root_dentry;
- //修改为原devtmpfs文件系统的struct vfsmount结构
- current->fs->root.mnt= &devtmpfs_root_vfsmnt;
- //模拟sda3块设备的mount过程,先模拟探测”/dev/sda3”文件,再创建sda3块设备bdev,探测该块设备中ext4文件系统,获取根目录dentry和inode,vfsmount,超级块等
- vfsmnt = vfs_kern_mount(get_fs_type(“ext4”) , MS_KERNMOUNT,”/sda3”,NULL);
- sda3_root_dentry = vfsmnt->mnt_root;
- //以sda3_root_dentry为根目录dentry,open该目录下的reboot_info文件
- filp= file_open_root(sda3_root_dentry, vfsmnt,”reboot_info”,O_RDWR);
- //将重启前进程的名字和pid保存到reboot_info文件。
- snprintf(buf,100,”%s %d\n”,current->comm,current->pid);
- pos = 0;
- vfs_write(filp, buf,strlen(buf)+1,&pos);
- filp_close(filp);
- }
以上是该模块完整的核心源码,数下来也就几十行,实现起来不算复杂。capture_reboot_init()提前获取”/dev/sda3”文件的dentry、inode信息,以及该文件所在的devtmpfs文件系统的根目录dentry、inode、vfsmount、superblock信息,然后把这些信息保存到全局变量中。同时伪造一个devtmpfs文件系统,把各个成员错综复杂的关系建立起来,重点是建立devtmpfs文件系统根目录dentry、inode与”/dev/sda3”文件dentry、inode的联系,”/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的内核实现。
- static struct block_device *bd_acquire(struct inode *inode)
- {
- struct block_device *bdev;
- bdev = inode->i_bdev;
- //如果inode->i_bdev不为NULL,直接return
- if (bdev) {
- ihold(bdev->bd_inode);
- spin_unlock(&bdev_lock);
- return bdev;
- }
- //根据传入的主次设备号,找到或者创建块设备的bdev
- bdev = bdget(inode->i_rdev);
- }
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_fast,open “/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”这个块设备文件节点,获取其dentry、inode,这个inode就是sda3_inode,接着使用这个inode->i_bdev创建sda3块设备bdev,然后探测sda3对应的磁盘ext4文件系统,获取ext4文件系统根目录dentry和inode,还有super_block,vfsmount。
5 filp= file_open_root(sda3_root_dentry, vfsmnt,”reboot_info”,O_RDWR); 是sda3 ext4文件系统根目录dentry为父目录dentry,然后在该目录下open文件”reboot_info”。
好了,这里终于相关技术难点讲解完毕。涉及的东西繁多,可能思路有点绕。文章有什么错误,请大佬多指点。