用户态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 函数以打开文件,

本文对Linux-5.15内核中用户态open()函数进行源码分析与实践。先介绍open操作流程,接着用perf工具追踪函数调用关系,详细分析open系统调用各入口函数。最后从实践角度学习相关结构体,编写用户态测试函数和内核模块并调试,加深对open()函数的认识。
最低0.47元/天 解锁文章
907






