注:本文分析基于3.10.0-693.el7内核版本,即CentOS 7.4
上回说到根据用户给的路径,通过path_init函数设定起始目录nd->path,那接下来就要遍历目录了,我们从link_path_walk函数开始分析。
static int link_path_walk(const char *name, struct nameidata *nd)
{
struct path next;
int err;
while (*name=='/')
name++;//过滤前导"/",即使ls /home 正常执行
if (!*name)//只有根目录,没有子目录,直接返回
return 0;
/* At this point we know we have a real path component. */
//开始一级一级遍历子目录
for(;;) {
struct qstr this;
long len;
int type;
err = may_lookup(nd);//权限校验
if (err)
break;
//计算路径名的哈希值,并返回路径长度
len = hash_name(name, &this.hash);
this.name = name;
this.len = len;
type = LAST_NORM;
//处理路径为.和..的情况,设置相应标记
if (name[0] == '.') switch (len) {
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)) {
err = parent->d_op->d_hash(parent, &this);
if (err < 0)
break;
}
}
//更新子路径名
nd->last = this;
nd->last_type = type;
if (!name[len])
return 0;
/*
* If it wasn't NUL, we know it was '/'. Skip that
* slash, and continue until no more slashes.
*/
do {
len++;
} while (unlikely(name[len] == '/'));//过滤掉中间连续的/
if (!name[len])
return 0;
name += len;
//开始处理子路径
err = walk_component(nd, &next, LOOKUP_FOLLOW);
if (err < 0)
return err;
if (err) {
err = nested_symlink(&next, nd);
if (err)
return err;
}
if (!d_can_lookup(nd->path.dentry)) {
err = -ENOTDIR;
break;
}
}
terminate_walk(nd);
return err;
}
处理好“.”和“..”的情况,同时过滤掉多余的/之后,此时路径名肯定就是一个目录或者链接了。这是就要进入walk_component函数处理这些子路径。
static inline int walk_component(struct nameidata *nd, struct path *path,
int follow)
{
struct inode *inode;
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))//处理.和..的情况
return handle_dots(nd, nd->last_type);
...
}
对于这个子路径,有三种情况,分别是“.”和“..” ,普通目录以及符号链接。我们先看看“.”和“..”的处理。
static inline int handle_dots(struct nameidata *nd, int type)
{
//..——上级目录
if (type == LAST_DOTDOT) {
if (nd->flags & LOOKUP_RCU) {
//处理..,往上一级目录查询
if (follow_dotdot_rcu(nd))
return -ECHILD;
} else
return follow_dotdot(nd);
}
//如果是.,那就是当前目录,不需要处理
return 0;
}
平时我们认为“..”很简单,就是上一级目录而已,但是在内核中并没有表现出来的这么简单。因为往上一级就有可能走到另一个文件系统中,而且由于涉及到挂载的问题,处理起来还是略显复杂的。
static int follow_dotdot_rcu(struct nameidata *nd)
{
set_root_rcu(nd);//设置nd->root为根文件系统
while (1) {
//如果当前目录已经是预设的根目录,那到顶了,直接返回
if (nd->path.dentry == nd->root.dentry && nd->path.mnt == nd->root.mnt) {
break;
}
//如果当前目录不是预设的根目录,且不是当前文件系统的根目录,那就向上走一级
if (nd->path.dentry != nd->path.mnt->mnt_root) {
struct dentry *old = nd->path.dentry;
struct dentry *parent = old->d_parent;//获取父目录
unsigned seq;
seq = read_seqcount_begin(&parent->d_seq);
if (read_seqcount_retry(&old->d_seq, nd->seq))
goto failed;
nd->path.dentry = parent;//nd跨越到上一级目录
nd->seq = seq;
if (unlikely(!path_connected(&nd->path)))
goto failed;
break;
}
//判断父mount结构是否在另一个文件系统中,返回0表示在同一个文件系统中
//在不同文件系统时,需要一直往上走,因为可能是多个文件系统挂载同一个目录
if (!follow_up_rcu(&nd->path))
break;
nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
}
//如果此时找到的父目录也是一个挂载点,需要往上继续找(内核空间)
//虽然从路径名上我们往上一层就可以了,但是在内核里,
//当前这个路径名同样可能是经过多次挂载呈现出来的,因此需要找到最新的那个挂载点
while (d_mountpoint(nd->path.dentry)) {
struct mount *mounted;
//在散列表里查找对应的挂载点
mounted = __lookup_mnt(nd->path.mnt, nd->path.dentry);
if (!mounted)
break;//找到的目录不是挂载点就可以退出了
//找到的目录仍然是挂载点,需要继续找
//因为有可能多个文件系统挂载到同一个目录,因此需要在链表中找到最新的那个目录
nd->path.mnt = &mounted->mnt;
nd->path.dentry = mounted->mnt.mnt_root;
nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
if (read_seqretry(&mount_lock, nd->m_seq))
goto failed;
}
nd->inode = nd->path.dentry->d_inode;//更新inode
return 0;
failed:
nd->flags &= ~LOOKUP_RCU;
if (!(nd->flags & LOOKUP_ROOT))
nd->root.mnt = NULL;
rcu_read_unlock();
return -ECHILD;//返回使用ref-walk方式查找
}
可见,如果当前目录不是根目录,也不是当前文件系统的根目录,也就是谁就是简简单单的一个普通目录,那处理“..”也就是获取父目录的索引而已。
但是如果是该目录挂载了多个文件系统,那么跨越到父目录的时候需要先找到最初挂载的目录结构,否则获取到的文件系统状态仍然是该目录层级。
static int follow_up_rcu(struct path *path)
{
struct mount *mnt = real_mount(path->mnt);
struct mount *parent;
struct dentry *mountpoint;
//获取上一级mount结构
parent = mnt->mnt_parent;
//当前文件系统的挂载点就是自己,即跨越到根文件系统(rootfs)的时候
//在根文件系统里,其上一级mount结构指向的就是自己,因此他们的vfsmount结构相同
if (&parent->mnt == path->mnt)
return 0;
//走到这说明上一级mount结构是在另一个文件系统中
mountpoint = mnt->mnt_mountpoint;
path->dentry = mountpoint;//更新挂载点,挂载点的本质也是目录
path->mnt = &parent->mnt;//更新vfsmount结构,也就跨越到上一级目录,完成..的操作
return 1;
}
如果当前文件系统的挂载点就是自己,即跨越到根文件系统(rootfs)的时候。此时follow_up_rcu函数将返回0,之后退出while(1)循环。
我们举个例子说明这种情况,考虑以下场景,当前目录pwd=/home/a/,文件路径path=…/log.txt(即绝对路径为/home/log.txt),其中目录home和a都是普通目录,没有挂载任何文件系统。因此我们可以知道此时传入follow_up_rcu函数的入参nd->path->mnt指向的是根文件系统(rootfs),因此其上一级mount结构指向的还是自己,follow_up_rcu函数返回0,退出while(1)循环。
而需要while(1)循环则是因为某个目录可能被重复挂载多个文件系统。考虑另一种场景,当前目录pwd=/home/a/,文件路径path=…/log.txt(即绝对路径为/home/log.txt)。但是在此之前先将某个分区,如/dev/sda1,文件系统为fs1,挂载至/home目录,原先/home目录下的文件将被隐藏。然后再将另一个分区,如/dev/sda2,文件系统为fs2,挂载至/home目录,此时在/home目录下再创建目录a和文件log.txt,形成开始时的场景。这个时候因为/home目录被重复挂载,因此在a目录访问上级目录下的log.txt文件,我们需要一个循环体来顺着mount结构的链表从fs2->fs1找到最初的文件系统。
如果退出循环体,至此已经获取到了当前目录项的上一级目录项(即“…”所代表的父目录项)。
接下来又是一个while循环,这是考虑到这个父目录项有可能也是一个挂载点,也可能被重复挂载,所以要获取到最新的那个挂载系统。通过__lookup_mnt()检查父目录下挂载的文件系统是否为最新的文件系统,如果是则检查结束;否则,将继续检查;
走完上述流程,我们也就处理完“.”和“…”的情况,接下来我们来看下如果是普通目录的情况,这就是下次要说的了。