
万字解析虚拟文件系统读写open调用
本篇文章记录笔者阅读linux 6.1内核的文件读写和open系统调用的笔记,关于虚拟文件系统整体框架和读写open系统调用的核心思路也可以去阅读笔者之前的文章:kidwjb的小站-虚拟文件系统(VFS)
进程的文件管理
虚拟文件系统最最重要的思想就是:linux下一切皆文件!
不论我们是访问的普通文件或是设备,在系统的角度来讲都是访问的文件
struct file结构体
而linux对于文件的管理,在最上层的就是struct file结构体,它包含了文件的内容和属性
struct file {
union {
struct llist_node f_llist;
struct rcu_head f_rcuhead;
unsigned int f_iocb_flags;
};
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct hlist_head *f_ep;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
errseq_t f_sb_err; /* for syncfs */
} __randomize_layout
__attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
struct file_handle {
__u32 handle_bytes;
int handle_type;
/* file identifier */
unsigned char f_handle[];
};
在这里包含有path结构体(包含dentry和vfsmount),inode,具体文件操作file_operations,文件标志f_flags,文件的模式f_mode等等
而在一个进程中会维护一个全局的struct file数组,用于统一管理进程打开的文件,当然为了方便管理在原本struct file的基础上又进一步进行了封装:
在task_struct中有一个成员:
/* Open file information: */
struct files_struct *files;
struct files_struct结构体
进程通过一个files_struct结构体去维护它当前打开的文件
/*
* Open file table structure
*/
struct files_struct {
/*
* read mostly part
*/
atomic_t count;
bool resize_in_progress;
wait_queue_head_t resize_wait;
struct fdtable __rcu *fdt;
struct fdtable fdtab;
/*
* written part on a separate cache line in SMP
*/
spinlock_t file_lock ____cacheline_aligned_in_smp;
unsigned int next_fd;
unsigned long close_on_exec_init[1];
unsigned long open_fds_init[1];
unsigned long full_fds_bits_init[1];
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};
在现在我们主要关注这个几个成员:
struct fdtable __rcu *fdt;
struct fdtable fdtab;
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
在 files_struct里面有一个存放file指针的数组fd_array
而fdtable是用于存放指向fd_array的指针以及其他对于file指针的数组的描述信息
struct fdtable结构体
struct fdtable {
unsigned int max_fds;
struct file __rcu **fd; /* current fd array */
unsigned long *close_on_exec;
unsigned long *open_fds;
unsigned long *full_fds_bits;
struct rcu_head rcu;
};
在该结构体内部有一个二级file指针fd,他指向一个file指针的数组
当进程被创建时(fork 或 exec),files_struct 被初始化:
fdt(当前文件描述符表指针)指向&fdtab(内嵌的备用表)。fdtab.fd(即fdtable结构体里的**fd成员)被赋值为fd_array的首地址。fdtab.max_fds被设置为NR_OPEN_DEFAULT(在 x86_64 上通常是 64)。
所以,此时 fdtable 中的 fd 数组指针,指向的正是 files_struct 末尾的 fd_array[64]。它们是同一块栈/堆内存。
肯定很多读者可能有疑问:为什么本来files_struct就有fd_array,还要单独定义一个
fdtable结构体让它里面的fd指针指向fd_array?这是因为存在扩容机制
文件打开上限扩容
如果进程打开的文件描述符超过了 64 个(现代服务器进程很容易超过),内核需要扩容:
- 内核会调用
alloc_fdtable()申请一块更大的、全新的struct fdtable和配套的struct file **新数组(大小可能是 128、256 或更大)。 - 将旧
fdtab中的fd_array里的文件指针拷贝到新数组里。 - 将
files_struct中的fdt(指针)指向新申请的fdtable。 - 此时,新
fdtable中的fd指针指向的是新申请的大块内存(通常来自kmalloc或vmalloc),而不再是那个只有 64 个槽位的fd_array。 - 原有
fd_array里剩余的文件指针在扩容时会被清空(置为NULL)。 - 当所有文件关闭,进程准备销毁
files_struct时,内核会检查fdt是否指向&fdtab。 - 如果指向的是自己内嵌的
fdtab,不需要释放(因为是结构体的一部分,随files_struct一起销毁)。 - 如果指向的是外部申请的
fdtable,则需要调用kfree释放掉那块大内存。
初始init的files_struct结构体
struct files_struct init_files = {
.count = ATOMIC_INIT(1),
.fdt = &init_files.fdtab,
.fdtab = {
.max_fds = NR_OPEN_DEFAULT,
.fd = &init_files.fd_array[0],
.close_on_exec = init_files.close_on_exec_init,
.open_fds = init_files.open_fds_init,
.full_fds_bits = init_files.full_fds_bits_init,
},
.file_lock = __SPIN_LOCK_UNLOCKED(init_files.file_lock),
.resize_wait = __WAIT_QUEUE_HEAD_INITIALIZER(init_files.resize_wait),
};
从这个init_files结构体我们也能看出来,最开始的fdt指向了fdtab
总结图

ksys_read
在上层调用了read函数之后,触发系统调用从用户态转为内核态,最终会调用到内核虚拟文件系统的read函数
// fs/read_write.c
ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos, *ppos = file_ppos(f.file);
if (ppos) {
pos = *ppos;
ppos = &pos;
}
ret = vfs_read(f.file, buf, count, ppos);
if (ret >= 0 && ppos)
f.file->f_pos = pos;
fdput_pos(f);
}
return ret;
}
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
return ksys_read(fd, buf, count);
}
获取file结构体
获取file结构体使用的是fdget_pos函数,这个函数返回的是一个struct fd结构体(注意不是文件描述符fd),这个结构体里面含有struct file指针和标志为flags
// include/linux/fdtable.h
struct fd {
struct file *file;
unsigned int flags;
};
struct fd f = fdget_pos(fd);
fdget_pos函数的定义如下:
// include/linux/file.h
static inline struct fd fdget_pos(int fd)
{
return __to_fd(__fdget_pos(fd));
}
这里是两个函数组合,我们首先来看__fdget_pos函数
__fdget_pos
// fs/file.c
unsigned long __fdget_pos(unsigned int fd)
{
unsigned long v = __fdget(fd);
struct file *file = (struct file *)(v & ~3);
if (file && file_needs_f_pos_lock(file)) {
v |= FDPUT_POS_UNLOCK;
mutex_lock(&file->f_pos_lock);
}
return v;
}
传入参数是文件描述符fd,在函数最开始调用的是__fdget,返回值是一个unsigned long无符号长整形,至于为什么会返回这个类型并且下面file指针指向的操作那么奇怪,我们后续讨论,让我们先进入__fdget查看它的实现
unsigned long __fdget(unsigned int fd)
{
return __fget_light(fd, FMODE_PATH);
}
可以看到底层调用的是__fget_light
__fget_light
static unsigned long __fget_light(unsigned int fd, fmode_t mask)
{
struct files_struct *files = current->files;
struct file *file;
/*
* If another thread is concurrently calling close_fd() followed
* by put_files_struct(), we must not observe the old table
* entry combined with the new refcount - otherwise we could
* return a file that is concurrently being freed.
*
* atomic_read_acquire() pairs with atomic_dec_and_test() in
* put_files_struct().
*/
if (atomic_read_acquire(&files->count) == 1) {
file = files_lookup_fd_raw(files, fd);
if (!file || unlikely(file->f_mode & mask))
return 0;
return (unsigned long)file;
} else {
file = __fget(fd, mask);
if (!file)
return 0;
return FDPUT_FPUT | (unsigned long)file;
}
}
首先获取得到当前进程的files_struct指针,然后会去进行判断当前这个files_struct是否是私有的:
- 如果文件表是独占的(
files->count == 1),它直接返回裸指针(unsigned long)file(此时低 2 位为0)。 - 如果文件表是共享的,它会调用
__fget增加引用计数,并返回FDPUT_FPUT | (unsigned long)file(即强置 bit 0 为 1)。
files_lookup_fd_raw(独占情况)
// include/linux/fdtable.h
/*
* The caller must ensure that fd table isn't shared or hold rcu or file lock
*/
static inline struct file *files_lookup_fd_raw(struct files_struct *files, unsigned int fd)
{
struct fdtable *fdt = rcu_dereference_raw(files->fdt);
if (fd < fdt->max_fds) {
fd = array_index_nospec(fd, fdt->max_fds);
return rcu_dereference_raw(fdt->fd[fd]);
}
return NULL;
}
函数主要做的就是首先获取得到当前进程files_struct结构体成员中的 fdtable指针files->fdt,fdt指向了当前进程files_struct结构体成员中的fdtab,也就是struct fdtable实例
- 而
struct fdtable中有一个struct file __rcu **fd成员,指向的是files_struct中的struct file __rcu * fd_array[NR_OPEN_DEFAULT];
然后去判断fd是否符合规范大小,并且直接返回fd_array数组对应的下标fd项的file*指针
可以看到在这里面有两个宏定义
array_index_nospec(fd, fdt->max_fds):这是 Linux 内核为了防御 Spectre V1(边界检查绕过) 漏洞而添加的宏。rcu_dereference_raw()—— 无锁并发读取
注意:为什么上面说的是返回的裸指针低2位为0呢:
核心原理:内存对齐
在 Linux 内核中,struct file 结构体是通过 kmalloc 分配的,返回值在 64 位系统上默认是 8 字节对齐,在 32 位系统上至少是 4 字节对齐。这意味着指针地址的最低 2 个二进制位永远是 0(因为 4 的倍数二进制末尾是 00)。
__fget
__fget函数也是进行了层层封装:
static inline struct file *__fget(unsigned int fd, fmode_t mask)
{
return __fget_files(current->files, fd, mask);
}
static struct file *__fget_files(struct files_struct *files, unsigned int fd,
fmode_t mask)
{
struct file *file;
rcu_read_lock();
file = __fget_files_rcu(files, fd, mask);
rcu_read_unlock();
return file;
}
这里由于是文件是多个线程或进程共享的,所以需要使用RCU锁去获取file *
最终会调用到__fget_files_rcu函数
static inline struct file *__fget_files_rcu(struct files_struct *files,
unsigned int fd, fmode_t mask)
该函数其实也是和上面files_lookup_fd_raw同样的获取方法,去当前进程管理的file *数组里面根据fd下标获取得到对应文件的file *
for (;;) {
struct file *file;
struct fdtable *fdt = rcu_dereference_raw(files->fdt);
struct file __rcu **fdentry;
if (unlikely(fd >= fdt->max_fds))
return NULL;
fdentry = fdt->fd + array_index_nospec(fd, fdt->max_fds);
file = rcu_dereference_raw(*fdentry);
if (unlikely(!file))
return NULL;
if (unlikely(file->f_mode & mask))
return NULL;
但是由于RCU机制以及多个task可能在同一时间对这个文件进行了操作,导致拿到的是已经过时的,所以会进行判断重新获取操作
/*
* Ok, we have a file pointer. However, because we do
* this all locklessly under RCU, we may be racing with
* that file being closed.
*
* Such a race can take two forms:
*
* (a) the file ref already went down to zero,
* and get_file_rcu() fails. Just try again:
*/
if (unlikely(!get_file_rcu(file)))
continue;
/*
* (b) the file table entry has changed under us.
* Note that we don't need to re-check the 'fdt->fd'
* pointer having changed, because it always goes
* hand-in-hand with 'fdt'.
*
* If so, we need to put our ref and try again.
*/
if (unlikely(rcu_dereference_raw(files->fdt) != fdt) ||
unlikely(rcu_dereference_raw(*fdentry) != file)) {
fput(file);
continue;
}
/*
* Ok, we have a ref to the file, and checked that it
* still exists.
*/
return file;
1.首先是get_file_rcu去给file 指针增加引用计数:
- 这里会有失败的场景:就在我们拿到指针的瞬间,另一个 CPU 核心可能正在执行
close(fd)。close会调用fput减少引用计数,如果计数降为 0,文件对象就会被标记为待释放。 get_file_rcu的作用:它不仅仅是对计数执行atomic_inc,而是会先检查*“计数是否已经为 0”*。如果发现计数已经归零,它会返回false。
所以如果返回false,那就说明我们拿到的指针可能随时会被释放回内存池,所以需要重新进行查找
2.其次是验证在增加引用计数确保文件指针不会被释放之后,当前fdt是否还指向原来的fdtable,以及当前的文件描述符表中对应下标的file指针是否和刚才获取得到的file指针相同
增加完引用计数后,我们要确保在这个极短的时间窗口内,文件描述符表的条目没有被人动过。
- 两种具体的调包情况:
- 表被换了(
files->fdt != fdt):另一个线程可能打开了超过 64 个文件,触发了文件表的扩容(Expand),导致整个fdtable结构体被换掉了。 - 槽位被覆盖了(
*fdentry != file):另一个线程调用了dup2(new_fd, fd),强行把这个fd指向了另一个文件对象。
- 表被换了(
只有当上述两个情况都通过才能够成功返回file指针
提取file指针:(v & ~3)操作含义
在上面我们通过__fdget获取得到返回的无符号长整型file指针数值后,会看到下面用一个新的file指针指向这个数值并且与上~3
struct file *file = (struct file *)(v & ~3);
这里操作是为什么呢?
其实在上面提到了:核心原理:内存对齐,struct file至少是四字节对齐,所以指针地址的最低 2 个二进制位永远是 0(因为 4 的倍数二进制末尾是 00)。
又因为在 Linux 内核的系统调用实现中,函数只能返回一个值(一个寄存器宽度)。但是调用者(如 ksys_read)既需要知道 struct file * 指针,又需要知道在退出时如何释放这个文件引用(是需要 fput 减引用计数,还是只需放锁)。
所以内核利用指针天然对齐的特性,将指针和状态标志硬编码塞进了同一个 unsigned long 里。
由于3的二进制是0b0011,取反就是最低两位为0,这样v & ~3就能保证最低两位为0取出纯正的file指针
查看文件是否需要加锁
if (file && file_needs_f_pos_lock(file)) {
v |= FDPUT_POS_UNLOCK;
mutex_lock(&file->f_pos_lock);
}
return v;
- 如果需要加锁(
file_needs_f_pos_lock),它会对&file->f_pos_lock上锁,并执行v |= FDPUT_POS_UNLOCK(将 bit 1 置 1)。 - 最后将带有标记的
v返回。
__to_fd
在最上面的fdget_pos的实现中我们看到,在内部还调用了__to_fd函数, 并且把__fdget_pos的返回值作为参数
让我们来看一下这个函数具体做了什么
static inline struct fd __to_fd(unsigned long v)
{
return (struct fd){(struct file *)(v & ~3),v & 3};
}
可以看到函数是一个内联函数,实现很简单,只有一行代码,因为函数返回值是struct fd,所以这里直接return时候构造一个fd结构体:
struct fd {
struct file *file;
unsigned int flags;
};
结构体的成员只有两项,结合上面对于(v & ~3)的分析,就很清晰的看出来就是把__fdget_pos的返回值包含的file结构体指针和文件的标志位分别填充到结构体里面
获取文件偏移
if (f.file) {
loff_t pos, *ppos = file_ppos(f.file);
if (ppos) {
pos = *ppos;
ppos = &pos;
}
这里首先创建了loff_t(本质上是long long)类型变量pos;以及loff_t类型指针ppos指向file结构体中的f_pos
如果ppos不为空,就说明当前文件有文件偏移;然后赋值file->f_pos保存的偏移量给pos,并且让ppos指向pos地址,这样用于保证后续文件读写操作修改偏移量时不会直接改动file本身保存的偏移量(应对多任务情况)
FMODE_STREAM
/* file_ppos returns &file->f_pos or NULL if file is stream */
static inline loff_t *file_ppos(struct file *file)
{
return file->f_mode & FMODE_STREAM ? NULL : &file->f_pos;
}
这里需要对FMODE_STREAM进行补充:
FMODE_STREAM 是 Linux 内核中一个文件标志,用来标记那些不支持随机访问、只能顺序读写的“流式”文件
不可寻址 (Non-seekable):你不能对它使用 lseek() 系统调用来改变读写位置。
- 顺序读写 (Sequential access):数据像水流一样,只能从当前流位置开始,按照顺序被读取或写入
- 无文件偏移 (No file position):对于这类文件,“当前读写位置”这个概念没有意义
因此,内核在调用其read/write操作时,会传入 NULL 作为位置参数
最典型的“流式”文件包括:
- 管道 (Pipes):包括匿名管道和命名管道(FIFO)
- 套接字 (Sockets):各种类型的 socket
- 终端设备 (TTYs):如
/dev/tty*和伪终端(PTY) - 串口设备 (Serial Ports):如
/dev/ttyS*、/dev/ttyUSB*等 - 其他各种不支持
lseek的字符设备。
调用vfs_read
在上面获取得到file指针和文件偏移pos之后,就正式进入了虚拟文件系统提供的读函数vfs_read
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(buf, count)))
return -EFAULT;
ret = rw_verify_area(READ, file, pos, count);
if (ret)
return ret;
if (count > MAX_RW_COUNT)
count = MAX_RW_COUNT;
if (file->f_op->read)
ret = file->f_op->read(file, buf, count, pos);
else if (file->f_op->read_iter)
ret = new_sync_read(file, buf, count, pos);
else
ret = -EINVAL;
if (ret > 0) {
fsnotify_access(file);
add_rchar(current, ret);
}
inc_syscr(current);
return ret;
}
在函数最开始都是去查看文件是否可读,用户buf是否可以访问,以及教研读写区域是否正常,用户请求的写字节总数是否超过最大支持字节数
然后就会去调用file_operations对应的read函数或是新内核支持的read_iter
这里就做到了虚拟文件系统和底层不同文件系统的解耦,具体的读写函数可以由底层不同的文件系统自己实现
比如笔者自己实现的file_operations:
//文件操作结构体定义
static const struct file_operations myfs_file_ops = {
.read = my_test_read,
.read_iter = generic_file_read_iter,
.write_iter = generic_file_write_iter,
.mmap = generic_file_mmap,
.fsync = noop_fsync,
.llseek = generic_file_llseek,
};
自定义read函数
在一般的驱动程序或是文件系统,都会提供一个read函数,当然对于绝大多数文件系统使用generic_file_read_iter就足够了。generic_file_read_iter 是内核提供的一个非常通用且强大的标准读取函数。它直接操作页缓存 (page cache),几乎所有基于磁盘的常规文件系统(如 ext4, Btrfs 等)都使用它作为自己的 .read_iter 实现。
这里拿笔者写的一个简单的测试函数做分析
ssize_t my_test_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
static const char *str = "you are watching the file\n";
size_t len = strlen(str); // 注意:不包含 '\0',因为普通文件不应输出终止符
printk("my_test_read: pos=%lld, count=%zu\n", *pos, count);
// 1. 检查是否已到文件尾
if (*pos >= len) {
printk("EOF reached\n");
return 0; // 返回 0 表示文件结束
}
// 2. 计算本次能读取的字节数(不能超过用户请求的 count,也不能超过剩余长度)
size_t bytes_to_read = len - *pos;
if (bytes_to_read > count)
bytes_to_read = count;
// 3. 将数据拷贝到用户空间
if (copy_to_user(buf, str + *pos, bytes_to_read)) {
printk("copy_to_user failed\n");
return -EFAULT; // 返回负的错误码
}
// 4. 更新文件偏移
*pos += bytes_to_read;
// 5. 返回实际读取的字节数
printk("read %zu bytes, new pos=%lld\n", bytes_to_read, *pos);
return bytes_to_read;
}
这里有几点需要注意,是笔者当时很久没写驱动忘记了的:
错误地将 copy_to_user 的返回值当作了实际读取的字节数。
copy_to_user的返回值copy_to_user返回未能成功拷贝的字节数。- 如果全部拷贝成功,返回
0。 - 如果有
n个字节拷贝失败,返回n。
- 如果全部拷贝成功,返回
最终实际展示如下:
kidwjb@dshanpi-a1:~/workspace/sourceCode/mySourceCode/myFilesystem$ sudo insmod myfs.ko
kidwjb@dshanpi-a1:~/workspace/sourceCode/mySourceCode/myFilesystem$ sudo mount -t myfs none /mnt/myfs/
kidwjb@dshanpi-a1:~/workspace/sourceCode/mySourceCode/myFilesystem$ sudo touch /mnt/myfs/test
kidwjb@dshanpi-a1:~/workspace/sourceCode/mySourceCode/myFilesystem$ ls /mnt/myfs/test
/mnt/myfs/test
kidwjb@dshanpi-a1:~/workspace/sourceCode/mySourceCode/myFilesystem$ sudo chmod 666 /mnt/myfs/test
kidwjb@dshanpi-a1:~/workspace/sourceCode/mySourceCode/myFilesystem$ cat /mnt/myfs/test
you are watching the file
kidwjb@dshanpi-a1:~/workspace/sourceCode/mySourceCode/myFilesystem$ dmesg | tail -20
[ 5123.591484] myfs: loading out-of-tree module taints kernel.
[ 5123.592877] myfs:filesystem registered succ
[ 5232.502050] myfs:mount succ
[ 5247.722228] myfs:file test created
[ 5300.774026] my_test_read: pos=0, count=131072
[ 5300.774052] read 26 bytes, new pos=26
[ 5300.774085] my_test_read: pos=26, count=131072
[ 5300.774090] EOF reached
ksys_write
接下来我们来看ksys_write函数,函数定义如下:
ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos, *ppos = file_ppos(f.file);
if (ppos) {
pos = *ppos;
ppos = &pos;
}
ret = vfs_write(f.file, buf, count, ppos);
if (ret >= 0 && ppos)
f.file->f_pos = pos;
fdput_pos(f);
}
return ret;
}
可以看到和ksys_read流程一模一样,首先通过文件描述符fd去进程的文件描述表中找到对应的file指针,然后获取当前文件的文件偏移,最后调用vfs_write
不用去看也能猜出来vfs_write做了什么,肯定也是调用f_op->write或者f_op->write_iter
do_sys_open
do_sys_open是open系统调用函数
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
// fs/open.c
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_how how = build_open_how(flags, mode);
return do_sys_openat2(dfd, filename, &how);
}
build_open_how
该函数用于构造并返回一个open_how结构体,主要是规范用户程序传来的flags和mode参数
// fs/open.c
inline struct open_how build_open_how(int flags, umode_t mode)
{
struct open_how how = {
.flags = flags & VALID_OPEN_FLAGS,
.mode = mode & S_IALLUGO,
};
/* O_PATH beats everything else. */
if (how.flags & O_PATH)
how.flags &= O_PATH_FLAGS;
/* Modes should only be set for create-like flags. */
if (!WILL_CREATE(how.flags))
how.mode = 0;
return how;
}
- 过滤标志:
how.flags = flags & VALID_OPEN_FLAGS;首先只保留内核认可的合法标志。 - 过滤模式:
how.mode = mode & S_IALLUGO;只保留mode中的有效权限位。 - 特殊处理
O_PATH:如果设置了O_PATH,则将how.flags重置为仅包含O_PATH_FLAGS中定义的标志。这是因为O_PATH的行为特殊,其他大多数打开标志对它没有意义 - 条件性清除
mode:如果how.flags不会创建文件(即!WILL_CREATE(how.flags)),则将how.mode设置为 0。这是因为文件权限 (mode) 只有在创建新文件时才有效,对于打开已存在的文件,指定mode是没有意义的。
do_sys_openat2
在我们上面获取得到打开文件所需要的正确标志位和模式位之后,就会正式进入打开文件流程,调用的函数是do_sys_openat2
// fs/open.c
static long do_sys_openat2(int dfd, const char __user *filename,
struct open_how *how)
{
struct open_flags op;
int fd = build_open_flags(how, &op);
struct filename *tmp;
if (fd)
return fd;
tmp = getname(filename);
if (IS_ERR(tmp))
return PTR_ERR(tmp);
fd = get_unused_fd_flags(how->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
在最开始调用的就是该函数,它的作用是把用户空间传来的杂乱标志位,翻译、校验、分类后,打包成内核文件系统(VFS)路径查找引擎能看懂的标准结构体 struct open_flags。
inline int build_open_flags(const struct open_how *how, struct open_flags *op)
{
u64 flags = how->flags;
u64 strip = FMODE_NONOTIFY | O_CLOEXEC;
int lookup_flags = 0;
int acc_mode = ACC_MODE(flags);
由于函数很长且都是对于标志位的处理,这里就不做过多分析
这个函数就是在给它们分别归类:
- 文件属性类(
O_CREAT,O_TMPFILE,O_TRUNC,O_APPEND)→ 决定文件的创建方式。 - 访问权限类(
O_RDONLY,O_WRONLY,O_RDWR)→ 决定acc_mode。 - 路径解析类(
O_NOFOLLOW,O_DIRECTORY,RESOLVE_BENEATH等)→ 决定lookup_flags。
创建filename结构体
tmp = getname(filename);
if (IS_ERR(tmp))
return PTR_ERR(tmp);
在这里调用到了getname函数去解析传递进入的char *类型指向用户空间传入名字的指针,并且返回一个struct filename结构体指针
filename结构体
/* fs/open.c */
struct audit_names;
struct filename {
const char *name; /* pointer to actual string */
const __user char *uptr; /* original userland pointer */
atomic_t refcnt;
struct audit_names *aname;
const char iname[];
};
name:该指针用于指向实际的存储文件名的字符串,根据后文介绍的getname内部实现来看,最开始默认指向iname(文件名不长的时候)uptr:指向用户空间的名字字符串refcnt:引用计数aname:当前版本的内核对于audit_names相关的似乎都还未实现iname[]:这在C语言中是一个非常特殊且严格的语法——它被称为灵活数组成员必须满足以下两个硬性条件,否则非法:- 必须是结构体的最后一个成员。
- 结构体中必须至少有一个其他命名字段(不能只有这一个数组)。
无法直接定义该结构体的静态或局部变量(会报错),必须使用动态内存分配,并手动为数组预留空间
getname_flags
// fs/namei.c
struct filename *
getname(const char __user * filename)
{
return getname_flags(filename, 0, NULL);
}
getname函数调用的是getname_flags,传递参数flags是0,empty为NULL
struct filename *
getname_flags(const char __user *filename, int flags, int *empty)
{
struct filename *result;
char *kname;
int len;
result = audit_reusename(filename);
if (result)
return result;
result = __getname();
if (unlikely(!result))
return ERR_PTR(-ENOMEM);
/*
* First, try to embed the struct filename inside the names_cache
* allocation
*/
kname = (char *)result->iname;
result->name = kname;
len = strncpy_from_user(kname, filename, EMBEDDED_NAME_MAX);
if (unlikely(len < 0)) {
__putname(result);
return ERR_PTR(len);
}
/*
* Uh-oh. We have a name that's approaching PATH_MAX. Allocate a
* separate struct filename so we can dedicate the entire
* names_cache allocation for the pathname, and re-do the copy from
* userland.
*/
if (unlikely(len == EMBEDDED_NAME_MAX)) {
const size_t size = offsetof(struct filename, iname[1]);
kname = (char *)result;
/*
* size is chosen that way we to guarantee that
* result->iname[0] is within the same object and that
* kname can't be equal to result->iname, no matter what.
*/
result = kzalloc(size, GFP_KERNEL);
if (unlikely(!result)) {
__putname(kname);
return ERR_PTR(-ENOMEM);
}
result->name = kname;
len = strncpy_from_user(kname, filename, PATH_MAX);
if (unlikely(len < 0)) {
__putname(kname);
kfree(result);
return ERR_PTR(len);
}
if (unlikely(len == PATH_MAX)) {
__putname(kname);
kfree(result);
return ERR_PTR(-ENAMETOOLONG);
}
}
atomic_set(&result->refcnt, 1);
/* The empty path is special. */
if (unlikely(!len)) {
if (empty)
*empty = 1;
if (!(flags & LOOKUP_EMPTY)) {
putname(result);
return ERR_PTR(-ENOENT);
}
}
result->uptr = filename;
result->aname = NULL;
audit_getname(result);
return result;
}
函数的实现有点长,但是其实仔细一看会发现是和前文有一定相似之处的下面我们来逐一分析一下代码(关于audit相关的未实现,先不管):
1.函数首先调用__getname()分配一个filename结构体
// include/linux/fs.h
#define __getname() kmem_cache_alloc(names_cachep, GFP_KERNEL)
#define __putname(name) kmem_cache_free(names_cachep, (void *)(name))
// fs/namei.c
result = __getname();
if (unlikely(!result))
return ERR_PTR(-ENOMEM);
2.从用户空间拷贝到filename结构体中的iname数组
/*
* First, try to embed the struct filename inside the names_cache
* allocation
*/
kname = (char *)result->iname;
result->name = kname;
len = strncpy_from_user(kname, filename, EMBEDDED_NAME_MAX);
if (unlikely(len < 0)) {
__putname(result);
return ERR_PTR(len);
}
这里首先让kname指针指向filename结构体中的iname数组,然后让filename结构体中的name指针也指向这个数组
接下来就是调用strncpy_from_user将用户传递进来的名字字符串拷贝到iname数组。我们会发现这里和上面的struct file结构体有点相似,都是有默认的数组,其中有一个指针初始指向这个数组,并且如果数组大小不足那么就会去新建一个更大的数组,然后指针指向新的数组
3.判断名字长度是否超过默认值,如果超过了就重新分配一个更大的空间
从这里就可以更能看出来,其实在内核中很多设计思想是一致的
/*
* Uh-oh. We have a name that's approaching PATH_MAX. Allocate a
* separate struct filename so we can dedicate the entire
* names_cache allocation for the pathname, and re-do the copy from
* userland.
*/
if (unlikely(len == EMBEDDED_NAME_MAX)) {
const size_t size = offsetof(struct filename, iname[1]);
kname = (char *)result;
/*
* size is chosen that way we to guarantee that
* result->iname[0] is within the same object and that
* kname can't be equal to result->iname, no matter what.
*/
result = kzalloc(size, GFP_KERNEL);
if (unlikely(!result)) {
__putname(kname);
return ERR_PTR(-ENOMEM);
}
result->name = kname;
len = strncpy_from_user(kname, filename, PATH_MAX);
if (unlikely(len < 0)) {
__putname(kname);
kfree(result);
return ERR_PTR(len);
}
if (unlikely(len == PATH_MAX)) {
__putname(kname);
kfree(result);
return ERR_PTR(-ENAMETOOLONG);
}
}
这里大致分析一下上述代码的操作:
(1)首先使用offsetof(这个库函数用于算出从结构体开始到指定成员的字节)得到iname[1]之前的字节数
(2) 让kname指向result开头,这是因为原来的 result 是通过 __getname() 分配的,它其实是一个 PATH_MAX(通常 4096 字节)大小的内存页,结构体 struct filename 只占用了这个页面的开头部分。kname 现在指向这整块大内存的起始地址,它将被腾出来专门当作纯字符串缓冲区(不再当作结构体头)。
(3)重新分配一个只有sized大小的filename结构体
(4)重定向指针,filename中用于指向名字字符串的指针result->name指向原本的4096大小的空间,并且执行拷贝操作
result->name = kname; // 新结构体的 name 指针指向旧的大缓冲区
len = strncpy_from_user(kname, filename, PATH_MAX); // 直接拷贝到旧的大缓冲区(至少有 4096 字节)
4.增加filename的引用计数
atomic_set(&result->refcnt, 1);
5.处理空路径情况
/* The empty path is special. */
if (unlikely(!len)) {
if (empty)
*empty = 1;
if (!(flags & LOOKUP_EMPTY)) {
putname(result);
return ERR_PTR(-ENOENT);
}
}
由于传入参数是0和NULL,所以这里的if都不会执行
6.返回filename结构体
result->uptr = filename;
result->aname = NULL;
audit_getname(result);
return result;
获取一个最小的fd
在完成上面解析标志位和存储文件路径名到filename中后,下一步就是获取一个当前进程的最小文件描述符fd
fd = get_unused_fd_flags(how->flags);
// fs/file.c
int __get_unused_fd_flags(unsigned flags, unsigned long nofile)
{
return alloc_fd(0, nofile, flags);
}
int get_unused_fd_flags(unsigned flags)
{
return __get_unused_fd_flags(flags, rlimit(RLIMIT_NOFILE));
}
最终底层调用的是alloc_fd函数,不过在此之前先让我们来看一下传入__get_unused_fd_flags的第二个参数rlimit(RLIMIT_NOFILE)
资源限制机制rlimit
linux提供资源限制(rlimit)机制,对进程使用系统资源施加某些限制,该机制利用了一个rlim数组,数组项类型为strcut rlimit
// include/linux/sched/signal.h
struct rlimit rlim[RLIM_NLIMITS];
// include/uapi/linux/resource.h
struct rlimit {
__kernel_ulong_t rlim_cur;
__kernel_ulong_t rlim_max;
};
上述定义设计的很通用,因此可以用于许多不同的资源类型
rlim_cur:进程当前的资源限制,也称为软限制rlim_max:该限制的最大容许值,也称为硬限制
在上面代码中RLIMIT_NOFILE它用于限制单个进程所能打开的最大文件描述符(File Descriptor)数量,默认限制在1024
除此以外还有其他限制标志这里不做展开
// include/linux/sched/signal.h
static inline unsigned long rlimit(unsigned int limit)
{
return task_rlimit(current, limit);
}
static inline unsigned long task_rlimit(const struct task_struct *task,
unsigned int limit)
{
return READ_ONCE(task->signal->rlim[limit].rlim_cur);
}
分配文描述符-alloc_fd
alloc_fd用于在当前进程文件描述符表中去寻找一个最小的空闲的fd,函数的定义有点长,但是本质上只是几步的重要操作
// fs/file.c
/*
* allocate a file descriptor, mark it busy.
*/
static int alloc_fd(unsigned start, unsigned end, unsigned flags)
{
struct files_struct *files = current->files;
unsigned int fd;
int error;
struct fdtable *fdt;
spin_lock(&files->file_lock);
repeat:
fdt = files_fdtable(files);
fd = start;
if (fd < files->next_fd)
fd = files->next_fd;
if (fd < fdt->max_fds)
fd = find_next_fd(fdt, fd);
/*
* N.B. For clone tasks sharing a files structure, this test
* will limit the total number of files that can be opened.
*/
error = -EMFILE;
if (fd >= end)
goto out;
error = expand_files(files, fd);
if (error < 0)
goto out;
/*
* If we needed to expand the fs array we
* might have blocked - try again.
*/
if (error)
goto repeat;
if (start <= files->next_fd)
files->next_fd = fd + 1;
__set_open_fd(fd, fdt);
if (flags & O_CLOEXEC)
__set_close_on_exec(fd, fdt);
else
__clear_close_on_exec(fd, fdt);
error = fd;
#if 1
/* Sanity check */
if (rcu_access_pointer(fdt->fd[fd]) != NULL) {
printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
rcu_assign_pointer(fdt->fd[fd], NULL);
}
#endif
out:
spin_unlock(&files->file_lock);
return error;
}
获取当前进程文件描述表
struct files_struct *files = current->files;
unsigned int fd;
int error;
struct fdtable *fdt;
spin_lock(&files->file_lock);
repeat:
fdt = files_fdtable(files);
在这里还有一个goto的标签,是为了后续使用的,这里让fdt指向struct files_struct中的fdtble,并且使引用计数+1
寻找下一个空闲的fd(重要)
fd = start;
if (fd < files->next_fd)
fd = files->next_fd;
这里首先让fd等于扫描开始值start,如果开始的范围比next_fd小,那么就直接从next_fd开始扫描
next_fd 像一个*“水印标记”*。每次成功分配完 fd 后,next_fd 会被更新为 fd + 1。这意味着 next_fd 之前的槽位大概率(但不绝对)是满的。直接从水印处开始找,避免了每次从 0 扫描的低效。
if (fd < fdt->max_fds)
fd = find_next_fd(fdt, fd);
如果fd没有超过当前进程锁有的最大文件描述符表容量,那么就调用find_next_fd在表中查找最小空闲的量
find_next_fd
在了解这个函数的定义如何实现之前,我们需要先去了解一下最开始讲述的fdtable中没有讲的两个成员:
//struct fdtable中的定义
unsigned long *open_fds;
unsigned long *full_fds_bits;
//指向的是struct files中的下面两个数组
unsigned long open_fds_init[1];
unsigned long full_fds_bits_init[1];
open_fds:细粒度位图。一共有max_fds个 bit,每个 bit 对应一个具体的文件描述符(1=占用,0=空闲)。full_fds_bits:粗粒度位图(块级标记)。它的每一个 bit,对应open_fds中的 一个 long 字(通常是 64 个 bit)。- 如果
open_fds中某个 long 字全为 1(即该组 64 个 fd 全部被占用),则full_fds_bits中对应的 bit 置为 1。 - 如果该 long 字中至少有 1 个空闲位(0),则
full_fds_bits中对应的 bit 置为 0。
- 如果
static unsigned int find_next_fd(struct fdtable *fdt, unsigned int start)
{
unsigned int maxfd = fdt->max_fds;
unsigned int maxbit = maxfd / BITS_PER_LONG;
unsigned int bitbit = start / BITS_PER_LONG;
bitbit = find_next_zero_bit(fdt->full_fds_bits, maxbit, bitbit) * BITS_PER_LONG;
if (bitbit > maxfd)
return maxfd;
if (bitbit > start)
start = bitbit;
return find_next_zero_bit(fdt->open_fds, maxfd, start);
}
根据上面的成员解释,其实就比较明了了
第一步(第一次调用):跳过“整组已满”的块:
unsigned int maxbit = maxfd / BITS_PER_LONG; // 总共有多少个 long 组
unsigned int bitbit = start / BITS_PER_LONG; // 从第几组开始找
bitbit = find_next_zero_bit(fdt->full_fds_bits, maxbit, bitbit) * BITS_PER_LONG;
- 执行逻辑:在
full_fds_bits中查找第一个为 0 的 bit(意味着这一组里还有空位)。 - 计算出具体 fd 值:将找到的组索引乘以
BITS_PER_LONG(例如 64),得到该组起始的 fd 编号(比如组 0 对应 fd 0,组 1 对应 fd 64)。 - 目的:如果前面的几百个 fd 全都被占满了,内核不需要逐个 bit 去扫描
open_fds,只需在缩小 64 倍的full_fds_bits上快速跳过这些“全满组”。这是主要的性能优化。
第二步(边界修正):调整搜索起点:
if (bitbit > maxfd) return maxfd; // 如果超过上限,返回最大值(表示没位置了)
if (bitbit > start) start = bitbit; // 将搜索起点调整到未满组的起始位置
- 如果找到的空闲组起始地址(如 64)比你传入的
start(如 50)还大,说明50~63所在的那个 long 字是全满的。此时直接将起点跳到 64,不再回头看 50~63 这些已满的位。
第三步(第二次调用):在“未满组”内精确定位具体 fd:
return find_next_zero_bit(fdt->open_fds, maxfd, start);
执行逻辑:从修正后的 start 开始,在 open_fds 这个巨大的位图中,逐 bit 扫描第一个为 0(空闲)的位置。
- 为什么能保证快:因为通过第一步,我们已经知道当前
start所在的这个 long 组是未满的,所以第二次扫描最多只需要遍历 64 个 bit(即一个 long 字的大小)就能找到答案,而不会出现遍历成千上万个 bit 的最坏情况。
若无空闲则扩容
error = expand_files(files, fd);
if (error < 0)
goto out;
/*
* If we needed to expand the fs array we
* might have blocked - try again.
*/
if (error)
goto repeat;
expand_files函数内部实现这里就不做详细分析了,笔者大致看了一眼,大致就是根据传入的fd作为max_fd,将这个作为参数传递进入,在函数开始是调用alloc_fdtable函数根据传入的fd值分配一个新的fdtable,并且设置内部成员。然后将旧的fdt拷贝到新的fdt中
扩容之后需要重新去获取fdtable然后重新寻找空闲文件描述符
设置next_fd
if (start <= files->next_fd)
files->next_fd = fd + 1;
下一次寻找就可以直接从next_fd,节约时间
在三个位图中置位
__set_open_fd(fd, fdt);
if (flags & O_CLOEXEC)
__set_close_on_exec(fd, fdt);
else
__clear_close_on_exec(fd, fdt);
这里的close_on_exec是一个位图,每一位对应一个文件描述符(fd)。
如果该位 = 1:当进程调用 exec()(执行新程序)时,内核会在加载新程序前自动关闭这个文件描述符。
如果该位 = 0(默认情况):当进程调用 exec() 时,这个文件描述符会被子进程继承并保持打开,新程序可以直接读写这个 fd。
我们来看__set_open_fd:
static inline void __set_open_fd(unsigned int fd, struct fdtable *fdt)
{
__set_bit(fd, fdt->open_fds);
fd /= BITS_PER_LONG;
if (!~fdt->open_fds[fd])
__set_bit(fd, fdt->full_fds_bits);
}
首先是设置fd在open_fds的位置,然后fd /= BITS_PER_LONG获取得到fd所在第几块号,并且如果该块全部位为1,那么就要在full_fds_bits将该块置1
返回fd
error = fd;
#if 1
/* Sanity check */
if (rcu_access_pointer(fdt->fd[fd]) != NULL) {
printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
rcu_assign_pointer(fdt->fd[fd], NULL);
}
#endif
out:
spin_unlock(&files->file_lock);
return error;
do_filp_open
do_filp_open 是 Linux 内核中打开文件操作的“总调度中心”。它不亲自做路径解析或文件节点查找,而是负责搭建上下文(struct nameidata),然后以**“降级重试”**的策略调用核心函数 path_openat。
由于do_filp_open解析起来很长,所以这里单独给一个层级来解析,而不是嵌在do_sys_openat2层级下面
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, NULL);
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;
}
函数主要做的就是两步:首先创建struct nameidata,然后它根据不同的错误码,使用不同的 lookup_flags 重试 path_openat
三次重试机制
它根据不同的错误码,使用不同的 lookup_flags 重试 path_openat:
第一次尝试:RCU 模式(快速路径)
filp = path_openat(&nd, op, flags | LOOKUP_RCU);
- 标志:在原有的
lookup_flags基础上加上LOOKUP_RCU。 - 执行逻辑:在 RCU 读锁保护下进行路径查找,不持有任何引用计数(dget/dput),不阻塞(不能睡眠),完全不触碰文件系统的底层磁盘 I/O(除非 dentry 已在缓存中)。
- 优点:极致的性能,完全无锁竞争,适用于大多数热缓存场景。
- 失败条件:如果路径不在缓存中、需要等待磁盘 I/O、或者需要处理符号链接导致可能需要睡眠,
path_openat会返回-ECHILD(表示“操作不适用于 RCU 模式,请用传统锁重试”)。
第二次尝试:REF 模式(传统慢速路径)
if (unlikely(filp == ERR_PTR(-ECHILD)))
filp = path_openat(&nd, op, flags);
- 触发条件:仅当第一次 RCU 尝试返回
-ECHILD时触发。 - 执行逻辑:去掉
LOOKUP_RCU标志,加锁(持有引用计数),允许阻塞和睡眠。此时会真正走文件系统底层驱动,查询磁盘或 NFS 网络。 - 适用场景:文件不在 dcache 中、路径涉及挂载点切换、或需要实际读取磁盘 inode 的情况。
第三次尝试:REVAL 模式(强制重新验证)
if (unlikely(filp == ERR_PTR(-ESTALE)))
filp = path_openat(&nd, op, flags | LOOKUP_REVAL);
- 触发条件:第二次尝试返回
-ESTALE(Stale File Handle,陈旧文件句柄)。 - 背景:这主要发生在**网络文件系统(NFS)或分布式文件系统**中。客户端缓存的目录项信息与服务器端已不一致(比如文件被服务端删除了或移动了)。
- 执行逻辑:加上
LOOKUP_REVAL标志,强制文件系统绕过缓存,直接与服务器通信重新验证路径的有效性,确保获取最新的目录项信息。
set_nameidata
static void __set_nameidata(struct nameidata *p, int dfd, struct filename *name)
{
struct nameidata *old = current->nameidata;
p->stack = p->internal;
p->depth = 0;
p->dfd = dfd;
p->name = name;
p->path.mnt = NULL;
p->path.dentry = NULL;
p->total_link_count = old ? old->total_link_count : 0;
p->saved = old;
current->nameidata = p;
}
static inline void set_nameidata(struct nameidata *p, int dfd, struct filename *name,
const struct path *root)
{
__set_nameidata(p, dfd, name);
p->state = 0;
if (unlikely(root)) {
p->state = ND_ROOT_PRESET;
p->root = *root;
}
}
这里主要是根据传入的dfd和filename以及是否传入了path来调用__set_nameidata填充nameidata结构体
dfd参数
这里先来将一下之前一直忽略的dfd,在最开始的系统调用宏定义中传入的dfd的值是*AT_FDCWD*。它本质上是一个宏常量,通常定义为 -100,其核心含义是:“请相对于调用进程的当前工作目录(Current Working Directory)来解析路径”。
以 openat() 函数为例,它的原型是:int openat(int dirfd, const char *pathname, int flags, mode_t mode);
- 当
dirfd = AT_FDCWD时:openat(AT_FDCWD, "logs/app.log", O_CREAT);
这行代码的行为完全等价于传统的open("logs/app.log", O_CREAT);。因为AT_FDCWD告诉内核:“不要用任何目录文件描述符,直接用当前进程的工作目录作为起点去查找logs/app.log”。 - 当
pathname是绝对路径时:
如果pathname以/开头(如/etc/passwd),那么dirfd参数会被内核直接忽略,无论你填AT_FDCWD还是其他 fd,结果都一样。
所以内核接收到dfd参数的时候,会做判断
- 如果
dirfd == AT_FDCWD,则从当前进程的fs->pwd(当前工作目录结构体)开始解析路径。 - 如果
dirfd >= 0,则从该文件描述符对应的打开文件结构体(必须是目录)开始解析路径。
__set_nameidata
- 在
__set_nameidata函数中可以看到首先是用一个old指针指向进程当前的nameidata - 填充
dfd和filename - 然后要将原本进程的
nameidata保存到当前结构体中的saved - 最后让当前进程
nameidata指针指向传递进入的nameidata结构体
set_nameidata会配合**restore_nameidata**一起使用,后者的作用是在函数返回前恢复现场的清理工作。这里就像是上下文切换一样,把原本的寄存器值保存到内存中,切换回来时有恢复现场
path_openat
path_openat(重要)
接下来就是最核心的path_openat函数
// fs/namei.c
static struct file *path_openat(struct nameidata *nd,
const struct open_flags *op, unsigned flags)
{
struct file *file;
int error;
file = alloc_empty_file(op->open_flag, current_cred());
if (IS_ERR(file))
return file;
if (unlikely(file->f_flags & __O_TMPFILE)) {
error = do_tmpfile(nd, flags, op, file);
} else if (unlikely(file->f_flags & O_PATH)) {
error = do_o_path(nd, flags, file);
} else {
const char *s = path_init(nd, flags);
while (!(error = link_path_walk(s, nd)) &&
(s = open_last_lookups(nd, file, op)) != NULL)
;
if (!error)
error = do_open(nd, file, op);
terminate_walk(nd);
}
if (likely(!error)) {
if (likely(file->f_mode & FMODE_OPENED))
return file;
WARN_ON(1);
error = -EINVAL;
}
fput(file);
if (error == -EOPENSTALE) {
if (flags & LOOKUP_RCU)
error = -ECHILD;
else
error = -ESTALE;
}
return ERR_PTR(error);
}
分配空的file结构体-alloc_empty_file
struct file *file;
int error;
file = alloc_empty_file(op->open_flag, current_cred());
// fs/file_table.c
struct file *alloc_empty_file(int flags, const struct cred *cred)
{
static long old_max;
struct file *f;
/*
* Privileged users can go above max_files
*/
if (get_nr_files() >= files_stat.max_files && !capable(CAP_SYS_ADMIN)) {
/*
* percpu_counters are inaccurate. Do an expensive check before
* we go and fail.
*/
if (percpu_counter_sum_positive(&nr_files) >= files_stat.max_files)
goto over;
}
f = __alloc_file(flags, cred);
if (!IS_ERR(f))
percpu_counter_inc(&nr_files);
return f;
over:
/* Ran out of filps - report that */
if (get_nr_files() > old_max) {
pr_info("VFS: file-max limit %lu reached\n", get_max_files());
old_max = get_nr_files();
}
return ERR_PTR(-ENFILE);
}
函数首先会做检查查看当前打开的文件总数get_nr_files是否大于等于系统允许最大值并且当前进程没有 CAP_SYS_ADMIN 特权,那么分配失败,返回 -ENFILE 错误
然后调用__alloc_file从缓存中获取一个file内存并在内部进行一些初始化
并且增加全局的文件计数 ,然后返回file指针
struct cred
struct cred 是 Linux 内核中用于表示进程权限和安全上下文的核心结构体。它包含了进行权限检查所需的所有身份信息。
- 记录“打开者”身份:文件打开后,后续所有操作(如
read、write、ioctl)的权限检查,都应基于打开时的凭证,而不是操作发起时的凭证。将凭证保存在struct file中,可以确保权限检查的一致性。
__alloc_file
static struct file *__alloc_file(int flags, const struct cred *cred)
{
struct file *f;
int error;
f = kmem_cache_zalloc(filp_cachep, GFP_KERNEL);
if (unlikely(!f))
return ERR_PTR(-ENOMEM);
f->f_cred = get_cred(cred);
error = security_file_alloc(f);
if (unlikely(error)) {
file_free_rcu(&f->f_rcuhead);
return ERR_PTR(error);
}
atomic_long_set(&f->f_count, 1);
rwlock_init(&f->f_owner.lock);
spin_lock_init(&f->f_lock);
mutex_init(&f->f_pos_lock);
f->f_flags = flags;
f->f_mode = OPEN_FMODE(flags);
/* f->f_version: 0 */
return f;
}
1.首先从专用缓存中分配一个file结构体
f = kmem_cache_zalloc(filp_cachep, GFP_KERNEL);
if (unlikely(!f))
return ERR_PTR(-ENOMEM);
这里使用 kmem_cache_zalloc 而非普通的 kmalloc,是因为 struct file 是内核中一种非常频繁创建和销毁的对象。使用专用的“slab”缓存可以提高分配效率,并减少内存碎片。_zalloc 后缀意味着分配出的内存会被全部清零,确保对象处于一个干净的状态。
2.然后赋值当前进程的进程权限cred并且进行安全检查
f->f_cred = get_cred(cred);
error = security_file_alloc(f);
if (unlikely(error)) {
file_free_rcu(&f->f_rcuhead);
return ERR_PTR(error);
}
3.将file的引用计数+1,初始化读写锁,自选锁,文件偏移互斥锁等,以及将文件标志和打开模式放进file结构体中
atomic_long_set(&f->f_count, 1);
rwlock_init(&f->f_owner.lock);
spin_lock_init(&f->f_lock);
mutex_init(&f->f_pos_lock);
f->f_flags = flags;
f->f_mode = OPEN_FMODE(flags);
- 设置文件模式 (
f_mode):根据传入的flags(如O_RDONLY,O_WRONLY) 计算出文件的访问模式 - 设置文件标志 (
f_flags):将传入的flags(如O_APPEND,O_NONBLOCK等)直接赋给f->f_flags。
获取路径查找起点-path_init
const char *s = path_init(nd, flags);
在上面获取得到一个空的file结构体之后,就会调用path_init去根据nameidata获取路径查找的起点,它的返回值是路径名字符串的指针(const char *s)
注意:这里所说的获取路径查找的起点是指获取起点的path和inode,当然肯定最主要的就是获取得到inode节点。因为知道path之后就可以找到dentry,得到dentry找到了inode
函数的定义如下,还是有点长度的
/* must be paired with terminate_walk() */
static const char *path_init(struct nameidata *nd, unsigned flags)
{
int error;
const char *s = nd->name->name;
/* LOOKUP_CACHED requires RCU, ask caller to retry */
if ((flags & (LOOKUP_RCU | LOOKUP_CACHED)) == LOOKUP_CACHED)
return ERR_PTR(-EAGAIN);
if (!*s)
flags &= ~LOOKUP_RCU;
if (flags & LOOKUP_RCU)
rcu_read_lock();
else
nd->seq = nd->next_seq = 0;
nd->flags = flags;
nd->state |= ND_JUMPED;
nd->m_seq = __read_seqcount_begin(&mount_lock.seqcount);
nd->r_seq = __read_seqcount_begin(&rename_lock.seqcount);
smp_rmb();
if (nd->state & ND_ROOT_PRESET) {
struct dentry *root = nd->root.dentry;
struct inode *inode = root->d_inode;
if (*s && unlikely(!d_can_lookup(root)))
return ERR_PTR(-ENOTDIR);
nd->path = nd->root;
nd->inode = inode;
if (flags & LOOKUP_RCU) {
nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
nd->root_seq = nd->seq;
} else {
path_get(&nd->path);
}
return s;
}
nd->root.mnt = NULL;
/* Absolute pathname -- fetch the root (LOOKUP_IN_ROOT uses nd->dfd). */
if (*s == '/' && !(flags & LOOKUP_IN_ROOT)) {
error = nd_jump_root(nd);
if (unlikely(error))
return ERR_PTR(error);
return s;
}
/* Relative pathname -- get the starting-point it is relative to. */
if (nd->dfd == AT_FDCWD) {
if (flags & LOOKUP_RCU) {
struct fs_struct *fs = current->fs;
unsigned seq;
do {
seq = read_seqcount_begin(&fs->seq);
nd->path = fs->pwd;
nd->inode = nd->path.dentry->d_inode;
nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
} while (read_seqcount_retry(&fs->seq, seq));
} else {
get_fs_pwd(current->fs, &nd->path);
nd->inode = nd->path.dentry->d_inode;
}
} else {
/* Caller must check execute permissions on the starting path component */
struct fd f = fdget_raw(nd->dfd);
struct dentry *dentry;
if (!f.file)
return ERR_PTR(-EBADF);
dentry = f.file->f_path.dentry;
if (*s && unlikely(!d_can_lookup(dentry))) {
fdput(f);
return ERR_PTR(-ENOTDIR);
}
nd->path = f.file->f_path;
if (flags & LOOKUP_RCU) {
nd->inode = nd->path.dentry->d_inode;
nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
} else {
path_get(&nd->path);
nd->inode = nd->path.dentry->d_inode;
}
fdput(f);
}
/* For scoped-lookups we need to set the root to the dirfd as well. */
if (flags & LOOKUP_IS_SCOPED) {
nd->root = nd->path;
if (flags & LOOKUP_RCU) {
nd->root_seq = nd->seq;
} else {
path_get(&nd->root);
nd->state |= ND_ROOT_GRABBED;
}
}
return s;
}
参数校验与RCU锁决策
if ((flags & (LOOKUP_RCU | LOOKUP_CACHED)) == LOOKUP_CACHED)
return ERR_PTR(-EAGAIN);
LOOKUP_CACHED意为“仅使用目录项缓存(dcache),绝不触发磁盘 I/O”。- 这种模式必须在 RCU(Read-Copy-Update)无锁模式下运行,因为持有锁时如果去睡眠等待磁盘 I/O,会破坏内核调度。如果用户只传了
CACHED却没传RCU,直接报错让上层重试。
if (!*s) flags &= ~LOOKUP_RCU; // 空路径名直接关闭 RCU(后续处理当前目录需要持锁)
if (flags & LOOKUP_RCU) rcu_read_lock(); else nd->seq = nd->next_seq = 0;
- 如果路径名为空字符串(
""),表示操作当前目录("."),不适用 RCU 优化,直接降级。 - 若使用 RCU,则持有
rcu_read_lock()防止并发释放对象;若不用,则重置序列号(用于一致性校验)。
读取全局变更序列号(并发检测)
nd->m_seq = __read_seqcount_begin(&mount_lock.seqcount);
nd->r_seq = __read_seqcount_begin(&rename_lock.seqcount);
smp_rmb();
mount_lock:检测文件系统挂载/卸载事件的变更。rename_lock:检测文件重命名/移动事件的变更。- 在后续路径查找的每一步(如
walk_component),都会重新检查这些序列号。如果发现序列号变了,说明底层文件系统拓扑发生了改变,当前步骤的结果可能已失效,需要重试。smp_rmb()保证读取顺序的正确性。
什么是全局变更序列号?
它们本质上是轻量级、无锁的“版本号/时间戳”,用于无锁检测(RCU 模式下)文件系统全局拓扑结构是否发生了变更。
这是一种**顺序锁(Seqlock)**的读端操作:
- 读端(路径查找):不持有锁,只读一个整数序列号。开始读时记录值(如
seq1),读完后检查值是否还是seq1。如果是,说明没有写者介入,数据安全;如果不是,说明刚才读的过程被打断了,数据可能脏了,放弃结果重试。 - 写端(挂载/重命名):在修改挂载树或重命名时,会将序列号加 1。
核心优势:路径查找全程不阻塞(不需要拿互斥锁),除非发现数据确实过期了(序列号变了),否则就以最高效的无锁状态运行。这是 RCU 路径查找高性能的秘密武器。
ND_ROOT_PRESET分支(不触发)
if (nd->state & ND_ROOT_PRESET) {
struct dentry *root = nd->root.dentry;
struct inode *inode = root->d_inode;
if (*s && unlikely(!d_can_lookup(root)))
return ERR_PTR(-ENOTDIR);
nd->path = nd->root;
nd->inode = inode;
if (flags & LOOKUP_RCU) {
nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
nd->root_seq = nd->seq;
} else {
path_get(&nd->path);
}
return s;
}
由于在do_filp_open向set_nameidata传入的最后一个参数root是NULL,所以
if (unlikely(root)) {
p->state = ND_ROOT_PRESET;
p->root = *root;
}
这里不会被赋值,所以当前的if (nd->state & ND_ROOT_PRESET)不会被触发.这里应该是是用来预先传递进入的路径作为起始路径
绝对路径做起点
nd->root.mnt = NULL;
/* Absolute pathname -- fetch the root (LOOKUP_IN_ROOT uses nd->dfd). */
if (*s == '/' && !(flags & LOOKUP_IN_ROOT)) {
error = nd_jump_root(nd);
if (unlikely(error))
return ERR_PTR(error);
return s;
}
- 绝对路径(
/):调用nd_jump_root,它使用current->fs->root(进程的根目录)。 LOOKUP_IN_ROOT标志(对应openat2的RESOLVE_IN_ROOT)会禁止跳转到全局根,而是强制以dfd作为根进行相对解析,防止路径逃逸。
nd_jump_root
static int nd_jump_root(struct nameidata *nd)
{
if (unlikely(nd->flags & LOOKUP_BENEATH))
return -EXDEV;
if (unlikely(nd->flags & LOOKUP_NO_XDEV)) {
/* Absolute path arguments to path_init() are allowed. */
if (nd->path.mnt != NULL && nd->path.mnt != nd->root.mnt)
return -EXDEV;
}
if (!nd->root.mnt) {
int error = set_root(nd);
if (error)
return error;
}
if (nd->flags & LOOKUP_RCU) {
struct dentry *d;
nd->path = nd->root;
d = nd->path.dentry;
nd->inode = d->d_inode;
nd->seq = nd->root_seq;
if (read_seqcount_retry(&d->d_seq, nd->seq))
return -ECHILD;
} else {
path_put(&nd->path);
nd->path = nd->root;
path_get(&nd->path);
nd->inode = nd->path.dentry->d_inode;
}
nd->state |= ND_JUMPED;
return 0;
}
该函数主要实现了获取当前进程的根目录(path dentry inode)并且赋值给nameidata
1.set_root*
static int set_root(struct nameidata *nd)
{
struct fs_struct *fs = current->fs;
/*
* Jumping to the real root in a scoped-lookup is a BUG in namei, but we
* still have to ensure it doesn't happen because it will cause a breakout
* from the dirfd.
*/
if (WARN_ON(nd->flags & LOOKUP_IS_SCOPED))
return -ENOTRECOVERABLE;
if (nd->flags & LOOKUP_RCU) {
unsigned seq;
do {
seq = read_seqcount_begin(&fs->seq);
nd->root = fs->root;
nd->root_seq = __read_seqcount_begin(&nd->root.dentry->d_seq);
} while (read_seqcount_retry(&fs->seq, seq));
} else {
get_fs_root(fs, &nd->root);
nd->state |= ND_ROOT_GRABBED;
}
return 0;
}
(1)函数首先用一个fs_struct指针指向当前进程的fs
current->fs 是描述进程文件系统上下文的结构体,其中包含:
pwd(当前工作目录)root(进程的根目录)
if (WARN_ON(nd->flags & LOOKUP_IS_SCOPED))
return -ENOTRECOVERABLE;
- 如果设置了
LOOKUP_IS_SCOPED(对应RESOLVE_BENEATH或RESOLVE_IN_ROOT),绝对禁止直接跳到fs->root。 - 为什么要禁止?因为
openat2的作用域限制(如RESOLVE_BENEATH)要求路径不能逃逸出dfd指定的目录。如果直接跳到进程的fs->root,那作用域就瞬间变成整个根目录了,这破坏了openat2的安全语义。因此内核直接WARN_ON并报错,阻止这个操作。
| 场景 | fs->root 指向 | 真实物理路径 |
|---|---|---|
| 普通 Shell 进程 | 系统全局根目录的 dentry | / |
运行了 chroot /new_root 的进程 | /new_root 的 dentry | /new_root |
| Docker 容器内的进程 | 容器的 rootfs 挂载点(如 overlay) | 宿主机上的 /var/lib/docker/... |
(2)查看是否使用的LOOKUP_RCU
如果是那么就直接记录全局变更序列号,然后将当前进程记录的根目录赋值给nameidata
if (nd->flags & LOOKUP_RCU) {
unsigned seq;
do {
seq = read_seqcount_begin(&fs->seq);
nd->root = fs->root;
nd->root_seq = __read_seqcount_begin(&nd->root.dentry->d_seq);
} while (read_seqcount_retry(&fs->seq, seq));
}
如果不是就调用get_fs_root持有自旋锁获取进程根目录
else {
get_fs_root(fs, &nd->root);
nd->state |= ND_ROOT_GRABBED;
}
//include/linux/fs_struct.h
static inline void get_fs_root(struct fs_struct *fs, struct path *root)
{
spin_lock(&fs->lock);
*root = fs->root;
path_get(root);
spin_unlock(&fs->lock);
}
2.将nameidata的path指向刚刚获取得到的根目录root,并且获取得到inode
if (nd->flags & LOOKUP_RCU) {
struct dentry *d;
nd->path = nd->root;
d = nd->path.dentry;
nd->inode = d->d_inode;
nd->seq = nd->root_seq;
if (read_seqcount_retry(&d->d_seq, nd->seq))
return -ECHILD;
} else {
path_put(&nd->path);
nd->path = nd->root;
path_get(&nd->path);
nd->inode = nd->path.dentry->d_inode;
}
直接返回绝对路径
error = nd_jump_root(nd);
if (unlikely(error))
return ERR_PTR(error);
return s;
相对路径做起点(AT_FDCWD)
/* Relative pathname -- get the starting-point it is relative to. */
if (nd->dfd == AT_FDCWD) {
if (flags & LOOKUP_RCU) {
struct fs_struct *fs = current->fs;
unsigned seq;
do {
seq = read_seqcount_begin(&fs->seq);
nd->path = fs->pwd;
nd->inode = nd->path.dentry->d_inode;
nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
} while (read_seqcount_retry(&fs->seq, seq));
} else {
get_fs_pwd(current->fs, &nd->path);
nd->inode = nd->path.dentry->d_inode;
}
}
这里的整体思路和上面的绝对路径是一致的,首先是查看是否是LOOKUP_RCU快速查找
不论是快速查找还是普通获取锁查找,都是先获取得到当前进程的fs_struct,然后当前nameidata的path指向进程的当前工作目录current->fs->pwd,并且获取得到对应的inode节点
唯一和绝对路径不同的就是不需要去设置nameidata的root
相对路径做起点(dfd指向具体文件描述符)
else {
/* Caller must check execute permissions on the starting path component */
struct fd f = fdget_raw(nd->dfd);
struct dentry *dentry;
if (!f.file)
return ERR_PTR(-EBADF);
dentry = f.file->f_path.dentry;
if (*s && unlikely(!d_can_lookup(dentry))) {
fdput(f);
return ERR_PTR(-ENOTDIR);
}
nd->path = f.file->f_path;
if (flags & LOOKUP_RCU) {
nd->inode = nd->path.dentry->d_inode;
nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
} else {
path_get(&nd->path);
nd->inode = nd->path.dentry->d_inode;
}
fdput(f);
}
这里不做细致分析,我们当前值是分析的普通的open系统调用,传递进入的就是AT_FDCWD.但是仔细阅读会发现其实和上面并无思路上的差异,唯一不同的就是把path指向的dfd对应的路径
- 调用
fdget_raw(nd->dfd)获取struct file *。 - 检查该文件描述符是否指向目录(
d_can_lookup),如果不是且路径名非空,直接报ENOTDIR。 - 取用
f->f_path作为起点。
作用域限制(Scoped Lookups)
/* For scoped-lookups we need to set the root to the dirfd as well. */
if (flags & LOOKUP_IS_SCOPED) {
nd->root = nd->path;
if (flags & LOOKUP_RCU) {
nd->root_seq = nd->seq;
} else {
path_get(&nd->root);
nd->state |= ND_ROOT_GRABBED;
}
}
return s;
- 如果设置了
LOOKUP_IS_SCOPED(对应RESOLVE_BENEATH或RESOLVE_IN_ROOT),将刚刚确定的起点路径(nd->path)同时设置为根路径(nd->root)。 - 这确保了后续任何试图通过
..或符号链接逃逸到该起点之上的行为,都会在handle_dots()或follow_dotdot()中被拦截并报错。
路径查找循环
while (!(error = link_path_walk(s, nd)) &&
(s = open_last_lookups(nd, file, op)) != NULL)
;
这里是用来一个while循环去进行路径的处理,这两个函数是 path_openat() 函数中路径查找循环的两个核心引擎。它们分工明确:
link_path_walk:处理*“中间路径分量”*(即除最后一个分量外的所有目录)。open_last_lookups:处理*“最后一个路径分量”*,以及跟随尾随符号链接后的重新进入
这个 while 循环的设计是为了优雅地处理 尾随符号链接(Trailing Symlink)的递归或迭代解析。
路径查找-处理中间分量-link_path_walk
/*
* Name resolution.
* This is the basic name resolution function, turning a pathname into
* the final dentry. We expect 'base' to be positive and a directory.
*
* Returns 0 and nd will have valid dentry and mnt on success.
* Returns error and drops reference to input namei data on failure.
*/
static int link_path_walk(const char *name, struct nameidata *nd)
职责:从当前位置(由 path_init 设置的 nd->path)开始,解析路径字符串 s 中除最后一个分量外的所有分量。
函数定义很长,这里就不复制过来了,函数流程如下:
1.初始准备
nd->last_type = LAST_ROOT;
nd->flags |= LOOKUP_PARENT;
if (IS_ERR(name))
return PTR_ERR(name);
while (*name=='/')
name++;
if (!*name) {
nd->dir_mode = 0; // short-circuit the 'hardening' idiocy
return 0;
}
last_type初始为LAST_ROOT,表示尚未处理任何分量。- 设置
LOOKUP_PARENT标志,通知后续函数“我们正在解析路径的父级部分,最后一个分量还没处理”。
然后跳过路径开头的所有 /。如果路径只剩空串(如 "/" 或 ""),则直接返回成功(表示当前目录)。
2.主循环处理
每一次循环处理一个路径分量
/* At this point we know we have a real path component. */
for(;;) {
struct user_namespace *mnt_userns;
const char *link;
u64 hash_len;
int type;
mnt_userns = mnt_user_ns(nd->path.mnt);
err = may_lookup(mnt_userns, nd);
may_lookup检查当前进程是否有对nd->path所指向目录的执行(搜索)权限。这是路径遍历的基本权限检查:要进入一个目录,必须拥有该目录的x权限。
3.计算当前分量的哈希和长度
hash_len = hash_name(nd->path.dentry, name);
- 这个函数(在内核中通常是一个内联函数)使用
d_hash函数(基于路径名字符串)同时计算出哈希值和长度,打包在u64中返回。
4.识别特殊分量:. 和 ..
首先定义文件为LAST_NORM表示正常分量
type = LAST_NORM;
if (name[0] == '.') switch (hashlen_len(hash_len)) {
case 2:
if (name[1] == '.') {
type = LAST_DOTDOT;
nd->state |= ND_JUMPED;
}
break;
case 1:
type = LAST_DOT;
}
- 如果分量是
".",类型设为LAST_DOT(当前目录,通常无实际效果)。 - 如果是
"..",类型设为LAST_DOTDOT,并标记ND_JUMPED,表示路径发生了“跳转”(向上),后续的路径追溯会依赖这个标记。
5.对于正常分量,允许文件系统自定义哈希
if (likely(type == LAST_NORM)) {
nd->state &= ~ND_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);
...
hash_len = this.hash_len;
name = this.name;
}
}
- 如果文件系统定义了自定义的
d_hash操作(例如不区分大小写的文件系统),则调用它重新计算哈希和长度,并调整name指针(可能因规范化为小写而改变)。
6.将分量信息存入 nd->last
nd->last.hash_len = hash_len;
nd->last.name = name;
nd->last_type = type;
- 保存当前分量,后续的
walk_component或open_last_lookups会用到。
7.跳过该分量及后续的 /,确定是否到达路径末尾
name += hashlen_len(hash_len);
if (!*name) goto OK;
do { name++; } while (*name == '/');
if (!*name) goto OK;
- 推进
name指针到下一个分量的起始位置。 - 如果指针指向
'\0',表示当前分量是最后一个分量,跳转到OK标签。 - 如果指针指向
/,则跳过所有连续的/,若之后遇到'\0',也说明当前分量是最后一个(即路径以/结尾),同样跳转OK。
注意上面说的是跳过所有连续的/,不是跳过后续的每一个/
8. 处理非最后一个分量(即中间目录)
link = walk_component(nd, WALK_MORE);
walk_component会实际在文件系统中查找nd->last对应的 dentry(从父目录开始查找)。- 如果该分量是一个符号链接,且没有设置阻止跟随的标志(如
LOOKUP_NO_SYMLINKS),walk_component会返回该符号链接的目标路径字符串(link)。 - 如果该分量是普通目录,则返回
NULL,并且nd->path已经更新为指向该目录。
9.如果遇到符号链接(link 非空)
if (unlikely(link)) {
if (IS_ERR(link)) return PTR_ERR(link);
nd->stack[depth++].name = name; // 保存当前剩余未解析的路径
name = link; // 切换解析到符号链接的目标
continue; // 重新开始循环(解析目标路径)
}
- 此时
name指向当前符号链接之后的剩余路径(例如原路径是"/foo/link/bar",name指向"bar")。 - 将
name压入栈(nd->stack),然后让name指向符号链接的目标路径(可能是绝对或相对路径)。 - 重新开始循环,解析这个新路径。
10. 如果分量不是目录(但也不是符号链接),报错
if (unlikely(!d_can_lookup(nd->path.dentry))) {
if (nd->flags & LOOKUP_RCU) {
if (!try_to_unlazy(nd)) return -ECHILD;
}
return -ENOTDIR;
}
- 如果当前分量不是目录(例如是一个普通文件),而路径还需要继续往下解析,则报
ENOTDIR。 - 如果是 RCU 模式,先尝试转为非 RCU(
try_to_unlazy)再返回错误,以保证后续清理正确
11. 处理最后一个分量(OK 标签)
OK:
if (!depth) {
nd->dir_uid = i_uid_into_mnt(mnt_userns, nd->inode);
nd->dir_mode = nd->inode->i_mode;
nd->flags &= ~LOOKUP_PARENT;
return 0;
}
/* last component of nested symlink */
name = nd->stack[--depth].name;
link = walk_component(nd, 0);
- 如果
depth == 0,说明当前是在顶层路径的最后一个分量,此时nd->path指向该分量的父目录。保存父目录的 uid 和 mode(用于后续创建文件时的权限检查),清除LOOKUP_PARENT,然后返回成功。 - 如果
depth > 0,说明我们正在解析一个嵌套符号链接(即符号链接内部路径的最后一个分量)。此时需要从栈中弹出之前保存的剩余路径名(name),并调用walk_component(nd, 0)来实际解析该分量(最终会走到它的父目录)。这里walk_component会返回新的link(若该分量又是符号链接,则继续循环;否则返回NULL,并更新nd->path指向父目录),然后循环继续。
walk_component简易分析
static const char *walk_component(struct nameidata *nd, int flags)
{
struct dentry *dentry;
/*
* "." and ".." are special - ".." especially so because it has
* to be able to know about the current root directory and
* parent relationships.
*/
if (unlikely(nd->last_type != LAST_NORM)) {
if (!(flags & WALK_MORE) && nd->depth)
put_link(nd);
return handle_dots(nd, nd->last_type);
}
dentry = lookup_fast(nd);
if (IS_ERR(dentry))
return ERR_CAST(dentry);
if (unlikely(!dentry)) {
dentry = lookup_slow(&nd->last, nd->path.dentry, nd->flags);
if (IS_ERR(dentry))
return ERR_CAST(dentry);
}
if (!(flags & WALK_MORE) && nd->depth)
put_link(nd);
return step_into(nd, flags, dentry);
}
该函数主要是获取得到nd当前指向目录下对应的当前要查找分量的dentry,最后调用step_into
step_into 负责将 walk_component 刚查找到的 dentry(目录项)正式应用到 nameidata 上下文(nd)中,使 nd->path 从“当前目录的父目录”变成“当前分量指向的目录/文件”。
step_into 内部会做三件核心工作:
- 处理挂载点(Mount Point):如果
dentry是一个文件系统的挂载点,它会将nd->path.mnt切换到被挂载的文件系统根目录,同时更新dentry为根目录的 dentry。 - 处理跟随链接(Follow Link,仅在非最后分量时):如果在路径中间遇到了符号链接,且没有设置
LOOKUP_NO_FOLLOW,它会尝试跟随该链接(虽然walk_component已经检测并返回了link,但step_into在普通情况下只处理非链接的进入)。 - 更新
nd->path和nd->inode:最终将nd->path.dentry指向这个新的 dentry,nd->inode指向其对应的inode。
step_into内部处理实现较为复杂 ,笔者这里也不做过多分析了 ,大致知道怎么回事就行
我们主要来看一下查找dentry
查找dentry
dentry = lookup_fast(nd);
if (IS_ERR(dentry))
return ERR_CAST(dentry);
if (unlikely(!dentry)) {
dentry = lookup_slow(&nd->last, nd->path.dentry, nd->flags);
if (IS_ERR(dentry))
return ERR_CAST(dentry);
}
这里首先是使用的lookup_fast查找 ,然后使用的是lookup_slow查找 。前者是在缓存中查找,而后者则是由于缓存中没找到,需要根据inode节点分配一个dentry缓存
lookup_fast
static struct dentry *lookup_fast(struct nameidata *nd)
{
struct dentry *dentry, *parent = nd->path.dentry;
int status = 1;
/*
* Rename seqlock is not required here because in the off chance
* of a false negative due to a concurrent rename, the caller is
* going to fall back to non-racy lookup.
*/
if (nd->flags & LOOKUP_RCU) {
dentry = __d_lookup_rcu(parent, &nd->last, &nd->next_seq);
if (unlikely(!dentry)) {
if (!try_to_unlazy(nd))
return ERR_PTR(-ECHILD);
return NULL;
}
/*
* This sequence count validates that the parent had no
* changes while we did the lookup of the dentry above.
*/
if (read_seqcount_retry(&parent->d_seq, nd->seq))
return ERR_PTR(-ECHILD);
status = d_revalidate(dentry, nd->flags);
if (likely(status > 0))
return dentry;
if (!try_to_unlazy_next(nd, dentry))
return ERR_PTR(-ECHILD);
if (status == -ECHILD)
/* we'd been told to redo it in non-rcu mode */
status = d_revalidate(dentry, nd->flags);
} else {
dentry = __d_lookup(parent, &nd->last);
if (unlikely(!dentry))
return NULL;
status = d_revalidate(dentry, nd->flags);
}
if (unlikely(status <= 0)) {
if (!status)
d_invalidate(dentry);
dput(dentry);
return ERR_PTR(status);
}
return dentry;
}
函数主要核心就是两个获取目录下目标dentry的函数__d_lookup_rcu和__d_lookup ,其他的都是一些判断dentry操作这里不细看了
__d_lookup_rcu
// fs/dcache.c
struct dentry *__d_lookup_rcu(const struct dentry *parent,
const struct qstr *name,
unsigned *seqp)
{
u64 hashlen = name->hash_len;
const unsigned char *str = name->name;
struct hlist_bl_head *b = d_hash(hashlen_hash(hashlen));
struct hlist_bl_node *node;
struct dentry *dentry;
/*
* Note: There is significant duplication with __d_lookup_rcu which is
* required to prevent single threaded performance regressions
* especially on architectures where smp_rmb (in seqcounts) are costly.
* Keep the two functions in sync.
*/
if (unlikely(parent->d_flags & DCACHE_OP_COMPARE))
return __d_lookup_rcu_op_compare(parent, name, seqp);
/*
* The hash list is protected using RCU.
*
* Carefully use d_seq when comparing a candidate dentry, to avoid
* races with d_move().
*
* It is possible that concurrent renames can mess up our list
* walk here and result in missing our dentry, resulting in the
* false-negative result. d_lookup() protects against concurrent
* renames using rename_lock seqlock.
*
* See Documentation/filesystems/path-lookup.txt for more details.
*/
hlist_bl_for_each_entry_rcu(dentry, node, b, d_hash) {
unsigned seq;
/*
* The dentry sequence count protects us from concurrent
* renames, and thus protects parent and name fields.
*
* The caller must perform a seqcount check in order
* to do anything useful with the returned dentry.
*
* NOTE! We do a "raw" seqcount_begin here. That means that
* we don't wait for the sequence count to stabilize if it
* is in the middle of a sequence change. If we do the slow
* dentry compare, we will do seqretries until it is stable,
* and if we end up with a successful lookup, we actually
* want to exit RCU lookup anyway.
*
* Note that raw_seqcount_begin still *does* smp_rmb(), so
* we are still guaranteed NUL-termination of ->d_name.name.
*/
seq = raw_seqcount_begin(&dentry->d_seq);
if (dentry->d_parent != parent)
continue;
if (d_unhashed(dentry))
continue;
if (dentry->d_name.hash_len != hashlen)
continue;
if (dentry_cmp(dentry, str, hashlen_len(hashlen)) != 0)
continue;
*seqp = seq;
return dentry;
}
return NULL;
}
其实通过函数查看可以大致知道实现就是在dcache 哈希表中查找索引
- 通过
d_hash(hashlen_hash(hashlen))计算出一个索引,直接去全局dentry_hashtable的对应**哈希桶(bucket)**里找。 - 快速过滤:在同一个哈希桶里,可能有多个不同的文件名(哈希冲突)。此时内核会优先比对哈希值。只有哈希值完全相等的 dentry,才会去调用
memcmp做昂贵的逐字节字符串比对。
__d_lookup
__d_lookup也是同样的操作 ,只是多了获取读锁,会有睡眠
struct dentry *__d_lookup(const struct dentry *parent, const struct qstr *name)
{
unsigned int hash = name->hash;
struct hlist_bl_head *b = d_hash(hash);
struct hlist_bl_node *node;
struct dentry *found = NULL;
struct dentry *dentry;
rcu_read_lock();
lookup_slow
static struct dentry *lookup_slow(const struct qstr *name,
struct dentry *dir,
unsigned int flags)
{
struct inode *inode = dir->d_inode;
struct dentry *res;
inode_lock_shared(inode);
res = __lookup_slow(name, dir, flags);
inode_unlock_shared(inode);
return res;
}
如果上面的快速从缓存中获取dentry没有成功,那么就需要分配一个缓存结构并且调用inode的i_op->lookup查找到对应的dentry项
/* Fast lookup failed, do it the slow way */
static struct dentry *__lookup_slow(const struct qstr *name,
struct dentry *dir,
unsigned int flags)
{
struct dentry *dentry, *old;
struct inode *inode = dir->d_inode;
DECLARE_WAIT_QUEUE_HEAD_ONSTACK(wq);
/* Don't go there if it's already dead */
if (unlikely(IS_DEADDIR(inode)))
return ERR_PTR(-ENOENT);
again:
dentry = d_alloc_parallel(dir, name, &wq);
if (IS_ERR(dentry))
return dentry;
if (unlikely(!d_in_lookup(dentry))) {
int error = d_revalidate(dentry, flags);
if (unlikely(error <= 0)) {
if (!error) {
d_invalidate(dentry);
dput(dentry);
goto again;
}
dput(dentry);
dentry = ERR_PTR(error);
}
} else {
old = inode->i_op->lookup(inode, dentry, flags);
d_lookup_done(dentry);
if (unlikely(old)) {
dput(dentry);
dentry = old;
}
}
return dentry;
}
d_alloc_parallel 的行为取决于当前是否有其他线程也在查找同一个名字:
- 情况 A(无人竞争):在 dcache 中新建一个
dentry对象,并将其标记为DCACHE_IN_LOOKUP(正在查找中),然后返回这个 dentry。 - 情况 B(有人竞争):发现已经有一个同名的 dentry 正处于
DCACHE_IN_LOOKUP状态(即其他线程正在磁盘上查找它)。此时当前线程不会新建对象,而是获得这个正在被查找的 dentry 的引用,然后进入睡眠等待(利用&wq等待队列),直到第一个线程查找完成并唤醒它。
路径查找-处理最终分量-open_last_lookups
它的核心职责是:专门处理路径的最后一个分量,根据是否带有 O_CREAT 标志,决定是“查找已存在的文件”还是“尝试创建新文件”,并在必要时跟随尾随符号链接。
这里由于函数代码很长也不全部粘贴过来 ,以下是大致过程:
1. 预处理:处理特殊分量 (. 和 ..)
if (nd->last_type != LAST_NORM) {
if (nd->depth) put_link(nd);
return handle_dots(nd, nd->last_type);
}
- 如果最后一个分量是
"."或"..",直接交给handle_dots处理,它负责在当前目录或父目录上操作,不涉及真正的文件查找。
2. 分支一:没有 O_CREAT(仅打开已存在的文件)
if (!(open_flag & O_CREAT)) {
if (nd->last.name[nd->last.len])
nd->flags |= LOOKUP_FOLLOW | LOOKUP_DIRECTORY;
dentry = lookup_fast(nd); // 优先在 dcache 中查找
...
if (likely(dentry)) goto finish_lookup;
BUG_ON(nd->flags & LOOKUP_RCU);
}
- 关键细节:
nd->last.name[nd->last.len]检查最后一个分量后面是否跟了额外的字符。如果路径是"file/"(以斜杠结尾),说明用户期望打开一个目录,因此设置LOOKUP_DIRECTORY标志(后续step_into会检查是否真的是目录)。 - 查找策略:
- 优先调用
lookup_fast(RCU 无锁模式),试图在目录项缓存中秒级命中。 - 如果找到(
dentry非空),直接跳到finish_lookup进行收尾。 - 如果没找到(
dentry == NULL),此时必须退出 RCU 模式(BUG_ON确保当前不在 RCU 模式),因为后续的慢速查找(lookup_slow)需要在__lookup_slow中持锁甚至访问磁盘。
- 优先调用
3. 分支二:带有 O_CREAT(可能创建文件)
else {
if (nd->flags & LOOKUP_RCU) {
if (!try_to_unlazy(nd)) return ERR_PTR(-ECHILD);
}
audit_inode(nd->name, dir, AUDIT_INODE_PARENT);
if (unlikely(nd->last.name[nd->last.len]))
return ERR_PTR(-EISDIR);
}
- RCU 降级:创建文件必然涉及修改父目录(添加目录项),不能使用 RCU 无锁模式,必须强制转为持锁模式(
try_to_unlazy)。如果转换失败(如 RCU 数据已过期),返回-ECHILD让上层重试。 - 斜杠检查:如果路径是
"file/"且要创建文件,这是矛盾语义(创建文件还是目录?),直接返回-EISDIR(是个目录)报错。
4. 获取写权限与加锁(准备修改父目录)
if (open_flag & (O_CREAT | O_TRUNC | O_WRONLY | O_RDWR)) {
got_write = !mnt_want_write(nd->path.mnt);
// 注意:即使失败也不立即返回,让 lookup_open 决定最终错误
}
if (open_flag & O_CREAT)
inode_lock(dir->d_inode); // 创建需独占锁
else
inode_lock_shared(dir->d_inode); // 仅打开/截断需共享锁
mnt_want_write:检查并标记该挂载点是否可写(例如,只读挂载点会失败)。这里将失败结果暂存于got_write(true表示成功,false表示失败),不立即报错,而是交给lookup_open决定最终的错误码(因为可能触发-EROFS)。- Inode 锁:
O_CREAT需修改父目录的目录项,取独占锁(inode_lock);无需创建(仅打开/截断)取共享锁(inode_lock_shared),允许多个读操作并发。
5. 核心调用:lookup_open(真正的决策中心)
dentry = lookup_open(nd, file, op, got_write);
- 这个函数是*“压轴演员”*。它接收所有标志和锁状态,直接执行:
- 检查文件名是否合法(如空字符、过长等)。
- 如果
got_write为false,返回-EROFS或-EACCES。 - 在父目录下查找同名 dentry:
- 存在:根据
O_EXCL决定报-EEXIST,或者直接复用。 - 不存在:调用文件系统的
create回调(如ext4_create)在磁盘上创建新 inode,并构建新 dentry。
- 存在:根据
- 处理
O_TRUNC:若文件存在且为普通文件,清空内容。
- 后处理:
- 如果
FMODE_CREATED被设置,调用fsnotify_create触发文件系统通知事件(如 inotify)。 - 释放 inode 锁和写权限(
mnt_drop_write)。
- 如果
6. 提前返回优化(FMODE_OPENED / FMODE_CREATED)
if (file->f_mode & (FMODE_OPENED | FMODE_CREATED)) {
dput(nd->path.dentry);
nd->path.dentry = dentry;
return NULL;
}
- 如果
lookup_open已经完全打开了文件(设置了FMODE_OPENED)或创建了文件(FMODE_CREATED),说明struct file已经和底层 inode 绑定完成,无需再走step_into的标准“进入”流程。 - 直接替换
nd->path.dentry为这个最终的 dentry,然后返回NULL给上层循环,表示“路径解析彻底完成”。
7. 收尾标签:finish_lookup(标准进入流程)
finish_lookup:
if (nd->depth) put_link(nd);
res = step_into(nd, WALK_TRAILING, dentry);
if (unlikely(res))
nd->flags &= ~(LOOKUP_OPEN|LOOKUP_CREATE|LOOKUP_EXCL);
return res;
put_link:如果当前处于符号链接内部(nd->depth > 0),释放该链接的上下文。step_into(nd, WALK_TRAILING, dentry):将dentry正式挂载到nd->path上(类似进入目录)。- 返回值
res的处理:- 如果
step_into返回非NULL,说明这个最后一个分量实际上是尾随符号链接,且需要被跟随。此时清除LOOKUP_OPEN等意图标志,并将新路径返回给上层while循环,触发新一轮link_path_walk解析。
- 如果
lookup_open
它负责在父目录下查找或创建最终的 dentry,并处理各种复杂情况(缓存、并发、权限、原子创建等)。
函数实现代码也比较长,这里只大致分析一下干了什么
1.主要是在开始去查找对应的dentry是否存在,如果不存在就调用d_alloc_parallel创建一个dentry。如果返回的 dentry 不在查找状态(即已经是完整缓存的),则调用 d_revalidate 检查缓存是否有效(针对 NFS 等)。若有效(>0)则跳出;若无效(0),则使该缓存失效,释放后重新分配;若出错(<0)则直接报错。
if (!dentry) {
dentry = d_alloc_parallel(dir, &nd->last, &wq);
if (IS_ERR(dentry))
return dentry;
}
if (d_in_lookup(dentry))
break;
2.如果 dentry 有 d_inode,说明文件已经存在,直接返回这个 dentry。后续的 open 调用会通过 f_op->open 打开它。
if (dentry->d_inode) {
/* Cached positive dentry: will open in f_op->open */
return dentry;
}
3.查看当前进程是否对目录有权限创建修改,因为要在目录下创建一个inode节点
4.如果有权限那么就最终调用文件系统的 create 方法(如 ext4_create)在磁盘上创建新 inode 并关联 dentry。
if (!dentry->d_inode && (open_flag & O_CREAT)) {
file->f_mode |= FMODE_CREATED;
audit_inode_child(dir_inode, dentry, AUDIT_TYPE_CHILD_CREATE);
if (!dir_inode->i_op->create) {
error = -EACCES;
goto out_dput;
}
//调用文件系统的create
error = dir_inode->i_op->create(mnt_userns, dir_inode, dentry, mode, open_flag & O_EXCL);
if (error) goto out_dput;
}
关于inode_operations的create可以查看笔者上篇文章:kidwjb的小站-超级块&inode&dentry在内核中的分配
do_open
在完成上面路径查找循环之后,最终来到了do_open。决策并执行最后一个分量的最终打开操作。它集成权限检查、截断、粘滞位处理,并有条件地调用 vfs_open。
/*
* Handle the last step of open()
*/
static int do_open(struct nameidata *nd,
struct file *file, const struct open_flags *op)
{
struct user_namespace *mnt_userns;
int open_flag = op->open_flag;
bool do_truncate;
int acc_mode;
int error;
if (!(file->f_mode & (FMODE_OPENED | FMODE_CREATED))) {
error = complete_walk(nd);
if (error)
return error;
}
if (!(file->f_mode & FMODE_CREATED))
audit_inode(nd->name, nd->path.dentry, 0);
mnt_userns = mnt_user_ns(nd->path.mnt);
if (open_flag & O_CREAT) {
if ((open_flag & O_EXCL) && !(file->f_mode & FMODE_CREATED))
return -EEXIST;
if (d_is_dir(nd->path.dentry))
return -EISDIR;
error = may_create_in_sticky(mnt_userns, nd,
d_backing_inode(nd->path.dentry));
if (unlikely(error))
return error;
}
if ((nd->flags & LOOKUP_DIRECTORY) && !d_can_lookup(nd->path.dentry))
return -ENOTDIR;
do_truncate = false;
acc_mode = op->acc_mode;
if (file->f_mode & FMODE_CREATED) {
/* Don't check for write permission, don't truncate */
open_flag &= ~O_TRUNC;
acc_mode = 0;
} else if (d_is_reg(nd->path.dentry) && open_flag & O_TRUNC) {
error = mnt_want_write(nd->path.mnt);
if (error)
return error;
do_truncate = true;
}
error = may_open(mnt_userns, &nd->path, acc_mode, open_flag);
if (!error && !(file->f_mode & FMODE_OPENED))
error = vfs_open(&nd->path, file);
if (!error)
error = ima_file_check(file, op->acc_mode);
if (!error && do_truncate)
error = handle_truncate(mnt_userns, file);
if (unlikely(error > 0)) {
WARN_ON(1);
error = -EINVAL;
}
if (do_truncate)
mnt_drop_write(nd->path.mnt);
return error;
}
这里是它的核心逻辑:
- 补齐路径查找(
complete_walk):- 如果在
open_last_lookups阶段还未完成最后的路径查找(例如还没进入最后一个分量),则在此处完成。
- 如果在
- 处理
O_CREAT与O_EXCL的冲突:- 如果带
O_CREAT|O_EXCL,但file并未标记FMODE_CREATED(意味着文件是已存在的),直接返回-EEXIST。 - 如果带
O_CREAT且目标是目录,返回-EISDIR。 - 检查“粘滞位”(Sticky Bit)限制(
may_create_in_sticky):例如在/tmp下创建文件时,只有文件所有者或 root 才能覆盖别人的文件。
- 如果带
- 检查
LOOKUP_DIRECTORY:- 如果路径查找要求必须是目录(如路径末尾带
/),但目标不是目录,返回-ENOTDIR。
- 如果路径查找要求必须是目录(如路径末尾带
- 处理截断(
O_TRUNC):- 如果文件是普通文件且带有
O_TRUNC,先获取写权限(mnt_want_write),标记需要截断(do_truncate = true),但延迟执行,先不截断。
- 如果文件是普通文件且带有
- 调用
may_open:- 执行核心的 VFS 访问权限检查(基于
acc_mode,如MAY_READ/MAY_WRITE)。
- 执行核心的 VFS 访问权限检查(基于
- 实际打开文件(
vfs_open)——这是一个关键条件分支:
if (!error && !(file->f_mode & FMODE_OPENED))
error = vfs_open(&nd->path, file);
- 如果
file尚未标记FMODE_OPENED(意味着未被atomic_open等特殊路径处理过),则调用vfs_open,进而调用do_dentry_open做完整初始化。 - 如果已被标记(例如
lookup_open中的atomic_open已经调用了do_dentry_open),则跳过本次调用,避免重复初始化。这体现了内核的灵活性:允许底层文件系统“提前”完成打开工作。
- IMA 完整性检查(
ima_file_check):
- 如果启用了 IMA(完整性度量架构),进行文件完整性验证。
- 执行延迟截断(
handle_truncate):
- 如果第 4 步标记了
do_truncate,现在调用handle_truncate真正清空文件内容,并释放之前获取的写权限。 vfs_open/** * vfs_open - open the file at the given path * @path: path to open * @file: newly allocated file with f_flag initialized * @cred: credentials to use */ int vfs_open(const struct path *path, struct file *file) { file->f_path = *path; return do_dentry_open(file, d_backing_inode(path->dentry), NULL); }职责:do_dentry_open 的一个简单包装器,它只做两件事: - 把 path 结构体拷贝给 file->f_path(绑定挂载点和 dentry)。
- 调用 do_dentry_open(file, d_backing_inode(path->dentry), NULL)(第三个参数传 NULL,意味着直接使用 inode->i_fop->open)。 do_dentry_open
static int do_dentry_open(struct file *f, struct inode *inode, int (*open)(struct inode *, struct file *))这是do_open调用到底层的部分,它的作用是将一个已找到的inode和一个struct file对象绑定,然后调用文件系统给定的open回调函数,并在最后完成绝大部分的初始化工作。
- 基本属性绑定:
- 将 inode 赋值给
f->f_inode,将inode->i_mapping赋值给f->f_mapping(地址空间)。path_get(&f->f_path); f->f_inode = inode; f->f_mapping = inode->i_mapping;
- 特殊快速路径:
O_PATH:
- 如果只打开路径引用(O_PATH),不进行任何真正的读写或打开操作,直接设置 f_op = &empty_fops 并返回。这是最轻量的模式。
static const struct file_operations empty_fops = {}; if (unlikely(f->f_flags & O_PATH)) { f->f_mode = FMODE_PATH | FMODE_OPENED; f->f_op = &empty_fops; return 0; }
- 读写权限计数管理:
if ((f->f_mode & (FMODE_READ | FMODE_WRITE)) == FMODE_READ) { i_readcount_inc(inode); } else if (f->f_mode & FMODE_WRITE && !special_file(inode->i_mode)) { error = get_write_access(inode); if (unlikely(error)) goto cleanup_file; error = __mnt_want_write(f->f_path.mnt); if (unlikely(error)) { put_write_access(inode); goto cleanup_file; } f->f_mode |= FMODE_WRITER; }
- 如果以只读打开(
FMODE_READ),递增inode的读引用计数(i_readcount_inc)。 - 如果以写方式打开(
FMODE_WRITE)且不是特殊文件(如设备文件),需要:- 增加inode的写访问计数(
get_write_access),防止写时被意外删除。 - 增加挂载点的写者计数(
__mnt_want_write),防止在写过程中被卸载。
- 增加inode的写访问计数(
- 文件操作函数表(
f_op)的绑定:
f->f_op = fops_get(inode->i_fop):从 inode 获取文件系统特定的操作表(如 ext4_file_operations)。
- 调用文件系统的 open 方法:
if (!open) open = f->f_op->open; if (open) { error = open(inode, f); if (error) goto cleanup_all; }
- 如果传入的
open回调非空则调用,否则使用f->f_op->open。这一步让文件系统有机会做特定初始化(如 NFS 的open状态管理)。
- 能力标志位(
FMODE_*)的最终计算:f->f_mode |= FMODE_OPENED; if ((f->f_mode & FMODE_READ) && likely(f->f_op->read || f->f_op->read_iter)) f->f_mode |= FMODE_CAN_READ; if ((f->f_mode & FMODE_WRITE) && likely(f->f_op->write || f->f_op->write_iter)) f->f_mode |= FMODE_CAN_WRITE; if ((f->f_mode & FMODE_LSEEK) && !f->f_op->llseek) f->f_mode &= ~FMODE_LSEEK; if (f->f_mapping->a_ops && f->f_mapping->a_ops->direct_IO) f->f_mode |= FMODE_CAN_ODIRECT;
- 设置
FMODE_OPENED(表示已完全打开)。 - 根据
f_op是否提供read/write或read_iter/write_iter,动态设置FMODE_CAN_READ/FMODE_CAN_WRITE。 - 如果文件系统没有提供
llseek,则清除FMODE_LSEEK。 - 检查是否支持
O_DIRECT(直接 I/O)。 terminate_walk 在上面完成了目标文件dentry的查找并且创建/打开了目标文件之后,就可以释放掉nameidatastatic void terminate_walk(struct nameidata *nd) { drop_links(nd); if (!(nd->flags & LOOKUP_RCU)) { int i; path_put(&nd->path); for (i = 0; i < nd->depth; i++) path_put(&nd->stack[i].link); if (nd->state & ND_ROOT_GRABBED) { path_put(&nd->root); nd->state &= ~ND_ROOT_GRABBED; } } else { leave_rcu(nd); } nd->depth = 0; nd->path.mnt = NULL; nd->path.dentry = NULL; }返回最终的file结构体到do_sys_openat2// 从path_openat返回到do_filp_open if (likely(!error)) { if (likely(file->f_mode & FMODE_OPENED)) return file; ...//从do_filp_open返回到do_sys_openat2 //这里配合最前面的set_nameidata,这里将原本 保存在old中的拿出来 restore_nameidata(); return filp; static void restore_nameidata(void) { struct nameidata *now = current->nameidata, *old = now->saved; current->nameidata = old; if (old) old->total_link_count = now->total_link_count; if (now->stack != now->internal) kfree(now->stack); }
通知文件被打开-fsnotify_open
fsnotify_open(f);
在上面的struct file *f = do_filp_open(dfd, tmp, &op);返回到打开的文件file结构体之后,会调用这个函数。 作用是触发文件系统通知事件(inotify、fanotify、dnotify),告诉任何监听该文件的用户空间程序:“这个文件刚刚被打开了”。
注册file到全局文件描述表中
fd_install(fd, f);
这里就是把得到的file结构体放进fdtable中的全局文件描述表中,fd对应file项在数组中的下标
void fd_install(unsigned int fd, struct file *file)
{
struct files_struct *files = current->files;
struct fdtable *fdt;
rcu_read_lock_sched();
if (unlikely(files->resize_in_progress)) {
rcu_read_unlock_sched();
spin_lock(&files->file_lock);
fdt = files_fdtable(files);
BUG_ON(fdt->fd[fd] != NULL);
rcu_assign_pointer(fdt->fd[fd], file);
spin_unlock(&files->file_lock);
return;
}
/* coupled with smp_wmb() in expand_fdtable() */
smp_rmb();
fdt = rcu_dereference_sched(files->fdt);
BUG_ON(fdt->fd[fd] != NULL);
rcu_assign_pointer(fdt->fd[fd], file);
rcu_read_unlock_sched();
}
函数内部的实现还是很有操作的,考虑到了很多并法场景以及锁的各种用法,为的就是能够安全的把file放进文件描述数组中
do_sys_open整体流程图
do_sys_open整体流程分析起来很长很长,笔者写到这里也基本上写了快两万字了,所以为了在看完上面那么多后还能对整体流程有思路,笔者让AI整理出来了下述流程图,方便能够回顾
当然如果对于图表感觉总结的不到位,笔者也放上源码方便大家修改
graph TD
A[用户态调用 open()] --> B[SYSCALL_DEFINE3(open)]
B --> C[do_sys_open]
C --> D[build_open_how: 校验并规范化 flags/mode]
D --> E[do_sys_openat2]
E --> F[build_open_flags: 解析标志位 → struct open_flags]
E --> G[getname: 从用户空间复制路径名 → struct filename]
E --> H[get_unused_fd_flags → alloc_fd]
H --> H1[在 files_struct 中寻找最小空闲 fd]
H1 --> H2[必要时扩容 fdtable]
H1 --> H3[在位图 open_fds 中标记占用]
E --> I[do_filp_open]
I --> I1[set_nameidata: 初始化 nameidata 上下文]
I1 --> I2[path_openat 三次尝试<br>(RCU / 传统 / 强制重校验)]
I2 --> J[path_openat 核心]
J --> J1[alloc_empty_file: 分配 struct file 并初始化]
J1 --> J2[path_init: 确定路径查找起点]
J2 --> J2a{路径类型}
J2a -- 绝对路径 --> J2b[nd_jump_root: 使用进程根目录]
J2a -- 相对路径 (AT_FDCWD) --> J2c[使用当前工作目录 fs->pwd]
J2a -- 相对路径 (dfd) --> J2d[使用 dfd 指向的目录]
J2b & J2c & J2d --> K[路径查找循环]
K --> K1[link_path_walk: 处理中间分量]
K1 --> K1a[walk_component → lookup_fast/lookup_slow<br>获取 dentry]
K1a --> K1b[step_into: 进入下一层目录,处理挂载点/符号链接]
K --> K2[open_last_lookups: 处理最后一个分量]
K2 --> K2a{是否 O_CREAT?}
K2a -- 否 --> K2b[lookup_fast 查找已有 dentry]
K2a -- 是 --> K2c[加锁父目录,调用 lookup_open]
K2c --> K2d[lookup_open: 尝试查找,若不存在则调用<br>文件系统 create 方法创建新 inode]
K2b & K2d --> K2e[step_into 或直接绑定 dentry]
K2e --> L[do_open]
L --> L1[检查 O_EXCL / 粘滞位 / 目录要求]
L --> L2[may_open: 权限检查]
L --> L3[vfs_open → do_dentry_open]
L3 --> L4[绑定 inode 和 f_op]
L4 --> L5[调用文件系统的 open 回调 (f_op->open)]
L5 --> L6[设置 FMODE_OPENED 等标志]
L --> L7[处理 O_TRUNC 截断]
L6 --> M[terminate_walk: 清理 nameidata 资源]
M --> N[返回 struct file * 给 do_sys_openat2]
N --> O[fsnotify_open: 触发文件打开通知]
O --> P[fd_install: 将 file 指针放入当前进程的 fdtable[fd]]
P --> Q[返回 fd 给用户态]
style A fill:#f9f,stroke:#333
style Q fill:#9f9,stroke:#333
open系统调用核心数据关联过程
对于open这个系统调用,上面分析了那么多。对于open操作来说,就是根据传入的路径打开文件返回一个文件描述符fd,这个fd是文件描述表中fdt的下标,对应一个file结构体,而这个file记录了path结构体和inode,path记录了dentry,dentry记录了inode。
说白了,搞懂这几个核心数据的关联过程,基本上就对于open系统调用的流程八九不离十了 根据上面笔记,笔者整理了这些数据的大致层级流程
用户态
└─ 调用 open("/path/file", flags)
│
▼
内核入口: do_sys_openat2
├─ get_unused_fd_flags() → alloc_fd()
│ └─ 在进程 fd 位图中标记 fd 为已占用,但 fdt->fd[fd] = NULL
│
└─ do_filp_open()
│
▼
路径查找: do_filp_open → path_openat
├─ file = alloc_empty_file(op->open_flag, current_cred()); ★★分配一个file结构体
├─ path_init() → 设置起始目录 (nd->path)
├─ link_path_walk() → 循环解析中间路径分量
├─ open_last_lookups() → 处理最后一个分量
│ └─ lookup_open()
│ ├─ dcache 命中? ──是──→ 取出已有 dentry
│ └─ 未命中 ──→ d_alloc_parallel() 占位
│ └─ 调用文件系统 i_op->lookup/create()
│ └─ d_instantiate() ★★建立 dentry→inode 关联
│
└─ 返回 nd->path (包含 dentry 和 inode)
│
▼
文件初始化: do_open → vfs_open → do_dentry_open
├─ vfs_open: file->f_path = nd->path ★★建立 file→dentry 关联
├─ do_dentry_open: file->f_inode = inode ★★建立 file→inode 关联
├─ file->f_op = inode->i_fop
├─ 调用 f_op->open()(若存在)
└─ 设置 FMODE_OPENED
│
▼
返回 do_sys_openat2
├─ fsnotify_open(f) → 发送文件打开事件
├─ fd_install(fd, file) ★★建立 fd→file 关联 (fdt->fd[fd] = file)
└─ 返回 fd 给用户态
最终数据结构关系图
当open系统调用返回数字3给用户态时,内核中的数据已形成如下稳固的引用链:
用户态进程
|
| (持有 int fd = 3)
v
当前进程 files_struct -> fdtable (fdt)
|
| fdt->fd[3]
v
struct file (f)
|-- f_count = 1 (被 fd 表持有)
|-- f_path (struct path)
| |-- mnt (挂载点)
| |-- dentry (目录项) ----------+
| |
|-- f_inode (指针) ---------------|-----+
|-- f_op (操作表) | |
| |
v v
struct dentry
|-- d_inode ----+
|
v
struct inode
|-- i_mode (类型/权限)
|-- i_ops (inode操作)
`-- i_mapping (地址空间)
