打开文件源码分析及调试
一、open()在Linux内核的实现
内核版本5.4
(一)准备工作
1.数据结构
dentry结构:在文件路径和inode之间通过目录项(dentry)缓存进行关联,dentry缓存加快了vfs对文件的查找。
vfsmount结构:每个挂载在内核目录树中的文件系统都将对应一个vfsmount结构
nameidata结构:文件路径是由各级别的目录项组成的,因此路径的查找过程是对目录项的逐级查找。nameidata结构是路径查找过程中的核心数据结构,在每一级目录项查找过程中,它向查找函数输入参数,并且保存本次查找的结果,因此它是不断变化的。
2.基本原理
rcu机制:
写时拷贝(rcu,Read-Copy-Update)是Linux内核的一种锁机制,它是一种改良的rwlock(但并不能代替),适合读者多写者少的情景,可以保证读写者操作同时进行。
对于读者而言,**rcu机制可以保证多个读者在不申请锁的情况下直接对临界区资源进行访问。对于写者而言,它之所以可以与读者同时访问共享资源,是因为在读者读取原始数据的同时它修改的是原始数据的备份。**当所有读者都退出访问该共享资源时,写者将用修改后的新数据替换原始数据。同时,rcu中的回收机制将对原始数据进行回收。
与rwlock相比,在读多写少的情况下,rcu的效率会高很多。因为rcu所提供的拷贝技术使读写者可以同时访问共享资源,因此免去了读写者申请锁时所花费的开销。
由于rcu机制的自身特点,它所使用的上下文必须是不可睡眠的。因为,写者在替换原始数据之前会等待所有读者退出临界区,而此时如果读者处于阻塞状态,那么系统将进入死锁状态。
补充:读写锁rwlock:写独占,读共享;写锁优先级高。
rcu-walk和ref-walk:
内核中的路径查找提供两种模式:ref-walk和rcu-walk。前者是内核中传统的路径查找方式,而ref-walk是基于rcu锁机制的一种路径查找模式。由于路径查找正好是一个读多写少的情景,基于rcu机制快速高效的特点,该模式可以高效的进行路径查找。不过,rcu-walk并不是万能的,如果路径查找过程中需要睡眠,那么必须将查找模式由rcu-walk切换到ref-walk。
补充:ref-walk是操作简单,但是在路径行走的过程中,对于每一个目录项的操作可能需要睡眠、得到锁等等这些操作。
(二)基本实现
函数 do_sys_open() 源代码如下:
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_flags op;
int fd = build_open_flags(flags, mode, &op); //通过build_open_flags()将用户态的flags和mode转换成对应的内核态标志
struct filename *tmp;
if (fd)
return fd;
tmp = getname(filename); //由于filename是用户态的内存缓冲区(使用了__user修饰),因此通过getname()将文件名从用户态拷贝至内核态;
if (IS_ERR(tmp))
return PTR_ERR(tmp);
fd = get_unused_fd_flags(flags); //get_unused_fd_flags()为即将打开的文件分配文件描述符;也就是在当前进程的files数组中寻找一个未使用的位置;
if (fd >= 0) {
struct file *f = do_filp_open(dfd, tmp, &op); //通过do_filp_open()为文件创建file结构体;
if (IS_ERR(f)) { //如果创建file成功,则通过fd_install()将fd和file进行关联;如果创建file失败,通过put_unused_fd()将已分配的fd返回至系统,并且根据file生成错误的fd;
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
fsnotify_open(f);
fd_install(fd, f);
}
}
putname(tmp); //通过putname()释放在内核分配的路径缓冲区;
return fd;
}
(三)路径查找
文件的打开操作在内核中的实现思路很简单:即通过用户态传递的路径逐项查找文件;如果该文件存在,那么内核将为该文件创建file结构;同时将该file结构与files数组关联,最终返回数组的索引作为用户态的文件描述符。
函数 do_filp_open() 源代码如下:
/*do_filp_open()解析文件路径并新建file结构*/
struct file *do_filp_open(int dfd, struct filename *pathname,
const struct open_flags *op)
{
struct nameidata nd; //nameidata类型的nd在整个路径查找过程中充当中间变量,它既可以为当前查找输入数据,又可以保存本次查找的结果。
int flags = op->lookup_flags;
struct file *filp;
set_nameidata(&nd, dfd, pathname);
//path_openat有可能会被调用三次。通常内核为了提高效率,会首先在RCU模式(rcu-walk)下进行文件打开操作;如果在此方式下打开失败,则进入普通模式(ref-walk)。第三次调用比较少用,目前只有在nfs文件系统才有可能会被使用。
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;
}
函数 path_openat() 源代码如下:
/*path_openat()描述了整个路径查找过程的基本步骤*/
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()); //通过alloc_empty_file分配一个新的file结构,分配前会对当前进程的权限和文件最大数进行判断;
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); //path_init()对接下来的路径遍历做一些准备工作,主要用于判断路径遍历的起始位置,即通过根目录/,或当前路径(pwd),或指定路径
while (!(error = link_path_walk(s, nd)) &&
(error = do_last(nd, file, op)) > 0) //link_path_walk()对所打开文件路径进行逐一解析,每个目录项的解析结果都存在nd参数中;根据最后一个目录项的结果,do_last()将填充filp所指向的file结构;
{
nd->flags &= ~(LOOKUP_OPEN|LOOKUP_CREATE|LOOKUP_EXCL);
s = trailing_symlink(nd); //trailing_symlink 来检查当前打开的文件
}
terminate_walk(nd); //查找完成操作,包括解RCU锁。
}
if (likely(!error)) {
if (likely(file->f_mode & FMODE_OPENED))
return file;
WARN_ON(1);
error = -EINVAL;
}
fput(file); //返回file结构;
if (error == -EOPENSTALE) {
if (flags & LOOKUP_RCU)
error = -ECHILD;
else
error = -ESTALE;
}
return ERR_PTR(error);
}
函数 path_init() 源代码如下:
/*path_init()用于设置路径搜寻的起始位置,主要体现在设置nd变量*/
static const char *path_init(struct nameidata *nd, unsigned flags)
{
const char *s = nd->name->name;
if (!*s)
flags &= ~LOOKUP_RCU;
if (flags & LOOKUP_RCU)
rcu_read_lock();
nd->last_type = LAST_ROOT; /* if there are only slashes... */
nd->flags = flags | LOOKUP_JUMPED | LOOKUP_PARENT;
nd->depth = 0;
if (flags & LOOKUP_ROOT) { //如果flags设置了LOOKUP_ROOT标志,则表示该函数被open_by_handle_at函数调用,该函数将指定一个路径作为根;
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;
nd->m_seq = read_seqbegin(&mount_lock);
} else {
path_get(&nd->path);
}
return s;
}
nd->root.mnt = NULL;
nd->path.mnt = NULL;
nd->path.dentry = NULL;
nd->m_seq = read_seqbegin(&mount_lock);
if (*s == '/') { //如果路径名name以/为起始,则表示当前路径是一个绝对路径,通过set_root设置nd;否则,表示路径name是一个相对路径;
set_root(nd);
if (likely(!nd_jump_root(nd)))
return s;
return ERR_PTR(-ECHILD);
} else if (nd->dfd == AT_FDCWD) { //如果dfd为AT_FDCWD,那么表示这个相对路径是以当前路径pwd作为起始的,因此通过pwd设置nd;
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;
}
return s;
} else { //如果dfd不是AT_FDCWD,表示这个相对路径是用户设置的,需要通过dfd获取具体相对路径信息,进而设置nd;
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);
return s;
}
}
函数 link_path_walk() 源代码如下:
/*link_path_walk()主要用于对各目录项逐级遍历*/
static int link_path_walk(const char *name, struct nameidata *nd)
{
int err;
if (IS_ERR(name))
return PTR_ERR(name);
//在进入这个循环之前,如果路径name是一个绝对路径,那么该函数还对路径进行了一些处理,即过滤掉绝对路径/前多余的符号/。
while (*name=='/')
name++;
if (!*name)
return 0;
/* At this point we know we have a real path component. */
for(;;) {
u64 hash_len;
int type;
err = may_lookup(nd);
if (err)
return err;
hash_len = hash_name(nd->path.dentry, name);
type = LAST_NORM;
if (name[0] == '.') switch (hashlen_len(hash_len)) { //如果当前目录项为“.”,则type为LAST_DOT;如果目录项为“..”,则type为LAST_DOTDOT;否则,type默认为LAST_NORM;
case 2:
if (name[1] == '.') {
type = LAST_DOTDOT;
nd->flags |= LOOKUP_JUMPED;
}
break;
case 1:
type = LAST_DOT;
}
if (likely(type == LAST_NORM)) {
struct dentry *parent = nd->path.dentry;
nd->flags &= ~LOOKUP_JUMPED;
if (unlikely(parent->d_flags & DCACHE_OP_HASH)) {
struct qstr this = { { .hash_len = hash_len }, .name = name }; //this为qstr类型变量,表示当前搜索路径所处目录项的哈希值,用type指明当前目录项类型;
err = parent->d_op->d_hash(parent, &this);
if (err < 0)
return err;
hash_len = this.hash_len;
name = this.name;
}
}
nd->last.hash_len = hash_len;
nd->last.name = name;
nd->last_type = type;
name += hashlen_len(hash_len);
if (!*name)
goto OK;
//如果当前目录项紧邻的分隔符/有多个(比如/home///edsionte),则将其过滤,即使name指向最后一个/;
do {
name++;
} while (unlikely(*name == '/'));
if (unlikely(!*name)) {
OK:
/* pathname body, done */
if (!nd->depth)
return 0;
name = nd->stack[nd->depth - 1].name;
/* trailing symlink, done */
if (!name)
return 0;
//通过walk_component()处理当前目录项,更新nd和next;如果当前目录项为符号链接文件,则只更新next;
/* last component of nested symlink */
err = walk_component(nd, WALK_FOLLOW);
} else {
/* not the last component */
err = walk_component(nd, WALK_FOLLOW | WALK_MORE);
}
if (err < 0)
return err;
if (err) {
const char *s = get_link(nd);
if (IS_ERR(s))
return PTR_ERR(s);
err = 0;
if (unlikely(!s)) {
/* jumped */
put_link(nd);
} else {
nd->stack[nd->depth - 1].name = name;
name = s;
continue;
}
}
if (unlikely(!d_can_lookup(nd->path.dentry))) {
if (nd->flags & LOOKUP_RCU) {
if (unlazy_walk(nd))
return -ECHILD;
}
return -ENOTDIR;
}
}
}
//通过上述循环,将用户所指定的路径name从头至尾进行了搜索,至此nd保存了最后一个目录项的信息,但是内核并没有确定最后一个目录项是否真的存在,这些工作将在do_last()中进行。
函数 walk_component() 源代码如下:
//walk_component()处理当前目录项,更新nd和next;如果当前目录项为符号链接文件,则只更新next;
static int walk_component(struct nameidata *nd, int flags)
{
struct path path;
struct inode *inode; //变量inode保存当前目录项对应的索引节点
unsigned seq;
int err;
/*
* "." 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)) { //如果type为LAST_DOT和LAST_DOTDOT,将进入handle_dots()对当前目录项进行“walk”;
err = handle_dots(nd, nd->last_type);
if (!(flags & WALK_MORE) && nd->depth)
put_link(nd);
return err;
}
err = lookup_fast(nd, &path, &inode, &seq); //lookup_fast查询dentry中的缓存,看一下是否命中,如果没有命中,则会用lookup_slow下降到文件系统层进行路径查找。
if (unlikely(err <= 0)) {
if (err < 0)
return err;
path.dentry = lookup_slow(&nd->last, nd->path.dentry,
nd->flags);
if (IS_ERR(path.dentry))
return PTR_ERR(path.dentry);
path.mnt = nd->path.mnt;
err = follow_managed(&path, nd); //follow_managed函数会检查当前 dentry 是否是个挂载点,如果是就跟下去
if (unlikely(err < 0))
return err;
if (unlikely(d_is_negative(path.dentry))) {
path_to_nameidata(&path, nd); //至此,如果当前目录项查找成功,则通过path_to_nameidata()更新nd;
return -ENOENT;
}
seq = 0; /* we are already out of RCU mode */
inode = d_backing_inode(path.dentry);
}
return step_into(nd, &path, flags, inode, seq);
}
(四)“.”和“…”的处理
当目录项为”.”(LAST_DOT)或者“…”(LAST_DOTDOT),那么walk_component()将通过handle_dots()对其进行处理。
如果当前目录项为”.”,那么walk_component()此时的作用就是“越过”这些当前目录,而nd信息不做改变,因为所有“.”之前的普通目录项已经更新了nd。
如果当前目录项为“…”,即当前要walk的目录项为上一次已经walk的目录项的父目录,也就是需要向上获取当前目录的父目录。
函数 handle_dots() 源代码如下:
static inline int handle_dots(struct nameidata *nd, int type)
{
if (type == LAST_DOTDOT) {
if (!nd->root.mnt)
set_root(nd);
if (nd->flags & LOOKUP_RCU) { //如果当前搜索路径的模式位rcu,则进入follow_dotdot_rcu()的流程;否则进入follow_dotdot()的流程。
return follow_dotdot_rcu(nd);
} else
return follow_dotdot(nd);
}
return 0;
}
函数 follow_dotdot_rcu() 源代码如下:
//follow_dotdot_rcu()函数是在rcu模式下获取父目录项信息,如果搜索成功,则返回0;否则,返回ECHILD,也就是说需要切换到ref-walk方式下进行搜索路径。
static int follow_dotdot_rcu(struct nameidata *nd)
{
struct inode *inode = nd->inode;
//进入循环体,向上获取当前目录项的父目录项。通常情况下,这个循环体只会被执行一次即退出,只有当父目录项为一个挂载点时才有可能不断进行循环。
while (1) {
if (path_equal(&nd->path, &nd->root)) //如果当前目录项恰好为根目录目录项,则直接跳出循环;
break;
if (nd->path.dentry != nd->path.mnt->mnt_root) { //如果当前目录项既不是根目录,也不是一个挂载点,则属于最普通的情况,即直接获取当前目录项的父目录项。
struct dentry *old = nd->path.dentry;
struct dentry *parent = old->d_parent;
unsigned seq;
inode = parent->d_inode;
seq = read_seqcount_begin(&parent->d_seq);
if (unlikely(read_seqcount_retry(&old->d_seq, nd->seq)))
return -ECHILD;
nd->path.dentry = parent;
nd->seq = seq;
if (unlikely(!path_connected(&nd->path)))
return -ENOENT;
break;
} else {
struct mount *mnt = real_mount(nd->path.mnt);
struct mount *mparent = mnt->mnt_parent;
struct dentry *mountpoint = mnt->mnt_mountpoint;
struct inode *inode2 = mountpoint->d_inode;
unsigned seq = read_seqcount_begin(&mountpoint->d_seq);
if (unlikely(read_seqretry(&mount_lock, nd->m_seq)))
return -ECHILD;
if (&mparent->mnt == nd->path.mnt)
break;
/* we know that mountpoint was pinned */
nd->path.dentry = mountpoint;
nd->path.mnt = &mparent->mnt;
inode = inode2;
nd->seq = seq;
}
}
//如果这个父目录项是一个挂载点,那么还需做一些特殊检查。因为在特殊情况下,当前这个父目录项又被挂载了其他的文件系统,那么返回上级目录这个操作获取的应该是最新文件系统的内容而不是之前那个文件系统的内容。
while (unlikely(d_mountpoint(nd->path.dentry))) {
struct mount *mounted;
mounted = __lookup_mnt(nd->path.mnt, nd->path.dentry); //通过__lookup_mnt()检查父目录下挂载的文件系统是否为最新的文件系统,如果是则检查结束;否则,将继续检查;
if (unlikely(read_seqretry(&mount_lock, nd->m_seq)))
return -ECHILD;
if (!mounted)
break;
nd->path.mnt = &mounted->mnt;
nd->path.dentry = mounted->mnt.mnt_root;
inode = nd->path.dentry->d_inode;
nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
}
nd->inode = inode; //更新nd中的inode
return 0;
}
(五)普通目录项的处理
5.4内核未采用do_lookup()对普通目录项进行查找,而是采用 lookup_fast() 和 lookup_slow() 这两个函数对其进行查找,具体流程为:每次内核都会都会尝试用lookup_fast查询dentry中的缓存,看一下是否命中,如果没有命中,则会用lookup_slow下降到文件系统层进行路径查找。
static int lookup_fast(struct nameidata *nd,
struct path *path, struct inode **inode,
unsigned *seqp)
{
......
if (unlazy_walk(nd))
return -ECHILD;
......
}
如果当前目录项为符号链接文件,则内核的处理方式又是另一种方式,内核会调用unlazy_walk函数来终止RCU查找模式。
(六)符号链接目录项的处理
如果当前的目录项具备符号链接特性,那么内核将试图通过unlazy_walk()将当前的rcu-walk切换到ref-walk。因为处理符号链接文件会使用实体文件系统的钩子函数follow_link(),这个函数可能会引起阻塞,因此必须切换到允许阻塞的ref-walk模式。
如果切换失败,则返回-ECHILD,即返回值do_filp_open()处,将重新以ref-walk方式执行打开操作;否则,walk_component()将返回1,那么调用它的link_path_walk()处理符号链接文件(链接文件是一个路径)。
符号链接文件的处理方式比较特殊,但是本质的实现还是基于link_path_walk(),该函数是整个路径查找框架中的基础实现。对于一个路径来说,经过link_path_walk()的处理,nd将保存最后一个目录项的信息,但是最后一个目录项代表的文件(或目录)是否存在并不知晓,需要通过调用link_path_walk()的path_openat()做最后的处理。
(七)打开操作分析
**do_last():**在path_openat()中,经过link_path_walk()对open路径的查找,将进入do_last()对路径中最后一个目录项做处理。最后一个目录项可能是各种类型,比如“.”或者“…”,也可能是符号链接文件或者是“/”
(八)思维导图
do_filp_open():解析文件路径并新建file结构;
path_openat():描述了整个路径查找过程的基本步骤;
path_init():用于设置路径搜寻的起始位置,主要体现在设置nd变量;
link_path_walk():主要用于对各目录项逐级遍历;
do_last():对路径中最后一个目录项做处理;
walk_component():处理当前目录项,更新nd和next;如果当前目录项为符号链接文件,则只更新next;
handle_dots():处理”.”(LAST_DOT)或者“…”(LAST_DOTDOT)的目录项;
lookup_fast():通过dentry中的缓存对普通目录项进行查找;
lookup_slow():通过文件系统对普通目录项进行查找;
follow_dotdot_rcu():在rcu模式下获取父目录项信息,如果搜索成功,则返回0;否则,返回ECHILD,也就是说需要切换到ref-walk方式下进行搜索路径;
follow_dotdot():在读写锁模式下获取父目录项信息;
unlazy_walk():将当前的rcu-walk切换到ref-walk;
follow_link():实体文件系统的钩子函数,这个函数可能会引起阻塞;
二、调试打开文件的过程
在源代码中加入printk语句,打印出相关的数据结构字段值:
打开文件的模式:Linux系统中采用0ABC的形式来表示文件的操作权限:
0表示八进制;
A表示的是文件主的权限;
B表示的是组用户的权限;
C表示的是其他用户的权限;
其中A、B、C都是0~7的数字
0~7各个数字代表的含义如下(r:Read读,w:Write写,x:eXecute执行):
--- 0 不可读写,不可执行
--x 1 可执行,不可读写
-w- 2 可写,不可读,不可执行
-wx 3 可写可执行,不可读
r-- 4 可读,不可写,不可执行
r-x 5 可读,可执行,不可写
rw- 6 可读写,不可执行
rwx 7 可读写,可执行
file.c代码如下:
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/sched.h> //task结构体
#include <linux/syscalls.h>
#include <linux/fs_struct.h>
#include <linux/fdtable.h>
#include <linux/fs.h>
MODULE_LICENSE("GPL"); //许可证
static int __init init_file(void)
{
struct file *fp;
fp=filp_open("/home/zhang/code/file/test.txt",O_RDWR,0644); //0644代表的是文件主有可读写的权限,组用户和其他用户有可读的权限。
printk("users:%d umask:%d count:%d f_flags:%d f_mode:%d f_version:%d i_mode:%d\n",current->fs->users,current->fs->umask,current->files->count,fp->f_flags,fp->f_mode,fp->f_version,fp->f_inode->i_mode);
return 0;
}
static void __exit exit_file(void) //出口函数
{
printk("Exiting...\n");
}
// 指明入口点与出口点,入口/出口点是由module.h支持的
module_init(init_file);
module_exit(exit_file);
运行结果: