用户态open()源码分析&实践
一、引言:
在阅读了相关文章之后,对于open进行源码分析:
我们在用户态使用open打开一个文件时,需要传入两个参数:文件路径和打开权限。
fd = open(file_path, O_RDONLY);//file_path文件路径,O_RDONLY打开权限
- 内核首先检查这个文件路径存在的合法性;
- 同时还需检查使用者是否有合法权限打开该文件;
- 如果一切顺利,那么内核将对访问该文件的进程创建一个file结构;
- open操作成功会返回一个fd文件描述符;
二、内核linux-5.15 open源码分析
在进入冗杂的源码分析前,可以先大致看一下下面这张图,对open的脉络有个有一定的印象:
2.0、通过perf打印open的函数调用关系:
在研究open()源码之前,先通过linux自带的perf工具追踪open所涉及的函数调用关系。
关于perf工具的安装和使用可以参考这篇文章:Perf的安装与简单使用_perf安装_问号小朋友的博客-优快云博客
这里默认虚拟机已经安装并可以正常使用perf工具;
2.0.1、测试程序:
先写一个用户态的调用open()函数的测试程序,内容如下:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h> // 包含头文件 <unistd.h>
int main() {
char *file_path = "test.txt";
int fd;
while(1){//通过while循环打开关闭文件,便于perf监测到open()函数;
fd = open(file_path, O_RDONLY);
if (fd == -1) {
perror("Error in opening file");
return -1;
} else {
printf("File opened successfully with file descriptor: %d\n", fd);
// 在此处可以执行其他操作,比如读取文件内容或者进行其他处理。
close(fd); // 关闭文件
}
printf("PID==>%d\n",getpid());//获得该进程pid,便于使用perf跟踪该进程;
}
return 0;
}
程序要点:
- while(1)语句循环调用open(),保证perf能监测到open()函数;
- 通过getpid()获得进程pid号,方便perf传参;
2.0.2、使用perf监测程序:
- 编译测试程序
gcc -o open_test open_test.c
; - 运行程序
./open_test
; - 在另一个终端通过命令行使用perf监测该程序
- 在运行的测试进程窗口会不停的open(),close(),输出该进程的进程号;
- 在另一个终端输入
sudo perf record -p 117069 -g -F 200 -- sleep 10
- 这里的-p 是通过进程号跟踪某一进程,
- -g是将跟踪结果输出为树状图;
- -F是跟踪频率;
- 跟踪结束后将结果流入一个文件中
sudo perf report >op.txt
; - 在op.txt中便可以找到跟踪结果的树状图;
2.1、open系统调用的入口函数:
open系统调用的入口函数应该为sys_open。不过,目前内核统一使用SYSCALL_DEFINEx宏来描述系统调用入口函数,因此可以在/fs/open.c文件中找到该入口函数,具体如下所示:
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)//通过该宏进行open的系统调用
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(AT_FDCWD, filename, flags, mode);//进入do_sys_open
}
而SYSCALL_DEFINE3宏定义在/include/syscalls.h中,此处若干个SYSCALL_DEFINE(包含1~6)被宏定义为SYSCALL_DEFINEx()函数,该函数通过传参实现系统调用;
#define SYSCALL_DEFINE1(name, ...) SYSCA LL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE_MAXARGS 6
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
从SYSCALL_DEFINE3
函数中可以看到其被映射到do_sys_open函数进行系统调用;
2.2、do_sys_open()
在/fs/open.c文件中定义着do_sys_open()函数,该函数的逻辑流程:
- 通过build_open_how函数将用户态的flags和mode转换成对应的内核态标志,并构建成一个open_how结构体用于传递给文件打开操作;
- 通过调用do_sys_openat2函数实际执行文件打开操作;
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
// 创建一个名为 "how" 的结构体,并通过 build_open_how 函数构建该结构体的实例
struct open_how how = build_open_how(flags, mode);
//调用 do_sys_openat2 函数,实际执行文件打开操作的地方,在指定的条件下打开文件
return do_sys_openat2(dfd, filename, &how);
}
这里牵扯到了两个函数build_open_how
以及do_sys_openat2
,前者并不是本次学习open的重地,其简要分析如下,后者do_sys_openat2
的详细分析参见下一节。
inline struct open_how build_open_how(int flags, umode_t mode)
{
struct open_how how = {
.flags = flags & VALID_OPEN_FLAGS, // 使用按位与运算将 flags 与 VALID_OPEN_FLAGS 进行位操作,过滤掉不合法的标志位,然后将结果存储在 how 结构体的 flags 成员中。
.mode = mode & S_IALLUGO, // 初始化结构体中的 mode 成员,按位与运算将 mode 与 S_IALLUGO 进行位操作,以确保只保留合法的文件权限标志位,结果存储在 how 结构体的 mode 成员中
};
/* O_PATH beats everything else. */
if (how.flags & O_PATH)
how.flags &= O_PATH_FLAGS; // 如果 flags 中包含 O_PATH 标志,那么将 flags 与 O_PATH_FLAGS 进行位操作,以确保只保留 O_PATH_FLAGS 中的标志位
/* Modes should only be set for create-like flags. */
if (!WILL_CREATE(how.flags))
how.mode = 0; // 如果 flags 不包含任何创建(create-like)标志,将 how 结构体的 mode 成员设置为 0,只有在打开文件时需要创建文件时,才会设置文件权限模式
return how; // 返回构建好的 how 结构体
}
总的来说,do_sys_open
函数负责处理用户空间传递过来的文件打开请求,构建相应的 open_how
结构体,然后调用 do_sys_openat2
函数完成文件的打开操作;
2.3、do_sys_openat2():
主要作用是根据传入的参数 dfd
(目录文件描述符)、filename
(要打开的文件名)、以及 how
(文件打开选项和模式)来执行文件打开操作。
- build_open_flags()构建文件打开标志;
- get_unused_fd_flags():根据文件打开标志获得一个未使用过的fd文件描述符;
- do_filp_open():调用 do_filp_open 函数来打开文件;
- fd_install():安装文件描述符,将文件描述符 fd 与打开的文件关联起来,即将fd与struct file进行绑定;
static long do_sys_openat2(int dfd, const char __user *filename,
struct open_how *how)
{
struct open_flags op;//用于存储文件打开标志
/*调用 build_open_flags 函数
来创建打开文件标志,返回文件描述符 fd*/
int fd = build_open_flags(how, &op);
struct filename *tmp;//定义一个指向 filename 结构体的指针 tmp
if (fd)
return fd;//fd不为0,则表示已经打开;
tmp = getname(filename);//通过 getname 函数获取文件名的 filename 结构体
if (IS_ERR(tmp))
return PTR_ERR(tmp);
/*获取一个该进程未使用的fd*/
fd = get_unused_fd_flags(how->flags);// 根据文件打开标志分配一个未使用的fd文件描述符
if (fd >= 0) {
/*生成一个struct file*/
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与struct file进行绑定*/
fd_install(fd, f);// 打开文件成功后,通知文件系统监视器
}
}
putname(tmp);// 释放文件名的 filename 结构体
return fd;
}
此函数中重点就是do_filp_open()
,通过do_filp_open函数实现真正的打开文件操作;
2.4、do_filp_open():
/fs/namei.c文件中定义着该函数。
struct file *do_filp_open(int dfd, struct filename *pathname,
const struct open_flags *op)
{
struct nameidata nd;//用于文件路径名的处理
int flags = op->lookup_flags;//从传入的 open_flags 结构体 op 中提取 lookup_flags 字段的值,存储在本地变量 flags 中。
struct file *filp;
set_nameidata(&nd, dfd, pathname, NULL);//设置 nameidata 结构体 nd 的属性,以准备打开文件
/*用 path_openat 函数以打开文件,
大概率事件:使用传入的 nameidata 结构体 nd 和 open_flags op,使用LOOKUP_RCU标志在rcu模式下快速打开文件*/
filp = path_openat(&nd, op, flags | LOOKUP_RCU);
/*打开失败*/
/*错误码-ECHILD,表示子进程不存在,再次尝试在ref-walk传统模式下打开文件*/
if (unlikely(filp == ERR_PTR(-ECHILD)))
filp = path_openat(&nd, op, flags);
/*错误码-ESTALE,文件状态需要更新,使用LOOKUP_REVAL标志验证文件路径是否正确*/
if (unlikely(filp == ERR_PTR(-ESTALE)))
filp = path_openat(&nd, op, flags | LOOKUP_REVAL);
/*恢复 nameidata 结构体的状态,以释放相关资源*/
restore_nameidata();
return filp;//成功返回打开的文件对象指针。错误返回错误指针ERR_PTR
}
本函数主要通过path_openat()函数打开文件,共尝试打开三次,代码逻辑及思路如下:
-
set_nameidata():设置 nameidata 结构体 nd 的属性,以准备打开文件;
-
首次尝试path_openat()函数打开文件:通过LOOKUP_RCU 标志,尝试在rcu(写时复制模式)下进行高效打开;
-
若rcu模式下打开失败
- 1、错误原因:子进程不存在 (-ECHILD)
- 再次尝试path_openat()函数打开文件:在传统模式(ref-walk模式)下打开文件;
- 2、错误原因:文件状态需要更新 (-ESTALE)
- 再次尝试path_openat()函数打开文件:通过LOOKUP_REVAL标志,检查文件路径是否发生变化
- 1、错误原因:子进程不存在 (-ECHILD)
-
restore_nameidata():恢复 nameidata 结构体的状态,以释放相关资源;
2.5、path_openat():
/fs/namei.c文件中定义着该函数。
path_openat()
该函数通过file = alloc_empty_file(op->open_flag, current_cred());
为该进程生成了一个struct file
,最后由do_open()
函数完成剩下的打开动作。
/*在给定的 nameidata 结构 nd 中打开文件*/
static struct file *path_openat(struct nameidata *nd,
const struct open_flags *op, unsigned flags)
{
struct file *file;//在给定的 nameidata 结构 nd 中打开文件
int error;//用于存储错误码
/*创建一个空的文件对象,
使用传入的 open_flags 结构体 op 中的 open_flag 标志位
和当前进程的凭证信息*/
file = alloc_empty_file(op->open_flag, current_cred());
if (IS_ERR(file))//文件对象创建是否成功;
return file;
/*根据文件对象的标志位file->f_flags,进行不同的处理分支。*/
if (unlikely(file->f_flags & __O_TMPFILE)) {//该文件是临时文件
error = do_tmpfile(nd, flags, op, file);//调用 do_tmpfile 函数处理临时文件的情况
} else if (unlikely(file->f_flags & O_PATH)) {//路径
error = do_o_path(nd, flags, file);//调用do_o_path处理
} else {
//(大概率执行)执行默认路径打开操作
const char *s = path_init(nd, flags);//初始化 路径字符串 并存储在变量 s 中
/*在遍历路径进行文件查找的过程中,持续迭代直到找到文件或者出现错误。*/
while (!(error = link_path_walk(s, nd)) &&//link_path_walk 用于遍历路径并查找文件
(s = open_last_lookups(nd, file, op)) != NULL)//open_last_lookups 用于执行打开文件的最后一次查找
;
/*如果在遍历路径后没有发生错误,
调用 do_open 函数以打开文件。*/
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);
}
本函数主要通过do_open函数打开文件,代码逻辑及思路如下:
- alloc_empty_file():创建一个空的文件对象;
- 根据文件对象的标志位file->f_flags,进行不同的处理分支:
- 小概率事件的处理:
- 1、创建临时文件的请求:do_tmpfile()处理临时文件;
- 2、仅为了获取文件路径:do_o_path处理仅获取路径的情况;
- 大概率事件:执行默认路径的打开操作:
- 1、通过while()循环进行路径解析,查找文件;
- 2、路径解析成功,找到文件,通过do_open()函数来打开文件;
- 3、终止路径解析过程;
- 小概率事件的处理:
2.6、do_open():
/fs/namei.c文件中定义着该函数。
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))) {
// 如果文件的模式不包含FMODE_OPENED或FMODE_CREATED标志
error = complete_walk(nd); // 调用complete_walk函数,可能完成一些路径遍历操作
if (error)
return error;
}
/*审计文件操作*/
if (!(file->f_mode & FMODE_CREATED)) {//已经创建,不包含FMODE_CREATED标志
audit_inode(nd->name, nd->path.dentry, 0); // 记录审核日志,跟踪打开的文件
}
/*获取挂载点的用户命名空间:*/
mnt_userns = mnt_user_ns(nd->path.mnt); // 使用 mnt_user_ns 函数获取与挂载点相关联的用户命名空间
/*处理文件创建*/
/*要创建文件*/
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; // 访问模式设置为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; // 设置截断标志为真
}
/*打开文件并检查文件完整性*/
/*may_open尝试打开文件*/
error = may_open(mnt_userns, &nd->path, acc_mode, open_flag); // 尝试打开文件
/*无错误,但未打开文件,调用vfs_open()函数*/
if (!error && !(file->f_mode & FMODE_OPENED)) {
error = vfs_open(&nd->path, file); // 如果没有错误且文件没有打开,则调用vfs_open函数打开文件
}
/*无错误,调用 ima_file_check检查文件完整性*/
if (!error) {
error = ima_file_check(file, op->acc_mode); // 检查文件完整性
}
/*无错误,并且需要截断文件,用handle_truncate来执行截断*/
if (!error && do_truncate) {
error = handle_truncate(mnt_userns, file); // 处理文件截断
}
/*低概率事件*/
if (unlikely(error > 0)) {
WARN_ON(1); // 如果错误大于0,发出警告
error = -EINVAL; // 将错误码设置为无效参数错误
}
if (do_truncate) {
mnt_drop_write(nd->path.mnt); // 如果需要截断文件,释放挂载点的写许可
}
return error; // 返回最终的错误码
}
此函数主要是处理文件的打开操作,并且包含了各种条件检查和错误处理,以确保文件打开操作能够按照要求进行。其中的核心部分就是通过调用vfs_open函数来执行打开文件操作,代码逻辑及思路如下:
- 检查文件是否已经打开或已创建:complete_walk();
- 审计文件,记录审核日志,跟踪打开的文件:audit_inode();
- 处理文件创建;
- 检查目录查找权限;
- 处理截断操作;
- 打开文件并检查文件完整性:两次尝试打开文件:
- 1、通过may_open函数来打开文件;
- 2、通过vfs_open函数来打开文件;
2.7、vfs_open():
/fs/open.c文件中
显然该函数的主要功能是通过
int vfs_open(const struct path *path, struct file *file)
{
// 将文件结构体中的f_path字段设置为传入的路径结构体path的拷贝
file->f_path = *path;
// 调用do_dentry_open函数来实际执行文件打开操作
// 传递的参数包括file结构体、与dentry相关的inode(文件节点),以及一个空指针(NULL)
return do_dentry_open(file, d_backing_inode(path->dentry), NULL);
}
-
将文件路径信息存储在
file
结构体中 -
调用
do_dentry_open
函数来实际执行文件的打开操作。该操作涉及与文件节点(inode)的相关操作和可能的附加选项。这是Linux内核中处理文件打开的基本操作之一
2.8、do_dentry_open():
do_dentry_open()函数首先对于为进程生成的struct file进行赋值,与该路径(/dev/hello)的inode进行绑定,除此之外,最终要的是对于struct file的f_op进行赋值,该f_op的赋值会导致最终访问该文件时进行的write()、read()的行为,这个是重中之重。
该函数执行一系列的操作,包括设置文件的路径、权限检查、安全性检查、获取文件操作结构等。其中,它还设置了文件的模式,如读取、写入、原子位置等,以及根据文件操作结构中的函数判断文件是否可读或可写。最后,它初始化了文件读取加速状态。
static int do_dentry_open(struct file *f,
struct inode *inode,
int (*open)(struct inode *, struct file *))
{
static const struct file_operations empty_fops = {};// 初始化一个空的文件操作结构体作为默认值
int error;
path_get(&f->f_path);//文件路径;
f->f_inode = inode;//文件索引节点的设置;
f->f_mapping = inode->i_mapping;
f->f_wb_err = filemap_sample_wb_err(f->f_mapping);
f->f_sb_err = file_sample_sb_err(f);
/*文件是仅路径访问的标志,则设置文件模式为路径和已打开状态,并返回*/
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_WRITE && !special_file(inode->i_mode)) {
error = get_write_access(inode);// 获取对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;// 设置文件模式为写模式
}
/* POSIX.1-2008/SUSv4 Section XSI 2.9.7 */
/*根据POSIX标准,如果文件是常规文件或目录,则设置为原子定位模式*/
if (S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode))
f->f_mode |= FMODE_ATOMIC_POS;
/*获取索引节点对应的文件操作函数表*/
f->f_op = fops_get(inode->i_fop);//获取文件操作结构
if (WARN_ON(!f->f_op)) {
error = -ENODEV;
goto cleanup_all;
}
/*安全检查文件打开操作*/
error = security_file_open(f);
if (error)
goto cleanup_all;
/*检查并处理文件锁*/
error = break_lease(locks_inode(f), f->f_flags);
if (error)
goto cleanup_all;
/* normally all 3 are set; ->open() can clear them if needed */
/*设置文件模式,包括lseek、pread、pwrite 定位、预读、写入*/
f->f_mode |= FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;
if (!open)
/*如果open函数为空,则使用文件操作结构中的open函数*/
open = f->f_op->open;
if (open) {
/*调用open函数打开文件*/
error = open(inode, f);
if (error)
goto cleanup_all;
}
/*目前文件已经打开*/
f->f_mode |= FMODE_OPENED;//设置文件模式为已打开;
/*只有读模式被设置,增加inode的读计数*/
if ((f->f_mode & (FMODE_READ | FMODE_WRITE)) == FMODE_READ)
i_readcount_inc(inode);
/*有读操作 或迭代读操作,设置文件模式为 可读*/
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;
f->f_write_hint = WRITE_LIFE_NOT_SET;// 设置写入提示为未设置状态
f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);// 清除特定的文件标志
/*初始化文件读取加速状态*/
file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping);
/* NB: we're sure to have correct a_ops only after f_op->open */
/*以直接I/O模式打开文件*/
if (f->f_flags & O_DIRECT) {
/*在直接I/O模式下,这里检查文件的地址映射结构(f_mapping)的地址操作结构(a_ops)是否存在
以及是否有direct_IO函数。如果不存在,返回错误-EINVAL。*/
if (!f->f_mapping->a_ops || !f->f_mapping->a_ops->direct_IO)
return -EINVAL;
}
/*
* XXX: Huge page cache doesn't support writing yet. Drop all page
* cache for this file before processing writes.
*/
/*指出巨大页缓存不支持写入操作,因此在处理写入操作之前,需要删除所有与该文件相关的页面缓存*/
/*文件被以写入模式打开*/
if (f->f_mode & FMODE_WRITE) {
/*
* Paired with smp_mb() in collapse_file() to ensure nr_thps
* is up to date and the update to i_writecount by
* get_write_access() is visible. Ensures subsequent insertion
* of THPs into the page cache will fail.
*/
/*这是一个内存屏障,用于确保下面的操作有序进行*/
smp_mb();
/*如果有巨大页缓存,则需要清除这些缓存*/
if (filemap_nr_thps(inode->i_mapping))
truncate_pagecache(inode, 0);
}
return 0;
cleanup_all:
if (WARN_ON_ONCE(error > 0))
error = -EINVAL;
fops_put(f->f_op);//释放文件操作结构f->f_op
if (f->f_mode & FMODE_WRITER) {
put_write_access(inode);//用于减少对文件的写入访问计数
__mnt_drop_write(f->f_path.mnt);//用于减少文件路径的写入访问计数
}
cleanup_file:
path_put(&f->f_path);//减少路径结构f->f_path的引用计数
f->f_path.mnt = NULL;
f->f_path.dentry = NULL;
f->f_inode = NULL;
return error;
}
2.9、do_dentry_open()之后具体使用的函数:
do_dentry_open()函数这么多内容,无非是一些文件模式的检查与更改、安全性检查等。其中最重要的就是将inode—>i_op
所指向的操作函数(一些钩子函数)赋给f—>f_op
所指向的操作函数;再通过将f—>f_op—>open
函数赋给具体open()
函数,从而达到真正的通过open()打开文件;
f->f_op = fops_get(inode->i_fop);
/*...*/
if (!open)
/*如果open函数为空,则使用文件操作结构中的open函数*/
open = f->f_op->open;
if (open) {
/*调用open函数打开文件*/
error = open(inode, f);
if (error)
goto cleanup_all;
}
do_dentry_open()具体通过fops_get()函数从inode_operations结构体中获得操作函数集(如:mkdir,atomic_open等),并将这些钩子函数所指向的具体操作函数的地址赋给f_op所指向的struct file_operations操作函数集(同为钩子函数),下图为个人理解:
此时open=f->f_op->open=inode->i_op->atomic_open不同的文件系统有自己定义的open函数
我们可以再次通过2.0章节打印的函数关系图来理解open=f->f_op->open=inode->i_op->atomic_open
,当然这里分析的是特例,不同的文件系统调用的具体open()函数是不同的,此处所用的是ext4文件系统;
显然函数do_dentry_open调用了ext4_file_open()函数,那么这个ext4_file_open()函数是什么?在哪里?
ext4_file_open()函数就是文件系统ext4具体定义的open操作函数,在目录/fs/ext4/file.c文件中,具体代码如下:
static int ext4_file_open(struct inode *inode, struct file *filp)
{
int ret;
if (unlikely(ext4_forced_shutdown(EXT4_SB(inode->i_sb))))
return -EIO;
ret = ext4_sample_last_mounted(inode->i_sb, filp->f_path.mnt);
if (ret)
return ret;
ret = fscrypt_file_open(inode, filp);
if (ret)
return ret;
ret = fsverity_file_open(inode, filp);
if (ret)
return ret;
/*
* Set up the jbd2_inode if we are opening the inode for
* writing and the journal is present
*/
if (filp->f_mode & FMODE_WRITE) {
ret = ext4_inode_attach_jinode(inode);
if (ret < 0)
return ret;
}
filp->f_mode |= FMODE_NOWAIT | FMODE_BUF_RASYNC;
return dquot_file_open(inode, filp);
}
由于是特定文件系统的具体操作函数,我们便不做过多的注释和分析,只关注他调用了fscrypt_file_open()函数
。
在阅读/fs/ext4/file.c文件时,我们便发现在该文件中给struct file_operations结构体以及struct inode_operations结构体内的钩子函数做了具体实现并初始化;
const struct file_operations ext4_file_operations = {
.llseek = ext4_llseek,
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
.iopoll = iomap_dio_iopoll,
.unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext4_compat_ioctl,
#endif
.mmap = ext4_file_mmap,
.mmap_supported_flags = MAP_SYNC,
.open = ext4_file_open,
.release = ext4_release_file,
.fsync = ext4_sync_file,
.get_unmapped_area = thp_get_unmapped_area,
.splice_read = generic_file_splice_read,
.splice_write = iter_file_splice_write,
.fallocate = ext4_fallocate,
};
const struct inode_operations ext4_file_inode_operations = {
.setattr = ext4_setattr,
.getattr = ext4_file_getattr,
.listxattr = ext4_listxattr,
.get_acl = ext4_get_acl,
.set_acl = ext4_set_acl,
.fiemap = ext4_fiemap,
.fileattr_get = ext4_fileattr_get,
.fileattr_set = ext4_fileattr_set,
};
三、实践:
经过上述的源码分析,对于用户态的open()是如何实现的有了一定的了解,这一章将从实践的角度对文件系统进行探索。
3.1、相关结构体的学习:
3.1.1、file:
struct file {
union {
struct llist_node fu_llist; // 联合体的一个成员,可能用于将文件对象添加到一个链表中
struct rcu_head fu_rcuhead; // 联合体的另一个成员,可能用于支持RCU(Read-Copy Update)操作
} f_u;
struct path f_path; // 一个`struct path`结构,用于存储文件的路径信息
struct inode *f_inode; // 一个指向关联的`inode`结构体的指针,通常用于表示文件的相关属性
const struct file_operations *f_op; // 一个指向文件操作的指针,包括读、写、关闭等操作的函数指针集合
spinlock_t f_lock; // 一个自旋锁,用于保护该文件结构的并发访问,防止多线程或多处理器竞争
enum rw_hint f_write_hint; // 一个枚举类型,用于指示文件写入的倾向,可能用于优化文件I/O
atomic_long_t f_count; // 原子长整数,表示对该文件的引用计数
unsigned int f_flags; // 无符号整数,表示文件的标志,例如读、写、追加等
fmode_t f_mode; // 文件打开模式,包括O_RDONLY、O_WRONLY等
struct mutex f_pos_lock; // 一个互斥锁,用于保护文件位置(offset)的并发访问
loff_t f_pos; // 文件位置,表示当前读写操作的位置
struct fown_struct f_owner; // 文件拥有者信息,用于实现文件锁
const struct cred *f_cred; // 指向与文件关联的凭证(credential)结构的指针
struct file_ra_state f_ra; // 文件读取预取(readahead)状态
u64 f_version; // 64位整数,表示文件版本信息
#ifdef CONFIG_SECURITY
void *f_security; // 一个指向安全模块的指针,可用于实现文件访问控制
#endif
void *private_data; // 用于存储文件私有数据的指针,通常由文件系统或驱动程序使用
#ifdef CONFIG_EPOLL
struct hlist_head *f_ep; // 一个指向哈希链表头的指针,用于与`fs/eventpoll.c`中的事件挂钩
#endif
struct address_space *f_mapping; // 一个指向地址空间结构的指针,表示文件的地址空间
errseq_t f_wb_err; // 表示写入错误的错误序列号
errseq_t f_sb_err; // 用于`syncfs`的同步写错误的错误序列号
} __randomize_layout
__attribute__((aligned(4))); // 使用`__randomize_layout`和`aligned(4)`属性来确保结构体的布局和对齐方式,以避免奇怪的问题。
3.1.2、flies_struct
struct files_struct {
/*
* read mostly part
*/
atomic_t count; // 引用计数,用于跟踪结构体的引用次数,以确保在不再需要时可以正确地释放资源
bool resize_in_progress; // 布尔值,用于表示文件描述符表的扩展是否正在进行,可能用于并发控制
wait_queue_head_t resize_wait; // 等待队列头,可能用于进程等待文件描述符表的扩展完成
struct fdtable __rcu *fdt; // 一个指向`fdtable`结构体指针的指针,使用了`__rcu`标记,表示这是一个"Read-Copy Update"类型的指针,用于支持多线程并发读取文件描述符表
struct fdtable fdtab; // 一个`fdtable`结构体,用于存储当前进程的文件描述符表
/*
* written part on a separate cache line in SMP
*/
spinlock_t file_lock ____cacheline_aligned_in_smp; // 自旋锁,用于在多处理器系统中保护对文件描述符表的并发访问,`____cacheline_aligned_in_smp`用于确保自旋锁被分配到单独的缓存行,以减少多处理器系统中的竞争
unsigned int next_fd; // 一个无符号整数,表示下一个可用的文件描述符
unsigned long close_on_exec_init[1]; // 一个包含1个元素的无符号长整型数组,用于存储进程初始时需要关闭的文件描述符的位图
unsigned long open_fds_init[1]; // 一个包含1个元素的无符号长整型数组,用于存储进程初始时打开的文件描述符的位图
unsigned long full_fds_bits_init[1]; // 一个包含1个元素的无符号长整型数组,用于存储文件描述符表的初始化状态
struct file __rcu *fd_array[NR_OPEN_DEFAULT]; // 一个包含`NR_OPEN_DEFAULT`个元素的`file`指针数组,用于存储文件对象指针,可能用于管理进程打开的文件
// 这个结构体的不同部分分别用于存储读取频繁的信息和写入频繁的信息,以减少竞争并提高性能。
};
struct file __rcu *fd_array[NR_OPEN_DEFAULT];
3.1.3、fdtable
文件描述符表是一个用于跟踪和管理进程打开文件的数据结构。
struct fdtable {
unsigned int max_fds; // 存储文件描述符表中的最大文件描述符数
struct file __rcu **fd; // 一个指向文件指针数组的指针,这个数组用于存储打开的文件对象指针
// 这里使用了 __rcu 标记,表示这是一个"Read-Copy Update"类型的指针
// 它用于支持多线程并发读取文件描述符表
unsigned long *close_on_exec; // 一个指向位图数组的指针,用于标记在执行新程序时需要关闭的文件描述符
// close_on_exec 是一个位图,其中每一位对应一个文件描述符,如果位被设置为1,表示需要在执行新程序时关闭相应文件描述符
unsigned long *open_fds; // 一个指向位图数组的指针,用于标记文件描述符的状态,表示文件描述符是否处于打开状态
// open_fds 是一个位图,其中每一位对应一个文件描述符,如果位被设置为1,表示相应的文件描述符处于打开状态
unsigned long *full_fds_bits; // 一个指向位图数组的指针,用于标记文件描述符表中已分配的文件描述符的状态
// full_fds_bits 是一个位图,其中每一位对应一个文件描述符,如果位被设置为1,表示相应的文件描述符已分配
struct rcu_head rcu; // 用于实现"Read-Copy Update"机制的头结构,在文件描述符表被释放时用于资源回收
};
3.2、用户态测试函数:
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h> // 包含头文件 <unistd.h>
int main() {
char *file_path1 = "/home/xhb/mycode/test11/test.txt";
char *file_path2 = "/home/xhb/lmp/eBPF_Supermarket/CPU_Subsystem/README.md";
FILE *fp1,*fp2;
fp1= fopen(file_path1, "r");
if (fp1 == NULL) {
perror("Error in opening file");
exit(EXIT_FAILURE);
} else {
printf("File opened successfully 1 \n");
// 在此处可以执行其他操作,比如读取文件内容或者进行其他处理。
char tmp[10000];
while(fgets(tmp,sizeof(tmp),fp1)!=NULL)
printf("%s",tmp);
}
printf("\n");
fp2= fopen(file_path2, "r");
if (fp2 == NULL) {
perror("Error in opening file");
exit(EXIT_FAILURE);
} else {
printf("File opened successfully 2 \n");
// 在此处可以执行其他操作,比如读取文件内容或者进行其他处理。
char tmp[10000];
while(fgets(tmp,sizeof(tmp),fp2)!=NULL)
printf("%s",tmp);
}
printf("PID==>%d\n",getpid());
getchar();
fclose(fp1); // 关闭文件
fclose(fp2); // 关闭文件
//while(1);
return 0;
}
3.3、内核模块:
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/sched/signal.h>
#include <linux/pid.h>
#include <linux/fdtable.h>
#include <linux/fs.h>
#include <linux/dcache.h>
MODULE_LICENSE("GPL"); //许可证
static int pid = 156;
module_param(pid, int, 0644);
MODULE_PARM_DESC(pid, "give a pid to print this process' infomation of it's !");
// void print_fmode(fmode_t file_mod,struct file *file){
// /*处理权限*/
// file_mod = file->f_mode;
// int user_permissions = (file_mod & S_IRUSR) | ((file_mod & S_IWUSR) >> 1) | ((file_mod & S_IXUSR) >> 2);//用户权限
// int group_permissions = (file_mod & S_IRGRP) | ((file_mod & S_IWGRP) >> 1) | ((file_mod & S_IXGRP) >> 2);//组权限
// int other_permissions = (file_mod & S_IROTH) | ((file_mod & S_IWOTH) >> 1) | ((file_mod & S_IXOTH) >> 2);//其他权
// //int file_permissions = user_permissions * 64 + group_permissions * 8 + other_permissions;//权限;
// printk("文件权限:\n");
// printk("用户权限:\n");
// printk("读权限:%s\n", user_permissions & S_IRUSR ? "允许" : "拒绝");
// printk("写权限:%s\n", user_permissions & S_IWUSR ? "允许" : "拒绝");
// printk("执行权限:%s\n", user_permissions & S_IXUSR ? "允许" : "拒绝");
// printk("组权限:\n");
// printk("读权限:%s\n", group_permissions & S_IRGRP ? "允许" : "拒绝");
// printk("写权限:%s\n", group_permissions & S_IWGRP ? "允许" : "拒绝");
// printk("执行权限:%s\n", group_permissions & S_IXGRP ? "允许" : "拒绝");
// printk("其他权限:\n");
// printk("读权限:%s\n", other_permissions & S_IROTH ? "允许" : "拒绝");
// printk("写权限:%s\n", other_permissions & S_IWOTH ? "允许" : "拒绝");
// printk("执行权限:%s\n", other_permissions & S_IXOTH ? "允许" : "拒绝");
// }
static int __init print_pid_fs_info_init(void) {
struct task_struct *task;
struct files_struct *files;//打开的文件集
struct fdtable *files_table;
struct file *file;//具体打开的文件相关信息;
struct path file_path;
fmode_t file_mod;//文件的模式;
struct inode *inode;//文件的索引号
unsigned int file_flags;//文件标志,读写?
struct dentry *dentry;
// 通过PID找到task_struct
task = pid_task(find_vpid(pid), PIDTYPE_PID);
if (!task) {
printk(KERN_INFO "PID %d could not be found.\n", pid);
return -ESRCH;
}
//1.打印进程号
printk(KERN_INFO "PID ===> %d.\n", pid);
printk(KERN_INFO "COMM ===> %s.\n", task->comm);
//2.打印file
/*通过task_struct结构体中的files字段指向struct file结构体,再通过files_fdtable函数找到fdtable文件描述符;*/
files_table = files_fdtable(task->files);
//files_table =task->files->fdtab;//获得fdtab
int i;
for (i = 0; i < files_table->max_fds; i++) {
file = files_table->fd[i];//循环遍历该进程的所有file
if (file) {
/*处理路径信息:*/
file_path = file->f_path;
char *tmp;
path_get(&file_path); // Increment reference count of the path,增加路径引用次数
tmp = (char *)__get_free_page(GFP_KERNEL);
if (!tmp) {
printk(KERN_INFO "Memory allocation failed\n");
return -ENOMEM;
}
char *filepath;
filepath = d_path(&file_path, tmp, PAGE_SIZE);
if (IS_ERR(filepath))
{ // Make sure d_path() didn't fail
free_page((unsigned long)tmp);
continue;
}
/*处理权限*/
file_mod = file->f_mode;
int user_permissions = (file_mod & S_IRWXU) >> 6;//用户权限
int group_permissions = (file_mod & S_IRWXG) >> 3;//组权限
int other_permissions = file_mod & S_IRWXO;//其他权限
int file_permissions = user_permissions * 64 + group_permissions * 8 + other_permissions;//权限;
file_flags = file->f_flags;//文件标志,读写?
dentry = file_path.dentry;//dentry;
inode = file_inode(file);
/*打印*/
//file info:
printk(KERN_INFO "file info:\n");
printk(KERN_INFO "FD:%d->Path: %s\n", i, filepath);
printk(KERN_INFO " ref-count: %lu\n", file->f_count);
printk(KERN_INFO " file-version: %lu\n", file->f_version);
//print_fmode(file_mod,file);
printk(KERN_INFO " file-mod(Prime): %ld\n", file_mod);//原始mod
printk(KERN_INFO " user_permissions: %d\n", user_permissions);//八进制权限
printk(KERN_INFO " group_permissions: %d\n", group_permissions);//八进制权限
printk(KERN_INFO " other_permissions: %d\n", other_permissions);//八进制权限
printk(KERN_INFO " file_permissions: %d\n", file_permissions);//八进制权限
printk(KERN_INFO " file_flags: %d\n", file_flags);//八进制权限
//dentry info:
printk(KERN_INFO "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
printk(KERN_INFO "dentry info:\n");
printk(KERN_INFO " dentry-parentname: %s\n", dentry->d_parent->d_name.name);
printk(KERN_INFO " dentry-name: %s\n", dentry->d_name.name);
printk(KERN_INFO " dentry-inode-id: %lu\n", dentry->d_inode->i_ino);
//inode info
printk(KERN_INFO "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
printk(KERN_INFO "inode info:\n");
printk(KERN_INFO " Inode: %lu\n", inode->i_ino);
printk(KERN_INFO " nlink-count: %lu\n", inode->i_nlink);
printk(KERN_INFO " dev-id: %lu\n", inode->i_rdev);
printk(KERN_INFO " block-size: %lu\n", inode->i_blkbits);
printk(KERN_INFO " block-count: %lu\n", inode->i_blocks);
printk(KERN_INFO " ref_count: %lu\n", inode->i_count);
free_page((unsigned long)tmp);
path_put(&file_path); // Decrement the reference count
printk(KERN_INFO "--------------------------------------------------------------------------------------------------------------\n");
}
}
return 0;
}
static void __exit print_pid_fs_info_exit(void) {
printk(KERN_INFO "Module unloaded.\n");
}
module_init(print_pid_fs_info_init);
module_exit(print_pid_fs_info_exit);
3.4、调试截图:
在对用户态程序编译,内核模块编译之后,可以开始调试工作:
- 运行pr_file_test程序,他将两个打开的文件内容打印到终端,并输出pid号,之后他暂停在getchar()处,便于我们上载内核模块。
- 上载内核模块:
通过正在运行的测试进程打印出的pid号,我们上载内核模块:
- 查看日志:
通关dmesg
查看输出:
我们依次可以看到fd=0,1,2时,是标志输入输出以及标准错误,,其路径在/dev/pts/3下;
当fd>2时,便是我们新打开的文件了。
四、个人感悟:
在通过源码分析以及编写内核模块探索几个结构体的信息之后,个人在知识层面对于inode、file、files_struct、fdtable等结构体有了一定的了解,对于用户态的open()函数有了更深的认识;在实践方面,熟悉了perf的配置和使用,通过结合perf工具,我对open函数最终的落脚点有了更具体的认识,知道最终通过do_dentry_open()函数调用的具体的“open”是根据不同的文件系统而定的,写在哪个具体的文件中。