跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

进程如何打开磁盘文件:从 open() 到文件描述符的源码解析

Linux 内核中 open() 系统调用涉及用户态到内核态切换、路径解析、文件描述符分配及核心数据结构协作。过程包括 sys_open 入口、参数解析、fd 分配、do_filp_open 路径查找、file 结构创建及关联。关键数据结构涵盖 task_struct、files_struct、fdtable、struct file、inode 及 dentry。VFS 层通过抽象统一设备访问,利用缓存加速路径与元数据检索,引用计数管理生命周期。理解该机制有助于掌握操作系统资源管理与文件系统底层原理。

云间运维发布于 2026/2/4更新于 2026/7/255.4K 浏览
进程如何打开磁盘文件:从 open() 到文件描述符的源码解析

1. 前言:一次 open() 调用背后的史诗

在之前的文章中,我们已经深入探讨了文件在磁盘上的静态存储结构(ext2 文件系统),也了解了 VFS 如何通过'一切皆文件'的哲学统一了各种设备。今天,我们将把目光聚焦在一个看似简单却暗藏玄机的问题上:当进程调用 open() 时,内核究竟发生了什么?

想象一下:你写下一行代码 int fd = open("test.txt", O_RDONLY);,短短几个字符,却触发了一场跨越用户空间与内核空间、贯穿磁盘与内存的'信息接力'。

学习完这篇文章,你将彻底理解:

  1. open() 系统调用的完整调用链:从用户态到内核态的切换
  2. 路径解析的艺术:如何从 /home/user/test.txt 找到对应的 inode
  3. 核心数据结构的关系:task_struct、files_struct、fdtable、struct file、inode、dentry 是如何协作的
  4. 文件描述符的分配机制:为什么 fd 总是从最小的空闲数字开始
  5. 缓存的力量:dentry cache 和 inode cache 如何加速文件访问

image.png

这是一场关于'连接'的旅程——连接用户与内核、连接路径与 inode、连接进程与文件。让我们开始吧。

image.png


2. 全景概览:open() 调用的七个阶段

一次 open() 调用,内核经历了七个关键阶段。在深入源码之前,让我们先建立整体认知。

2.1 七个阶段概览

阶段名称核心函数/操作作用
1系统调用入口SYSCALL_DEFINE3(open)从用户态陷入内核态
2参数解析build_open_flags()解析 flags 和 mode
3分配 fdget_unused_fd_flags()在 fdtable 中找到空闲 fd
4路径解析do_filp_open() → path_openat()解析路径,查找/创建 inode
5创建 filealloc_empty_file()创建 struct file 结构体
6关联 fd 与 filefd_install()将 file 指针填入 fd_array[fd]
7返回用户态系统调用返回将 fd 返回给用户程序

2.2 核心数据结构关系

在深入源码之前,我们需要理解五个核心数据结构之间的关系:

task_struct (进程) └── files (files_struct) └── fdt (fdtable) └── fd_array[] └── struct file ├── f_inode → inode └── f_dentry → dentry 

这个关系链是理解 open() 实现的核心。接下来,我们将从源码层面深入分析每个阶段。

image.png


3. 深入内核:从 sys_open 到 do_sys_open

3.1 系统调用的入口

当用户程序调用 open() 时,glibc 会将其封装为系统调用。内核的入口点是 sys_open,由 SYSCALL_DEFINE3 宏定义:

// fs/open.c
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode) {
    // 如果强制使用大文件支持,设置 O_LARGEFILE 标志
    if(force_o_largefile()) flags |= O_LARGEFILE;
    // 调用核心的 do_sys_open
    // AT_FDCWD 表示从当前工作目录开始解析相对路径
    return do_sys_open(AT_FDCWD, filename, flags, mode);
}

这个函数非常简单,只是做了一些标志位的检查,然后调用 do_sys_open。真正的逻辑都在 do_sys_open 中。

3.2 do_sys_open:协调者

do_sys_open 是整个打开流程的'总导演',它协调了 fd 分配、路径解析、file 结构创建等所有步骤:

// fs/open.c
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode) {
    struct open_flags op;      // 存储解析后的打开标志
    struct filename *tmp;       // 内核空间的路径名
    int fd, error;              // ===== 步骤 1:解析用户传入的 flags =====
    // build_open_flags 将用户空间的 flags(如 O_RDONLY, O_CREAT)
    // 转换为内核使用的 open_flags 结构
    error = build_open_flags(flags, mode, &op);
    if(error) return error;

    // ===== 步骤 2:将用户空间的路径名拷贝到内核空间 =====
    // getname 会做以下事情:
    // - 检查路径名是否合法(长度、字符等)
    // - 将路径名从用户空间拷贝到内核空间
    // - 处理一些特殊情况(如空路径、绝对路径等)
    tmp = getname(filename);
    if(IS_ERR(tmp)) return PTR_ERR(tmp);

    // ===== 步骤 3:获取一个未使用的文件描述符 fd =====
    // get_unused_fd_flags 会在当前进程的 fdtable 中
    // 找到最小的空闲 fd 编号
    fd = get_unused_fd_flags(flags);
    if(fd >= 0) {
        // ===== 步骤 4:执行真正的文件打开操作(核心!)=====
        // do_filp_open 会完成以下工作:
        // - 路径解析(path_openat)
        // - 查找/创建 inode
        // - 创建 struct file
        struct file *f = do_filp_open(dfd, tmp, &op);
        if(IS_ERR(f)) {
            // 打开失败,释放之前分配的 fd
            put_unused_fd(fd);
            fd = PTR_ERR(f);
        } else {
            // ===== 步骤 5:将 struct file 与 fd 关联 =====
            // fd_install 会将 file 指针填入 fd_array[fd]
            // 这样以后通过 fd 就能找到这个 file 结构体
            fd_install(fd, f);
        }
    }
    // ===== 步骤 6:清理,释放临时分配的内存 =====
    putname(tmp);
    // 返回 fd 给用户空间
    // 如果 fd < 0,表示出错,fd 的绝对值就是错误码
    return fd;
}

这个函数的逻辑非常清晰,可以分为六个步骤,我在代码注释中已经详细标注了。接下来,我们将深入最核心的 do_filp_open,看看文件是如何被真正'打开'的。


4. 核心揭秘:do_filp_open 与路径解析

4.1 打开操作的入口

do_filp_open 是文件打开的核心实现,它负责协调路径解析和文件创建。让我深入分析这个关键函数的源码:

// fs/namei.c
struct file *do_filp_open(int dfd, struct filename *pathname, const struct open_flags *op) {
    struct nameidata nd;   // 路径解析的核心数据结构
    struct file *filp;
    int error;

    // 初始化 nameidata 结构体
    // nameidata 是"namei data"的缩写,namei 是"name to inode"的缩写
    // 这个结构体贯穿整个路径解析过程,存储中间状态和结果
    nd.mnt = NULL;         // 当前挂载点
    nd.dentry = NULL;      // 当前 dentry
    nd.flags = op->lookup_flags; // 查找标志

    // ===== 核心:执行路径解析和文件打开 =====
    // path_openat 是实际干活的地方
    // 它会解析路径、查找/创建 inode、创建 struct file
    error = path_openat(dfd, pathname, &nd, op);
    if(IS_ERR_VALUE(error)) {
        // 路径解析失败,返回错误指针
        return ERR_PTR(error);
    }
    // 成功!从 nameidata 中获取创建的 struct file
    filp = nd.filp;

    // ===== 后续处理 =====
    // 如果设置了 O_TRUNC 标志,需要截断文件
    if(op->open_flag & O_TRUNC) {
        error = handle_truncate(filp);
        if(error) {
            // 截断失败,释放 file 结构体
            fput(filp);
            return ERR_PTR(error);
        }
    }
    return filp;
}

这个函数虽然代码量不大,但每一个调用都隐藏着复杂的逻辑。接下来,我将深入分析 path_openat,这是路径解析的核心实现。

4.2 路径解析的核心:path_openat

path_openat 是路径解析的核心函数,它负责将路径字符串(如 /home/user/test.txt)转换为内核中的 inode 对象。让我详细分析这个函数:

// fs/namei.c
static int path_openat(int dfd, struct filename *name, struct nameidata *nd, const struct open_flags *op) {
    struct file *base = NULL;
    struct dentry *dentry;
    struct path path;
    int error;

    // ===== 步骤 1:初始化查找标志 =====
    // 设置查找标志,如 LOOKUP_FOLLOW(跟随符号链接)等
    nd->flags |= op->lookup_flags;

    // ===== 步骤 2:获取起始路径 =====
    // 根据 dfd(目录文件描述符)确定路径解析的起点
    // - 如果 dfd == AT_FDCWD,从当前工作目录开始
    // - 如果路径以 / 开头,从根目录开始
    // - 否则,从 dfd 指定的目录开始
    error = path_init(dfd, nd->flags, name, nd);
    if(error) return error;

    // ===== 步骤 3:逐级解析路径 =====
    // 这是一个循环,每次处理路径的一个分量(component)
    // 例如:/home/user/test.txt 会被分解为:""、"home"、"user"、"test.txt"
    while(!(nd->flags & LOOKUP_ROOT)) {
        // 3.1 获取下一个路径分量
        // path_walk 会解析路径字符串,返回下一个分量
        const char *s = path_walk(name->name, nd);
        if(IS_ERR(s)) {
            error = PTR_ERR(s);
            break;
        }
        // 如果已经到达路径末尾,跳出循环
        if(!*s) break;

        // 3.2 查找当前分量的 dentry
        // 首先在 dentry cache 中查找
        error = lookup_fast(nd, &path, &inode);
        if(unlikely(error)) {
            // cache 未命中,进行慢速查找(可能需要读取磁盘)
            error = lookup_slow(nd, &path);
        }
        if(error) break;

        // 3.3 处理符号链接
        // 如果当前分量是符号链接,需要跟随链接
        if(nd->flags & LOOKUP_FOLLOW) {
            error = follow_link(&path, nd);
            if(error) break;
        }
        // 3.4 继续解析下一个分量
        nd->path = path;
    }

    // ===== 步骤 4:创建或打开文件 =====
    if(!error) {
        // 4.1 获取最终的 dentry
        // 如果文件不存在且设置了 O_CREAT,会创建新的 dentry
        dentry = lookup_create(nd, op->open_flag);
        // 4.2 分配 struct file
        // alloc_empty_file 会分配一个 file 结构体,并进行基本初始化
        nd->filp = alloc_empty_file(op->open_flag, current_cred());
        if(IS_ERR(nd->filp)) {
            error = PTR_ERR(nd->filp);
            goto out_dput;
        }
        // 4.3 调用具体文件系统的 open 方法
        // 这是 VFS 多态的核心体现
        // 对于 ext4 文件系统,会调用 ext4_file_open
        // 对于 procfs,会调用 proc_file_open
        error = do_open(nd->filp, dentry, op);
        if(error) {
            // 打开失败,释放 file 结构体
            fput(nd->filp);
            nd->filp = NULL;
        }
    }
out_dput:
    dput(dentry);
    path_cleanup(nd);
    return error;
}

这个函数是路径解析的核心,它展示了 Linux 内核如何将一个路径字符串转换为一组内核数据结构(dentry、inode、file)。


5. 核心数据结构源码解析

理解 open() 的实现,必须深入理解五个核心数据结构。让我逐一分析它们的源码:

5.1 task_struct:进程控制块

task_struct 是 Linux 内核中描述一个进程的核心结构体。其中与文件相关的字段是 files:

// include/linux/sched.h
struct task_struct {
    // ... 大量其他字段 ...
    // 进程 ID
    pid_t pid;
    pid_t tgid;
    // 进程名称
    char comm[TASK_COMM_LEN];
    // 内存描述符(虚拟地址空间)
    struct mm_struct *mm;
    // ★★★ 打开文件表 ★★★
    // files_struct 包含了该进程打开的所有文件
    struct files_struct *files;
    // 文件系统信息(根目录、当前工作目录)
    struct fs_struct *fs;
    // ... 大量其他字段 ...
};

task_struct->files 指向一个 files_struct 结构体,后者包含了该进程打开的所有文件的信息。

5.2 files_struct 与 fdtable:打开文件表

files_struct 是进程的打开文件表,它管理着该进程的所有文件描述符:

// include/linux/fdtable.h
struct fdtable {
    unsigned int max_fds;          // 当前 fd_array 的最大容量
    struct file __rcu **fd_array;  // ★★★ fd 数组,核心!★★★
                                   // fd_array[fd] 指向对应的 struct file
    unsigned long *close_on_exec;  // exec 时关闭的 fd 位图
    unsigned long *open_fds;       // 已打开的 fd 位图
};

// include/linux/file.h
struct files_struct {
    atomic_t count;                // 引用计数(共享此表的进程数)
    struct fdtable __rcu *fdt;     // ★ 指向 fdtable(动态扩展)★
    struct fdtable fdtab;          // 内嵌的小容量 fdtable(优化)
    spinlock_t file_lock;          // 保护并发访问的自旋锁
    int next_fd;                   // 下一个可能空闲的 fd(优化搜索)
};

关键理解:

  • files_struct 是每个进程独有的(除非通过 clone(CLONE_FILES) 共享)
  • fdtable 存储了 fd 到 struct file 的映射
  • fd_array 是一个指针数组,fd_array[fd] 指向对应的 struct file

5.3 struct file:打开的文件对象

struct file 代表一次打开操作。同一个文件被多次打开,会产生多个 struct file:

// include/linux/fs.h
struct file {
    // ===== 文件位置和状态 =====
    loff_t f_pos;                  // ★ 当前文件读写位置(字节偏移)★
    fmode_t f_mode;                // 打开模式(读/写/读写)
    unsigned int f_flags;          // 打开标志(O_APPEND, O_NONBLOCK 等)
    // ===== 核心关联指针 =====
    struct inode *f_inode;         // ★ 指向 inode(文件元数据)★
    struct dentry *f_dentry;       // ★ 指向 dentry(路径缓存)★
    struct vfsmount *f_vfsmnt;     // 指向挂载点
    // ===== 操作函数表(VFS 多态核心)=====
    const struct file_operations *f_op; // ★★★ 文件操作函数表 ★★★
                                        // read, write, mmap, poll...
    // ===== 引用计数和状态 =====
    atomic_t f_count;              // 引用计数(fget/fput)
    struct address_space *f_mapping; // 页面缓存映射
    // ... 其他字段(锁、私有数据等)
};

关键理解:

  • struct file 是'一次打开'的抽象,同一个文件多次打开会产生多个 struct file
  • 但所有 struct file 都指向同一个 inode(共享文件元数据)
  • f_pos 是每个打开独立的,这就是为什么多个进程可以同时读写同一个文件的不同位置

5.4 inode 与 dentry:文件的'灵魂'与'导航'

inode:文件的元数据
// include/linux/fs.h
struct inode {
    // ===== 唯一标识 =====
    umode_t i_mode;                // 文件类型和权限
    kuid_t i_uid;                  // 文件所有者 UID
    kgid_t i_gid;                  // 文件所属组 GID
    unsigned long i_ino;           // ★★★ inode 编号(在分区中唯一)★★★
    // ===== 文件大小和时间 =====
    loff_t i_size;                 // 文件大小(字节)
    struct timespec64 i_atime;     // 最后访问时间
    struct timespec64 i_mtime;     // 最后修改时间
    struct timespec64 i_ctime;     // 最后状态改变时间
    // ===== 数据块定位 =====
    struct address_space *i_mapping; // 页面缓存映射
    // ===== 引用计数和状态 =====
    atomic_t i_count;              // 引用计数
    unsigned int i_nlink;          // 硬链接数
    unsigned long i_state;         // 状态标志
    // ===== 操作函数表 =====
    const struct inode_operations *i_op; // inode 操作
};
dentry:路径解析的缓存
// include/linux/dcache.h
struct dentry {
    // ===== 关键关联 =====
    struct inode *d_inode;         // ★ 指向对应的 inode
    struct dentry *d_parent;       // ★ 指向父目录的 dentry
    // ===== 标识信息 =====
    struct qstr d_name;            // ★ 文件名(qstr = quick string)
    unsigned int d_flags;          // dentry 标志
    // ===== 缓存组织 =====
    struct hlist_bl_node d_hash;   // 哈希表节点(用于 dentry cache 查找)
    struct list_head d_lru;        // LRU 链表节点(用于缓存回收)
    // ===== 引用计数 =====
    atomic_t d_count;              // 引用计数
    int d_lockref;                 // 锁和引用计数
};

关键理解:

  • dentry 是内核的'导航仪',它将'文件名'映射到'inode'
  • dentry cache(dcache)缓存了这些映射,避免每次都从磁盘读取目录
  • dentry 形成了一个树状结构(通过 d_parent),反映了文件系统的目录结构

6. 总结:open() 的本质

经过这篇长文的探索,让我们总结 open() 的本质:

image.png

6.1 open() 做了什么?

open() 的本质是:建立一条从'文件描述符'到'磁盘文件'的通路。

这条通路涉及多个数据结构的协作:

用户空间:fd (int) ↓ 系统调用 内核空间:fd_array[fd] → struct file → f_inode → inode (文件元数据) → f_dentry → dentry (路径缓存) → f_op → file_operations (操作函数表) 

6.2 为什么要这么多数据结构?

数据结构解决的问题设计思想
task_struct如何表示一个进程进程是资源分配的基本单位
files_struct进程如何管理打开的文件封装进程的文件访问能力
fdtable如何快速将 fd 映射到 file数组索引,O(1) 查找
struct file如何表示'一次打开'每次打开是独立的,有自己的状态和位置
inode如何表示文件的'本质'元数据和数据块位置
dentry如何加速路径解析缓存文件名到 inode 的映射

6.3 核心设计思想

1. 分层与抽象
  • VFS 层提供统一接口,不关心底层是哪个文件系统
  • 具体文件系统(ext4/xfs/btrfs)实现 VFS 接口
  • 设备驱动处理硬件细节
2. 缓存加速
  • dentry cache:避免重复路径解析
  • inode cache:避免重复读取元数据
  • page cache:避免重复读取文件内容
3. 引用计数与生命周期管理
  • 每个共享资源都有引用计数
  • 当引用归零时,资源被安全释放
  • 实现资源共享与安全回收的平衡

7. 结语:打开文件,打开一扇门

'The art of progress is to preserve order amid change and to preserve change amid order.'
—— Alfred North Whitehead

当我们写下 int fd = open("test.txt", O_RDONLY);,表面上是简单的函数调用,实际上却是打开了一扇通往操作系统内核深处的大门。

这扇门的背后:

  • 有 VFS 的优雅抽象,让万千设备都化作'文件'
  • 有 缓存 的精妙设计,让慢速磁盘也能飞速响应
  • 有 引用计数 的谨慎管理,在共享与独占间寻找平衡
  • 有 分层架构 的智慧,让复杂系统也能井然有序

理解 open() 的实现,不仅是学习一个系统调用的工作原理,更是领悟一种设计哲学:在复杂度面前,如何用分层、抽象、缓存等手段,构建出既高效又可靠的系统。

希望这篇文章能帮助你真正理解 Linux 文件系统的精髓。下次当你写下 open() 时,愿你能想起这一趟内核之旅,想起那些数据结构体的精密协作,想起 VFS 层的优雅调度——想起这扇通往操作系统深处的门。

Keep exploring, keep learning.


目录

  1. 1. 前言:一次 open() 调用背后的史诗
  2. 2. 全景概览:open() 调用的七个阶段
  3. 2.1 七个阶段概览
  4. 2.2 核心数据结构关系
  5. 3. 深入内核:从 sysopen 到 dosys_open
  6. 3.1 系统调用的入口
  7. 3.2 dosysopen:协调者
  8. 4. 核心揭秘:dofilpopen 与路径解析
  9. 4.1 打开操作的入口
  10. 4.2 路径解析的核心:path_openat
  11. 5. 核心数据结构源码解析
  12. 5.1 task_struct:进程控制块
  13. 5.2 files_struct 与 fdtable:打开文件表
  14. 5.3 struct file:打开的文件对象
  15. 5.4 inode 与 dentry:文件的“灵魂”与“导航”
  16. inode:文件的元数据
  17. dentry:路径解析的缓存
  18. 6. 总结:open() 的本质
  19. 6.1 open() 做了什么?
  20. 6.2 为什么要这么多数据结构?
  21. 6.3 核心设计思想
  22. 1. 分层与抽象
  23. 2. 缓存加速
  24. 3. 引用计数与生命周期管理
  25. 7. 结语:打开文件,打开一扇门
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • VMware 虚拟机共享文件夹挂载指南(Ubuntu 示例)
  • VSCode 远程 SSH 连接下 Copilot 无法使用 Claude 及 Agent 模式的解决方案
  • Visual Studio 与 MinGW 查看 C++ 编译器版本信息
  • Git 版本控制核心命令与实战指南
  • AI 普及时代,普通人如何靠差异化突围?
  • Python 实现 MCP 客户端调用高德地图天气查询示例
  • Copilot 四大模式详解:Ask、Edit、Agent、Plan 核心区别
  • 车端部署DeepSeek-R1蒸馏模型:做法与取舍
  • 金仓数据库 V9 深度评测:融合架构与 AI 实战
  • Ubuntu 18 + ROS Melodic 配置 XTDrone/Gazebo 无人机仿真及 Mid360 模型集成
  • PyTorch 文本引导图像生成与 Stable Diffusion 实践
  • 使用 wxauto 结合 AI 大模型实现微信聊天机器人
  • 高可靠性电子产品 FPGA 逻辑设计
  • GPU 共享技术深度剖析与总结
  • Spring AI Alibaba 快速入门指南
  • 基于 Trae Solo 模式 2 小时开发微信小程序实战
  • C++ 类与对象进阶:默认成员函数与操作符重载
  • 人工智能该如何学习?详细的AI学习路线与资料推荐
  • 深入浅出 PID 算法:原理、实现与应用实战
  • 前端精确数字运算解决方案:使用 BigNumber.js 处理 JavaScript 精度问题

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online