• Linux内核(十六)Linux 内核态进行读写文件的函数 使用和解析



    Linux 版本:Linux version 3.18.24

    概要

    在内核态进行文件读写,我们不能直接使用用户态的系统调用,而是需要使用一些特定的函数。
    以下是一些常用的函数:filp_open、filp_close、vfs_read、vfs_write、set_fs、get_fs等函数。


    函数声明

    extern struct file *filp_open(const char *, int, umode_t);

    参数说明:
    第一个参数表明要打开或创建文件的名称(包括路径部分)。
    第二个参数文件的打开方式,可以取O_CREAT,O_RDWR,O_RDONLY等。
    第三个参数创建文件时使用,设置创建文件的读写权限,其它情况可以设为0
    该函数返回strcut file*结构指针,供后继函数操作使用,该返回值用IS_ERR()来检验其有效性。

    extern int filp_close(struct file *, fl_owner_t id);

    参数说明:
    第一个参数是filp_open返回的file结构体指针
    第二个是参数POSIX线程,ID基本上都是NULL

    extern ssize_t vfs_read(struct file *, char __user *, size_t, loff_t *);
    extern ssize_t vfs_write(struct file *, const char __user *, size_t, loff_t *);

    参数说明:
    第一个参数是filp_open返回的file结构体指针
    第二个参数是buf,注意,这个参数有用__user修饰,表明buf指向用户空间的地址,如果传入内核空间的地址,就会报错,并返回-EFAULT。
    第三个参数表明文件要读写的起始位置。

    但在kernel中,要使这两个读写函数使用kernel空间的buf指针才能正确工作,需要使用set_fs()
    static inline void set_fs(mm_segment_t fs)

    该函数的作用是改变kernel对内存地址检查的处理方式,
    其实该函数的参数fs只有两个取值:USER_DS,KERNEL_DS,分别代表用户空间和内核空间,
    默认情况下,kernel取值为USER_DS,即对用户空间地址检查并做变换。
    那么要在这种对内存地址做检查变换的函数中使用内核空间地址,就需要使用set_fs(KERNEL_DS)进行设置,
    它的作用是取得当前的设置,这两个函数的一般用法为:

    filp_open()
    mm_segment_t old_fs;
    old_fs = get_fs();
    set_fs(KERNEL_DS);
    ...... //与内存有关的操作
    set_fs(old_fs);
    filp_close
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    函数具体解析

    filp_open —— 打开文件并返回文件指针

    filp_open
        -> file_open_name
            -> build_open_flags        // 使用传入的文件flag,初始化struct open_flags实例op
                -> if (flags & (O_CREAT | __O_TMPFILE))
                -> if (flags & __O_SYNC)
                -> if (flags & O_CREAT) 
                ......
            -> do_filp_open
                -> struct nameidata nd;
                -> filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);        // 内核为了提高效率,会首先在RCU模式(rcu-walk)下进行文件打开操作
                    if (unlikely(filp == ERR_PTR(-ECHILD)))
                        filp = path_openat(dfd, pathname, &nd, op, flags);                // 如果在此方式下打开失败,则进入普通模式(ref-walk)
                    if (unlikely(filp == ERR_PTR(-ESTALE)))
                        filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_REVAL);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    真正的文件open工作,都在path_openat里面完成。在do_filp_open函数中,除了声明了要返回的file以外还声明了一个结构体nameidata,其作用是保存本次查找的结果。

    enum { MAX_NESTED_LINKS = 8 };
    
    struct nameidata {
        struct path path;       /* 当前搜索的目录 path里保存着dentry指针和挂载信息vfsmount */
        struct qstr last;       /* 下一个待处理的component。只有last_type是LAST_NORM时这个字段才有用*/
        struct path root;       /* 保存根目录的信息 */
        struct inode    *inode; /* path.dentry.d_inode */
        unsigned int    flags;  /* 查找相关的标志位 */
        unsigned    seq;        /* 目录项的顺序锁序号 */
        int     last_type;      /* This is one of LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT, or LAST_BIND. */
        unsigned    depth;      /* 解析符号链接过程中的递归深度 */
        char *saved_names[MAX_NESTED_LINKS + 1]; /* 相应递归深度的符号链接的路径 */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    do_filp_open中调用3次path_openat,下面分别分析下三次的path_openat。
    第一次调用尝试以 rcu模式打开,当 flags = LOOKUP_FOLLOW | LOOKUP_RCU 会成功执行这个模式

    path_openat
        -> file = get_empty_filp();    // 初始化新的file结构,分配前会对当前进程的权限和当前系统的文件最大数进行检测
            -> error = path_init(dfd, pathname->name, flags | LOOKUP_PARENT, nd, &base);   //对路径遍历做准备工作,主要是判断路径遍历的起始位置
            -> error = link_path_walk(pathname->name, nd);   // 对所打开文件路径进行逐一解析,每个目录项的解析结果都存在nd参数中
            -> error = do_last(nd, &path, file, op, &opened, pathname);    // 根据最后一个目录项的结果,do_last()将填充filp所指向的file结构
            -> while (unlikely(error > 0)) ....        //filp为空,说明当前文件为符号链接文件
                // 如果设置了LOOKUP_FOLLOW标志,则通过follow_link()进入符号链接文件所指文件,填充file
                // 否则,直接返回当前符号链接文件的filp;
                   if (!(nd->flags & LOOKUP_FOLLOW)) {        
                path_put_conditional(&path, nd);
                path_put(&nd->path);
                filp = ERR_PTR(-ELOOP);
                break;
            }
                    error = follow_link(&link, nd, &cookie);
              ......
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    path_openat主要动作:
    1、通过get_empty_filp初始化file
    2、调用path_init固定路径查找点
    3、调用link_path_walk遍历路径下的文件
    4、使用do_last得到相应的file

    path_init固定路径查找点

    path_init
        -> nd->last_type = LAST_ROOT; /* if there are only slashes... */
        -> nd->flags = flags | LOOKUP_JUMPED;
        -> nd->depth = 0;
        -> nd->root.mnt = NULL;
        -> nd->m_seq = read_seqbegin(&mount_lock);
        -> if (*name=='/') {                    // 假如输入的路径为/sys/log
            if (flags & LOOKUP_RCU) {
                rcu_read_lock();
                nd->seq = set_root_rcu(nd);
                    -> nd->root = fs->root;        // 根目录
            } else {
                set_root(nd);
                    -> nd->root = fs->root;        // 根目录
                path_get(&nd->root);
            }
            nd->path = nd->root;
            } else if (dfd == AT_FDCWD) {     // 相对路径是以当前路径pwd作为起始的,因此通过pwd设置nd
                ......         
            } else {            // 这个相对路径是用户设置的,需要通过dfd获取具体相对路径信息,进而设置nd
                 ......        
            }
         -> nd->inode = nd->path.dentry->d_inode;         
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    path_init 主要是用来初始化struct nameidata实例中的path、root、inode等字段。

    重点 link_path_walk遍历路径下的文件

    static int link_path_walk(const char *name, struct nameidata *nd)
    {
        struct path next;
        int err;
    
        while (*name=='/')            //  跳过开始的‘/’字符
            name++;
        if (!*name)
            return 0;
    
        /* At this point we know we have a real path component. */
        for(;;) {
            u64 hash_len;
            int type;
    
            err = may_lookup(nd);
            if (err)
                break;
    
            hash_len = hash_name(name);        // 获取下一个path component的hash和len,并复制给hash_len。
    
            type = LAST_NORM;
            if (name[0] == '.') switch (hashlen_len(hash_len)) {
                case 2:
                    if (name[1] == '.') {
                        type = LAST_DOTDOT;
                        nd->flags |= LOOKUP_JUMPED;
                    }
                    break;
                case 1:
                    type = LAST_DOT;
            }
            if (likely(type == LAST_NORM)) {
                struct dentry *parent = nd->path.dentry;
                nd->flags &= ~LOOKUP_JUMPED;
                if (unlikely(parent->d_flags & DCACHE_OP_HASH)) {
                    struct qstr this = { { .hash_len = hash_len }, .name = name };
                    err = parent->d_op->d_hash(parent, &this);
                    if (err < 0)
                        break;
                    hash_len = this.hash_len;
                    name = this.name;
                }
            }
    
            nd->last.hash_len = hash_len;        // 将该path component的信息赋值给nd->last字段。
            nd->last.name = name;
            nd->last_type = type;
    
            name += hashlen_len(hash_len);        // 修改name的值,使其指向path的下一个component。
            if (!*name)
                return 0;
            /*
             * If it wasn't NUL, we know it was '/'. Skip that
             * slash, and continue until no more slashes.
             */
            do {
                name++;
            } while (unlikely(*name == '/'));
            if (!*name)                         // 如果下一个component为空,则goto到OK这个label,执行一些操作之后,最后return 0给上层。
                return 0;
    
            //如果下一个component不为空,则执行walk_component方法,找到nd->last字段指向的component对应的dentry、inode等信息,并更新nd->path、nd->inode等字段,使其指向新的路径。
            err = walk_component(nd, &next, LOOKUP_FOLLOW);
            if (err < 0)
                return err;
    
            if (err) {
                err = nested_symlink(&next, nd);
                if (err)
                    return err;
            }
            if (!d_can_lookup(nd->path.dentry)) {
                err = -ENOTDIR;
                break;
            }
        }
        terminate_walk(nd);
        return err;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    以open /sys/log为例,该方法最终的结果是,更新struct nameidata实例指针nd中的path、inode字段,使其指向路径/sys/,更新nd中的last值,使其为log。

    do_last得到相应的file

    do_last
        -> error = lookup_fast(nd, path, &inode);    // 找路径中的最后一个component
           if (likely(!error))
                goto finish_lookup;
        -> finish_lookup:
            error = step_into(nd, &path, 0, inode, seq);
            ...
            error = vfs_open(&nd->path, file);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果成功,就会跳到finish_lookup对应的label,然后执行step_into方法,更新nd中的path、inode等信息,使其指向目标路径。

    之后,调用vfs_open方法,继续执行open操作。

    最后,返回error给上层,如果成功,error为0。


    filp_close —— 内核空间的文件关闭操作

    内核中的文件如果不在使用,需要将文件进行关闭,释放其中的资源。Linux内核中关闭文件的函数为filp_close()

    int filp_close(struct file *filp, fl_owner_t id)
    {
        int retval = 0;
    
        if (!file_count(filp)) {
            printk(KERN_ERR "VFS: Close: file count is 0\n");
            return 0;
        }
    
        if (filp->f_op->flush)
            retval = filp->f_op->flush(filp, id);
    
        if (likely(!(filp->f_mode & FMODE_PATH))) {
            dnotify_flush(filp, id);
            locks_remove_posix(filp, id);
        }
        fput(filp);
        return retval;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    vfs_read/write —— 读写对应文件内容

    ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
    {
        ssize_t ret;
    
        if (!(file->f_mode & FMODE_READ))         //判断文件是否可读
            return -EBADF;
        if (!(file->f_mode & FMODE_CAN_READ))        //是否定义文件读方法
            return -EINVAL;
        if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
            return -EFAULT;
    
        ret = rw_verify_area(READ, file, pos, count);        //读校验 
        if (ret >= 0) {
            count = ret;
            if (file->f_op->read)
                ret = file->f_op->read(file, buf, count, pos);    //调用文件读操作方法
            else if (file->f_op->aio_read)
                ret = do_sync_read(file, buf, count, pos);             //通用文件模型读方法
            else
                ret = new_sync_read(file, buf, count, pos);         
            if (ret > 0) {
                fsnotify_access(file);
                add_rchar(current, ret);
            }
            inc_syscr(current);
        }
    
        return ret;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    如果该文件索引节点inode定义了文件的读实现方法的话,就调用此方法. Linux下特殊文件读往往是用此方法, 一些伪文件系统如:proc,sysfs等,读写文件也是用此方法 . 而如果没有定义此方法就会调用通用文件模型的读写方法.它最终就是读内存,或者需要从存储介质中去读数据。

    ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
    {
        ssize_t ret;
    
        if (!(file->f_mode & FMODE_WRITE))        //判断文件是否可写
            return -EBADF;
        if (!(file->f_mode & FMODE_CAN_WRITE))    //是否定义文件写方法
            return -EINVAL;
        if (unlikely(!access_ok(VERIFY_READ, buf, count)))
            return -EFAULT;
    
        ret = rw_verify_area(WRITE, file, pos, count);    //写校验
        if (ret >= 0) {
            count = ret;
            file_start_write(file);
            if (file->f_op->write)
                ret = file->f_op->write(file, buf, count, pos);    //调用文件写操作方法
            else if (file->f_op->aio_write)
                ret = do_sync_write(file, buf, count, pos);         //通用文件模型写方法
            else
                ret = new_sync_write(file, buf, count, pos);       
            if (ret > 0) {
                fsnotify_modify(file);
                add_wchar(current, ret);
            }
            inc_syscw(current);
            file_end_write(file);
        }
    
        return ret;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    这个函数和vfs_read()都是差不多的,只是调用的文件操作方法不同而已(file->f_op->write) ,如果没有定义file->f_op->write ,同样也需要do_sync_write()调用同样文件写操作, 首先把数据写到内存中,然后在适当的时候把数据同步到具体的存储介质中去。

  • 相关阅读:
    BUUCTF Reverse/[QCTF2018]Xman-babymips
    RI-TRP-DR2B 32mm 玻璃应答器|CID载码体标签在半导体行业重复利用之检测方法
    7-7 温度转换v1.02
    88. 合并两个有序数组 (Swift版本)
    WebDAV之π-Disk派盘 + Xplore
    【RC&RL充放电时间相关计算】
    MySQL的默认引擎为什么是InnoDB
    算法练习1——合并两个有序数组
    跨行转做产品经理岗位,怎么入门?
    uniapp的安装与基础
  • 原文地址:https://blog.csdn.net/weixin_43564241/article/details/132614601