1. 前言
本专题我们开始学习虚拟文件系统VFS的相关内容。本专题主要参考了《存储技术原理分析》、ULA、ULK的相关内容。本文主要记录open文件的过程,让我们能够了解文件描述符的file operations到底来源于哪里。
kernel版本:5.10
FS: minix
平台:arm64
注:
为方便阅读,正文标题采用分级结构标识,每一级用一个"-“表示,如:两级为”|- -", 三级为”|- - -“
2. 打开文件过程对象关系
-
task_struct
task_struct中的files指向了files_struct,指示当前进程打开了哪些文件 -
files_struct
files_struct是fdttable的一个封装,它通过fdt指向fdttable,它管理了一个打开的文件描述符指针数组fd_array -
fdtable
fdtable的fd指向了files_struct的打开文件描述符指针数组fd_array,通过fdtable的fd可以快速访问files_struct的fd_array数组 -
inode
inode用于维护文件的元数据,它在open文件时获取或创建 -
file
文件描述符file维护了当前路径和根路径path,path是一个<vfsmount,dentry>二元组,指示文件是属于哪个文件系统下的哪个dentry。file的f_op指向file_operations结构体,它来源于file对应的inode->i_fop,inode->i_fop将在创建 inode时被初始化,file->f_op是在打开文件时被初始化。f_mapping指向了file拥有的page cache地址空间(address_space),对于普通文件来源于inode->address_space,对于块设备文件(裸设备)来源于bdev->address_space -
address_space
address_space作为inode的内嵌结构体而存在,在创建inode时一同创建,它维护了一个page cache tree
3. do_sys_open
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
|--do_sys_open(AT_FDCWD, filename, flags, mode);
|--do_sys_openat2(dfd, filename, &how);
| //将flags标志转换后放入op->flags
|--build_open_flags(how, &op);
| //将文件路径名从用户空间复制到内核空间
|--tmp = getname(filename);
| // 从进程打开的文件描述表current->files分配一个未使用的file*,并返回它的索引
|--fd = get_unused_fd_flags(how->flags);
| //执行file->f_ops->open函数,返回file
|--struct file *f = do_filp_open(dfd, tmp, &op)
| //file指针赋值给current->files->fd[fd]
|--fd_install(fd, f);
open系统调用主要是通过解析路径名filename,并将路径名的最后分量生成最终的<vfsmount, dentry>二元组保存到路径上下文nd中(对于新建的文件则需要创建dentry),对于最后的路径分量通过其dentry获取到inode,如果inode为空还要为之创建inode,最后通过nd和inode来填充file,并返回file
-
getname(filename):首先从用户空间将文件路径名拷贝到内核空间
-
get_unused_fd_flags(flags):从进程打开的文件描述符表current->files->fd_array数组中分配一个未使用的file指针
-
do_filp_open: 创建file描述符并填充分配的file描述符
-
fd_install:将文件描述符和句柄fd联系起来,即将文件描述符保存在打开文件表current->files的句柄fd索引位置
|- -do_filp_open
struct file *f = do_filp_open(dfd, tmp, &op)
|--struct nameidata nd;
|--set_nameidata(&nd, dfd, pathname);
|--filp = path_openat(&nd, op, flags | LOOKUP_RCU);
|--file = alloc_empty_file(op->open_flag, current_cred());
| //初始化路径上下文开始解析文件路径名
|--const char *s = path_init(nd, flags);
|--while (!(error = link_path_walk(s, nd)) &&
| (s = open_last_lookups(nd, file, op)) != NULL);
|--do_open(nd, file, op);
| |--error = may_open(&nd->path, acc_mode, open_flag);
|--if (!error && !(file->f_mode & FMODE_OPENED))
vfs_open(&nd->path, file);
|--f->f_inode = inode;
f->f_mapping = inode->i_mapping;
f->f_op = fops_get(inode->i_fop);
.do_filp_open:填充分配的file描述符并返回
-
alloc_empty_file:创建file描述符
-
path_init:主要初始化最初的路径上下文nameidata(主要是nd->path),后续的查找过程将从此初始的nameidata进行。这里根据文件名name和dfd,可以将nd->path初始化为三种情况:
(1)如果name的第一个字符为’/’,将nd->path初始化为当前进程的根目录;
(2)如果dfd为AT_FDCWD,将 nd->path初始化为当前进程的当前路径;
(3)其它情况则根据具体设置相对路径 -
link_path_walk:解析文件路径名,由于使用了LOOKUP_PARENT,因此将得到最后一级目录的<vfsmount,dentry>二元组保存到路径上下文nd,否则保存的是最后一级路径名的<vfsmount,dentry>
注意:此处设置了标志LOOKUP_PARENT,这样在执行link_path_walk之后路径上下文将保存的是最后一个目录的上下文。 -
open_last_lookups:分析最后的路径分量,在open_last_lookups之前nd为最后一级目录的路径上下文,nd->last保存最后一级路径名分量的信息,open_last_lookups是将最后一级路径名单独拿出来进行处理,期间会为之获取dentry以及inode,如果为空则会创建,并用此inode和nd填充前述的file文件描述符
-
do_open:主要对file描述符进行初始化,并最终调用具体文件系统的open回调,其中f->f_op = fops_get(inode->i_fop);用来初始化file的f_op回调
|- - -open_last_lookups
open_last_lookups(struct nameidata *nd,struct file *file, const struct open_flags *op)
| //nd->path.dentry保存了最后一级目录的dentry
|--struct dentry *dir = nd->path.dentry;
| int open_flag = op->open_flag;
| struct dentry *dentry;
| //处理非创建文件的情况
|--if (!(open_flag & O_CREAT))
| //获取到最后一级分量的dentry
| dentry = lookup_fast(nd, &inode, &seq);
| if (likely(dentry))
| goto finish_lookup;
| //处理新建文件的情况
|--if (open_flag & (O_CREAT | O_TRUNC | O_WRONLY | O_RDWR))
| mnt_want_write(nd->path.mnt);
|--dentry = lookup_open(nd, file, op, got_write);
| |--struct dentry *dir = nd->path.dentry;
| | //查找到最后一级分量的dentry
| |--dentry = d_lookup(dir, &nd->last);
| | //如果dentry为空,则创建dentry
| |--for (;;)
| | if (!dentry) dentry = d_alloc_parallel(dir, &nd->last, &wq);
| | //创建inode
| |--if (!dentry->d_inode && (open_flag & O_CREAT))
| error = dir_inode->i_op->create(dir_inode, dentry, mode, open_flag & O_EXCL);
finish_lookup:
|--step_into(nd, WALK_TRAILING, dentry, inode, seq);
lookup_open:如果dentry->d_inode为空会调创建inode,其最终会调用到具体文件系统的create回调,对于minix文件系统就会调用minix_create->minix_new_inode创建minix_inode_info,而后者内嵌vfs inode;创建完inode后会通过minix_set_inode对inode进行初始化,minix_set_inode根据文件类型(目录、文件、链接)设置inode的i_op,i_fop(file->file_operation的来源),i_mapping->a_ops,对于字符设备、块设备则会调用init_special_inode进行初始化
参考文档
奔跑吧,Linux内核