注:本文分析基于3.10.0-693.el7内核版本,即CentOS 7.4
1、函数原型
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
参数说明:
pathname: 表示要打开的文件路径,可以是绝对路径也可以是相对路径;
flags: 表示打开文件所采用的操作,用于控制可读、可写、创建、截断等
注:以下模式有且只能指定其中一个
O_RDONLY:只读模式打开
O_WRONLY:只写模式打开
O_RDWR:可读可写打开
指定读写模式后还能位与一些其他控制标志,来控制打开的操作。
O_APPEND:表示追加内容至文件末尾
O_CREAT:表示如果指定文件不存在,则创建这个文件
O_TRUNC:表示截断,如果文件存在,则将其长度截断为0
O_NONBLOCK:表示将 I/O设置为非阻塞模式(nonblocking mode)
以上为常用选项,具体可参考man page用户手册。
mode: 表示设置文件访问权限的初始值,可按位与,S_IRWXU、S_IRUSR、S_IWUSR等选项,实际文件权限还受umask值影响。
总的来说,open()函数所做的事情就是将传进去的字符串的路径在内核里面转换成相应的inode节点和dentry结构体。执行这一任务的标准过程就是分析路径名并把它拆分成一个文件名序列,除了最后一个文件名以外,所有的文件名都必定是目录。
2、内核实现
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
//对于64位系统会添加O_LARGEFILE选项,以便能打开大文件
if (force_o_largefile())
flags |= O_LARGEFILE;
//第一个参数为AT_FDCWD,表示文件名是以当前路径作为起始目录的路径
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_flags op;
//根据入参构建文件打开标志,比如追加,不存在时创建等标志
int lookup = build_open_flags(flags, mode, &op);
//构建filename结构体,将用户给的文件名拷贝至内核,
struct filename *tmp = getname(filename);
int fd = PTR_ERR(tmp);
if (!IS_ERR(tmp)) {
//获取一个未使用的文件描述符
fd = get_unused_fd_flags(flags);
if (fd >= 0) {
//重头戏,打开文件的真正操作在这
struct file *f = do_filp_open(dfd, tmp, &op, lookup);
if (IS_ERR(f)) {
//打开文件出现错误,回收文件描述符
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
//产生notity事件,用于文件监控
fsnotify_open(f);
//将file结构体放入以fd为索引下标的数组中,让file和fd相关联
fd_install(fd, f);
}
}
putname(tmp);
}
return fd;//返回可用的文件描述符
}
可见,open系统调用在代码结构上显得和简洁直观,获取文件描述符fd->构建file结构体并和需要打开的文件关联->关联fd和file结构->返回文件描述符fd。这一流程的重点在于构建file结构体并和需要打开的文件关联,因为这涉及了文件系统相关操作,正因如此,我们才要来细细分析。
struct file *do_filp_open(int dfd, struct filename *pathname,
const struct open_flags *op, int flags)
{
struct nameidata nd;
struct file *filp;
//主角是path_openat,三种情况不同的是flags的值
//内核为了提高效率,会首先在RCU模式(rcu-walk)下进行文件打开操作
//如果在此方式下打开失败,则进入普通模式(ref-walk)
//第三次调用比较少用,目前只有在nfs文件系统才有可能会被使用
filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);
if (unlikely(filp == ERR_PTR(-ECHILD)))
filp = path_openat(dfd, pathname, &nd, op, flags);
if (unlikely(filp == ERR_PTR(-ESTALE)))
filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_REVAL);
return filp;
}
进入path_openat()之前,我们先来看看struct nameidata这个结构体,它用于保存本次查找的结果,因此在逐级查找目录项的过程中不断变化,是一个中间量。不过定义的nd这个变量最终目的就是保存用户所要打开文件的最后一个目录项的信息。
struct nameidata {
struct path path; //当前目录项
struct qstr last; //当前目录项的名称及散列值
struct path root; //根目录
struct inode *inode; //当前目录项对应的inode,取值来自于path.dentry.d_inode
unsigned int flags;
unsigned seq;
int last_type; //当前目录项的类型
unsigned depth; //符号链接当前的嵌套深度,最大为MAX_NESTED_LINKS(8)
char *saved_names[MAX_NESTED_LINKS + 1]; //符号链接每个嵌套层级的名称
RH_KABI_EXTEND(unsigned m_seq)
};
其中目录项的类型有以下几种,
LAST_NORM:最后一个分量是普通文件名
LAST_ROOT:最后一个分量是“/”
LAST_DOT:最后一个分量是“.”
LAST_DOTDOT:最后一个分量是“..”
LAST_BIND:最后一个分量是符号链接
了解了struct nameidata,那我们回过头来看path_openat函数。
static struct file *path_openat(int dfd, struct filename *pathname,
struct nameidata *nd, const struct open_flags *op, int flags)
{
struct file *base = NULL;
struct file *file;
struct path path;
int opened = 0;
int error;
//从缓存中获取一个file结构体
file = get_empty_filp();
if (IS_ERR(file))
return file;
file->f_flags = op->open_flag;//获取open时的选项
//初始化起始路径,即nd->path,LOOKUP_PARENT表示先查找最终目标文件的父目录
error = path_init(dfd, pathname->name, flags | LOOKUP_PARENT, nd, &base);
if (unlikely(error))
goto out;
//total_link_count用于记录符号链接的深度,每追踪一次符号链接,该值就加一
//目前系统最大允许追踪40层符号链接
current->total_link_count = 0;
//开始遍历目录
error = link_path_walk(pathname->name, nd);
......
return file;
}
在逐级查找目录项之前,首先得确定起始目录,根据用户传入的参数,文件路径可能是绝对路径,也可能是相对路径,因此需要先通过path_init函数处理,说白了也就是设置nd->path的值。
static int path_init(int dfd, const char *name, unsigned int flags,
struct nameidata *nd, struct file **fp)
{
int retval = 0;
nd->last_type = LAST_ROOT; /* if there are only slashes... */
nd->flags = flags | LOOKUP_JUMPED;
nd->depth = 0;//跟踪符号链接的递归深度
//如果flags设置了LOOKUP_ROOT标志,则表示该函数被open_by_handle_at或sysctl函数调用
//该函数将指定一个路径作为根,此处暂不分析
if (flags & LOOKUP_ROOT) {
...
}
nd->root.mnt = NULL;
nd->m_seq = read_seqbegin(&mount_lock);
if (*name=='/') {
//文件名以/开头,说明是绝对路径,不关注dfd的值
if (flags & LOOKUP_RCU) {
rcu_read_lock();
set_root_rcu(nd);//设置nd->root为根文件系统
} else {
set_root(nd);
path_get(&nd->root);
}
nd->path = nd->root;//设置起始遍历路径nd->path为根文件系统
} else if (dfd == AT_FDCWD) {
//dfd为AT_FDCWD,那么这个相对路径是以当前路径pwd作为起始的
if (flags & LOOKUP_RCU) {//rcu-walk遍历
struct fs_struct *fs = current->fs;
unsigned seq;
rcu_read_lock();
do {
seq = read_seqcount_begin(&fs->seq);
nd->path = fs->pwd; //设置起始遍历路径为进程运行的当前路径
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);//获取当前路径
}
} else {
//dfd不是AT_FDCWD,那么这个相对路径是用户设置的
//需要通过dfd获取具体相对路径信息
struct fd f = fdget_raw(dfd);
struct dentry *dentry;
if (!f.file)
return -EBADF;
dentry = f.file->f_path.dentry;
if (*name) {
if (!d_can_lookup(dentry)) {
fdput(f);
return -ENOTDIR;
}
}
nd->path = f.file->f_path;//获取到路径
if (flags & LOOKUP_RCU) {
if (f.flags & FDPUT_FPUT)
*fp = f.file;
nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
rcu_read_lock();
} else {
path_get(&nd->path);
fdput(f);
}
}
//当前目录项对应的inode
nd->inode = nd->path.dentry->d_inode;
return 0;
}
设置好遍历的起始目录后,就可以开始真正的遍历目录了,我们下篇文章再继续分析。