Bootstrap

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

函数具体解析

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);

真正的文件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]; /* 相应递归深度的符号链接的路径 */
};

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);
          ......

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;         

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;
}

以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);

如果成功,就会跳到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;
}

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;
}

如果该文件索引节点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;
}

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

;