• 正文
  • 相关推荐
申请入驻 产业图谱

从open到close:一个文件在内核里的完整旅程

11/03 08:29
132
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

大家好,我是仲一。

本篇文章从 open() 系统调用切入,跟进去看内核里一步步做了什么,然后 close() 又是怎么收场的。

open 总览

用户程序调 open:

int fd = open("/home/zhongyi/test.c", O_RDWR, 0);

C 库封装后通过软中断进内核,到达 SYSCALL_DEFINE3 定义的入口(SYSCALL_DEFINE 怎么展开的见前文)。最终实际调用的是 do_sys_open:

long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
    struct open_flags op;
    int fd = build_open_flags(flags, mode, &op);
    struct filename *tmp;

    if (fd)
        return fd;

    tmp = getname(filename);
    if (IS_ERR(tmp))
        return PTR_ERR(tmp);

    fd = get_unused_fd_flags(flags);
    if (fd >= 0) {
        struct file *f = do_filp_open(dfd, tmp, &op);
        if (IS_ERR(f)) {
            put_unused_fd(fd);
            fd = PTR_ERR(f);
        } else {
            fsnotify_open(f);
            fd_install(fd, f);
        }
    }
    putname(tmp);
    return fd;
}

流程拆开:

build_open_flags() — 把用户传的 flags/mode 转成内核的

struct open_flagsgetname() — 把文件路径从用户空间拷到内核空间

get_unused_fd_flags() — 拿一个空闲的文件描述符

do_filp_open() — 真正打开文件,拿到

struct filefsnotify_open() — 通知文件系统有人开了文件

fd_install() — 把 fd 和 file 结构体绑到一起

putname() — 释放内核里存路径名的内存

按这个顺序往下看。


build_open_flags()

struct open_flags {
    int open_flag;
    umode_t mode;
    int acc_mode;
    int intent;
    int lookup_flags;
};

static inline int build_open_flags(int flags, umode_t mode, struct open_flags *op)
{
    int lookup_flags = 0;
    int acc_mode = ACC_MODE(flags);

    flags &= VALID_OPEN_FLAGS;

    if (flags & (O_CREAT | __O_TMPFILE))
        op->mode = (mode & S_IALLUGO) | S_IFREG;
    else
        op->mode = 0;

    flags &= ~FMODE_NONOTIFY & ~O_CLOEXEC;

    if (flags & __O_SYNC)
        flags |= O_DSYNC;

    if (flags & __O_TMPFILE) {
        ...
    } else if (flags & O_PATH) {
        flags &= O_DIRECTORY | O_NOFOLLOW | O_PATH;
        acc_mode = 0;
    }

    op->open_flag = flags;

    if (flags & O_TRUNC)
        acc_mode |= MAY_WRITE;
    if (flags & O_APPEND)
        acc_mode |= MAY_APPEND;

    op->acc_mode = acc_mode;
    op->intent = flags & O_PATH ? 0 : LOOKUP_OPEN;

    if (flags & O_CREAT) {
        op->intent |= LOOKUP_CREATE;
        if (flags & O_EXCL)
            op->intent |= LOOKUP_EXCL;
    }

    if (flags & O_DIRECTORY)
        lookup_flags |= LOOKUP_DIRECTORY;
    if (!(flags & O_NOFOLLOW))
        lookup_flags |= LOOKUP_FOLLOW;

    op->lookup_flags = lookup_flags;
    return 0;
}

做的事情:各种标志位检查,翻译成 open_flags 结构体,后面给路径查找和文件打开函数用。


getname()

struct filename {
    const char         *name;   /* 内核空间路径指针 */
    const __user char  *uptr;   /* 用户空间原始指针 */
    struct audit_names *aname;
    int                refcnt;
    const char         iname[]; /* 实际文件名 */
};

struct filename *getname(const char __user *filename)
{
    return getname_flags(filename, 0, NULL);
}

内核从 slab 分配器拿一个 struct filename,用 strncpy_from_user() 把用户空间的路径拷到内核的 iname 数组里。如果路径超过 EMBEDDED_NAME_MAX(256 字节),再单独给路径字符串分配空间。


get_unused_fd_flags()

拿一个空闲 fd:

int get_unused_fd_flags(unsigned flags)
{
    return __alloc_fd(current->files, 0, rlimit(RLIMIT_NOFILE), flags);
}

__alloc_fd 做的事情:

从当前进程的 files_struct 拿到 fdtable

在位图里 find_next_fd() 找一个空闲 fd(最小可用原则)槽位不够就 expand_files() 扩容标记该 fd 已使用设置了 O_CLOEXEC 的话,标记 exec 时关闭


do_filp_open()

前面都是准备,到这里才真正开文件。核心是把路径名和 inode 节点对上。

struct file 是文件读写的上下文,最终会挂到进程的 files_struct 数组里:

struct file {
    struct path     f_path;     /* 文件路径 */
    struct inode    *f_inode;   /* inode 节点 */
    const struct file_operations *f_op;
    spinlock_t      f_lock;
    atomic_long_t   f_count;    /* 引用计数 */
    unsigned int    f_flags;
    fmode_t         f_mode;
    loff_t          f_pos;      /* 当前读写位置 */
    ...
};

do_filp_open 主体

struct file *do_filp_open(int dfd, struct filename *pathname,
            const struct open_flags *op)
{
    struct nameidata nd;
    int flags = op->lookup_flags;
    struct file *filp;

    set_nameidata(&nd, dfd, pathname);
    filp = path_openat(&nd, op, flags | LOOKUP_RCU);
    if (unlikely(filp == ERR_PTR(-ECHILD)))
        filp = path_openat(&nd, op, flags);
    if (unlikely(filp == ERR_PTR(-ESTALE)))
        filp = path_openat(&nd, op, flags | LOOKUP_REVAL);
    restore_nameidata();
    return filp;
}

path_openat 最多调三次,一次比一次慢:

RCU-walk — RCU 模式下查 dcache,最快

ref-walk — 引用模式再查 dcache

force-reval — 强制文件系统从磁盘重新读

path_openat

static struct file *path_openat(struct nameidata *nd,
            const struct open_flags *op, unsigned flags)
{
    struct file *file;
    int opened = 0;

    file = get_empty_filp();
    if (IS_ERR(file))
        return file;

    file->f_flags = op->open_flag;

    if (unlikely(file->f_flags & __O_TMPFILE)) {
        error = do_tmpfile(nd, flags, op, file, &opened);
        goto out2;
    }
    if (unlikely(file->f_flags & O_PATH)) {
        error = do_o_path(nd, flags, file);
        ...
        goto out2;
    }

    s = path_init(nd, flags);
    ...
    while (!(error = link_path_walk(s, nd)) &&
            (error = do_last(nd, file, op, &opened)) > 0) {
        ...
    }
    ...
    return file;
}

几步:

get_empty_filp() — 分配

struct filepath_init() — 确定从哪里开始找

link_path_walk() — 逐层路径遍历

do_last() — 处理路径最后一个分量(文件名)


path_init

三种情况:

绝对路径(/ 开头)

— set_root() 从根目录开始

相对路径(AT_FDCWD) — 从 current->fs->pwd(当前工作目录)开始

指定 fd — 从 fdget_raw() 拿到的目录开始


link_path_walk

路径用 / 分隔,逐层解析。比如 /root/hello/world/data,前半部分 /root/hello/world 在这解析完,最后剩个 data 留给 do_last。

大循环里每次都 may_lookup() 检查权限,hash_name() 算哈希,然后 walk_component() 走一步。


walk_component

处理每一步的路径分量:

. — 原地不动

.. — 退回父目录普通目录

— 先 lookup_fast() 查 dcache,没命中就 lookup_slow() 调文件系统的 lookup 函数


do_last

最后一步处理文件名。

决策逻辑:

如果是目录类型(. 或 ..)→ 返回失败符号链接 → 返回 1,让上层跟进文件不存在但有 O_CREAT → 创建 inode 然后打开文件不存在且无 O_CREAT → 返回 -ENOENT文件存在 → 检查权限后打开

调用链:

do_last()
  → lookup_fast() / lookup_open()   // 找 dentry
  → may_open()                       // 检查权限
  → vfs_open()                       // 实际打开
  → open_check_o_direct() / ima_file_check()

fd_install()

dentry、inode、file 结构体都就位了,最后一步:把 fd 和 file 结构体绑起来。

void __fd_install(struct files_struct *files, unsigned int fd,
            struct file *file)
{
    struct fdtable *fdt;
    ...
    fdt = rcu_dereference_sched(files->fdt);
    BUG_ON(fdt->fd[fd] != NULL);
    rcu_assign_pointer(fdt->fd[fd], file);
    ...
}

核心就一行:fdt->fd[fd] = file。


putname()

释放 getname() 分配的内核路径缓冲区,引用计数减到 0 就真正释放。


close

close() 的入口是 SYSCALL_DEFINE1(close, ...):

SYSCALL_DEFINE1(close, unsigned int, fd)
{
    int retval = __close_fd(current->files, fd);
    ...
    return retval;
}

__close_fd 的流程:

    1. 检查 fd 是否超范围从 fdtable 取出 file 指针把 fdtable 槽位置 NULL清除 close-on-exec 标记标记 fd 为空闲调

filp_close()

     做实际关闭
int __close_fd(struct files_struct *files, unsigned fd)
{
    struct file *file;

    spin_lock(&files->file_lock);
    fdt = files_fdtable(files);
    if (fd >= fdt->max_fds)
        goto out_unlock;
    file = fdt->fd[fd];
    if (!file)
        goto out_unlock;
    rcu_assign_pointer(fdt->fd[fd], NULL);
    __clear_close_on_exec(fd, fdt);
    __put_unused_fd(files, fd);
    spin_unlock(&files->file_lock);
    return filp_close(file, files);
    ...
}

filp_close

int filp_close(struct file *filp, fl_owner_t id)
{
    if (!file_count(filp)) {
        printk(KERN_ERR "VFS: Close: file count is 0n");
        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. 如果文件系统有

    flush

      1.  方法,把内容刷到磁盘释放 POSIX 锁

    fput()

      1.  减引用计数,减到 0 时真正释放

    struct file


完整链路

open()
  → SYSCALL_DEFINE3(open, ...)
    → do_sys_open()
      → build_open_flags()     // 解析 flags/mode
      → getname()              // 复制文件名到内核
      → get_unused_fd_flags()  // 分配 fd
      → do_filp_open()         // 路径查找 + 文件创建
        → path_init()          // 确定起点
        → link_path_walk()     // 逐层遍历
        → do_last()            // 处理最后一级
          → vfs_open()         // 实际打开
      → fd_install()           // 绑定 fd 和 file
      → putname()              // 释放路径名
    → return fd

close(fd)
  → SYSCALL_DEFINE1(close, ...)
    → __close_fd()             // 释放 fd 槽位
    → filp_close()             // flush + 释放锁
    → fput()                   // 释放 file

参考:

    https://blog.csdn.net/lickylin/article/details/101619601https://cloud.tencent.com/developer/article/1439044https://zhuanlan.zhihu.com/p/471175983以及文内列出的其他参考链接

仲一做嵌入式已经六七年了,现在在某芯片公司做驱动开发,之前带过团队、也面试招过人。简历看多了,面试也面多了,所以职业怎么走、简历哪里不对、面试卡在哪儿,这些事我算是有点发言权。

职业方向、简历、面试,哪块有疑惑都可以找我聊聊。想聊的,公众号后台回复「咨询」。

相关推荐

作者就职于某500强公司,担任BSP工程师。具有丰富的嵌入式开发经验。专栏主要分享计算机基础,操作系统,Linux驱动开发,Arm体系与架构,C/C++,数据结构与算法等相关文章。欢迎关注我的公众号【嵌入式与Linux那些事】,一起学习交流。