C++ 与 Linux 基础:进程打开磁盘文件的内核实现与源码解析
进程调用 open() 系统调用时,内核经历从用户态到内核态切换、参数解析、文件描述符分配、路径解析及 inode 查找等七个关键阶段。文章通过源码分析展示了 task_struct、files_struct、fdtable、struct file、inode 及 dentry 等核心数据结构的协作机制,阐述了 VFS 分层抽象、缓存加速及引用计数的设计思想,揭示了文件描述符与磁盘文件建立连接的本质过程。

进程调用 open() 系统调用时,内核经历从用户态到内核态切换、参数解析、文件描述符分配、路径解析及 inode 查找等七个关键阶段。文章通过源码分析展示了 task_struct、files_struct、fdtable、struct file、inode 及 dentry 等核心数据结构的协作机制,阐述了 VFS 分层抽象、缓存加速及引用计数的设计思想,揭示了文件描述符与磁盘文件建立连接的本质过程。

在之前的文章中,我们已经深入探讨了文件在磁盘上的静态存储结构(ext2 文件系统),也了解了 VFS 如何通过'一切皆文件'的哲学统一了各种设备。今天,我们将把目光聚焦在一个看似简单却暗藏玄机的问题上:当进程调用 open() 时,内核究竟发生了什么?
想象一下:你写下一行代码 int fd = open("test.txt", O_RDONLY);,短短几个字符,却触发了一场跨越用户空间与内核空间、贯穿磁盘与内存的'信息接力'。
学习完这篇文章,你将彻底理解:
task_struct、files_struct、fdtable、struct file、inode、dentry 是如何协作的这是一场关于'连接'的旅程——连接用户与内核、连接路径与 inode、连接进程与文件。
一次 open() 调用,内核经历了七个关键阶段。在深入源码之前,让我们先建立整体认知。
| 阶段 | 名称 | 核心函数/操作 | 作用 |
|---|---|---|---|
| 1 | 系统调用入口 | SYSCALL_DEFINE3(open) | 从用户态陷入内核态 |
| 2 | 参数解析 | build_open_flags() | 解析 flags 和 mode |
| 3 | 分配 fd | get_unused_fd_flags() | 在 fdtable 中找到空闲 fd |
| 4 | 路径解析 | do_filp_open() → path_openat() | 解析路径,查找/创建 inode |
| 5 | 创建 file | alloc_empty_file() | 创建 struct file 结构体 |
| 6 | 关联 fd 与 file | fd_install() | 将 file 指针填入 fd_array[fd] |
| 7 | 返回用户态 | 系统调用返回 | 将 fd 返回给用户程序 |
在深入源码之前,我们需要理解五个核心数据结构之间的关系:
task_struct (进程) └── files (files_struct) └── fdt (fdtable) └── fd_array[] └── struct file ├── f_inode → inode └── f_dentry → dentry
这个关系链是理解 open() 实现的核心。接下来,我们将从源码层面深入分析每个阶段。
当用户程序调用 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 中。
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)) {
put_unused_fd(fd);
fd = PTR_ERR(f);
} {
fd_install(fd, f);
}
}
putname(tmp);
fd;
}
这个函数的逻辑非常清晰,可以分为六个步骤,我在代码注释中已经详细标注了。接下来,我们将深入最核心的 do_filp_open,看看文件是如何被真正'打开'的。
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,这是路径解析的核心实现。
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
error = lookup_fast(nd, &path, &inode);
(unlikely(error)) {
error = lookup_slow(nd, &path);
}
(error) ;
(nd->flags & LOOKUP_FOLLOW) {
error = follow_link(&path, nd);
(error) ;
}
nd->path = path;
}
(!error) {
dentry = lookup_create(nd, op->open_flag);
nd->filp = alloc_empty_file(op->open_flag, current_cred());
(IS_ERR(nd->filp)) {
error = PTR_ERR(nd->filp);
out_dput;
}
error = do_open(nd->filp, dentry, op);
(error) {
fput(nd->filp);
nd->filp = ;
}
}
out_dput:
dput(dentry);
path_cleanup(nd);
error;
}
这个函数是路径解析的核心,它展示了 Linux 内核如何将一个路径字符串转换为一组内核数据结构(dentry、inode、file)。
理解 open() 的实现,必须深入理解五个核心数据结构。让我逐一分析它们的源码:
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 结构体,后者包含了该进程打开的所有文件的信息。
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 filestruct 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 filestruct file 都指向同一个 inode(共享文件元数据)f_pos 是每个打开独立的,这就是为什么多个进程可以同时读写同一个文件的不同位置// 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;
};
// 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),反映了文件系统的目录结构经过这篇长文的探索,让我们总结 open() 的本质:
open() 的本质是:建立一条从'文件描述符'到'磁盘文件'的通路。
这条通路涉及多个数据结构的协作:
用户空间:fd (int)
↓ 系统调用
内核空间:fd_array[fd] → struct file → f_inode → inode (文件元数据) → f_dentry → dentry (路径缓存) → f_op → file_operations (操作函数表)
| 数据结构 | 解决的问题 | 设计思想 |
|---|---|---|
task_struct | 如何表示一个进程 | 进程是资源分配的基本单位 |
files_struct | 进程如何管理打开的文件 | 封装进程的文件访问能力 |
fdtable | 如何快速将 fd 映射到 file | 数组索引,O(1) 查找 |
struct file | 如何表示'一次打开' | 每次打开是独立的,有自己的状态和位置 |
inode | 如何表示文件的'本质' | 元数据和数据块位置 |
dentry | 如何加速路径解析 | 缓存文件名到 inode 的映射 |
dentry cache:避免重复路径解析inode cache:避免重复读取元数据page cache:避免重复读取文件内容'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);,表面上是简单的函数调用,实际上却是打开了一扇通往操作系统内核深处的大门。
这扇门的背后:
理解 open() 的实现,不仅是学习一个系统调用的工作原理,更是领悟一种设计哲学:在复杂度面前,如何用分层、抽象、缓存等手段,构建出既高效又可靠的系统。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online