注:本文分析基于3.10.0-693.el7内核版本,即CentOS 7.4
上回说到普通目录的情况,这次接着说说符号链接的事。
static inline int walk_component(struct nameidata *nd, struct path *path,
int follow)
{
...
if (unlikely(nd->last_type != LAST_NORM))
return handle_dots(nd, nd->last_type); //.和..的处理
err = lookup_fast(nd, path, &inode); //普通目录的处理,先在内存中找
if (unlikely(err)) {
err = lookup_slow(nd, path); //调用对应文件系统的查找函数找
}
err = -ENOENT;
if (!inode || d_is_negative(path->dentry))
goto out_path_put;
//如果这是个符号链接,就要另外处理了
if (should_follow_link(path->dentry, follow)) {
//如果处于RCU模式,要先切换到ref-walk模式,因为符号链接追踪可能会涉及底层驱动
//会有阻塞发生,因此不能在RCU模式下
if (nd->flags & LOOKUP_RCU) {
if (unlikely(unlazy_walk(nd, path->dentry))) {
err = -ECHILD;
goto out_err;
}
}
BUG_ON(inode != path->dentry->d_inode);
return 1;
}
...
}
如果普通目录的查找结果表明该dentry是个符号链接,那就要跟踪这个链接,找到实际目录。这种情况下,walk_component函数返回值为1。
static int link_path_walk(const char *name, struct nameidata *nd)
{
...
err = walk_component(nd, &next, LOOKUP_FOLLOW);
if (err < 0)
return err;
if (err) {
//处理符号链接的情况
err = nested_symlink(&next, nd);
if (err)
return err;
}
...
}
返回到link_path_walk函数,接着处理符号链接,调用nested_symlink函数。
static inline int nested_symlink(struct path *path, struct nameidata *nd)
{
int res;
//通过进程信息,检查符号链接嵌套深度,最大为MAX_NESTED_LINKS,8层
if (unlikely(current->link_count >= MAX_NESTED_LINKS)) {
path_put_conditional(path, nd);
path_put(&nd->path);
return -ELOOP;
}
//通过遍历路径的记录来检查符号链接嵌套深度
BUG_ON(nd->depth >= MAX_NESTED_LINKS);
nd->depth++;
current->link_count++;
//这里do-while循环是因为有可能符号链接指向的还是一个符号链接
//因此
do {
struct path link = *path;
void *cookie;
//跟踪符号链接
res = follow_link(&link, nd, &cookie);
if (res)
break;
//到这里也就找到了符号链接最终目标的最后一级目录,还需再判断是否是符号链接
res = walk_component(nd, path, LOOKUP_FOLLOW);
put_link(nd, &link, cookie);
} while (res > 0);//如果不是符号链接,就可以返回了,算是完成了符号链接的跟踪
current->link_count--;
nd->depth--;
return res;
}
由于符号链接可能出现环路,为避免死循环,必须对符号跟踪深度做个限制,然后再跟随这个符号链接去寻找目标。
static __always_inline int
follow_link(struct path *link, struct nameidata *nd, void **p)
{
struct dentry *dentry = link->dentry;
int error;
char *s;
BUG_ON(nd->flags & LOOKUP_RCU);
if (link->mnt == nd->path.mnt)
mntget(link->mnt);
error = -ELOOP;
//即使没有循环嵌套,也有可能每一级目录都是一个符号链接
//限定一个路径中最多的符号链接数为40
if (unlikely(current->total_link_count >= 40))
goto out_put_nd_path;
cond_resched();
current->total_link_count++;
//更新符号链接的访问时间
touch_atime(link);
//置空nd->saved_names数组对应链接深度
//该变量用于保存对应符号链接深度指向的目标
nd_set_link(nd, NULL);
error = security_inode_follow_link(link->dentry, nd);
if (error)
goto out_put_nd_path;
nd->last_type = LAST_BIND;
//调用对应的文件系统的符号链接跟踪函数,返回的是
*p = dentry->d_inode->i_op->follow_link(dentry, nd);
error = PTR_ERR(*p);
if (IS_ERR(*p))
goto out_put_nd_path;
error = 0;
//获取对应符号链接深度指向的目标
s = nd_get_link(nd);
if (s) {
//看看我们找到的目标是不是绝对路径还是相对路径
//如果是相对路径还得继续找下去
error = __vfs_follow_link(nd, s);
if (unlikely(error))
put_link(nd, link, *p);
}
...
}
follow_link最后会调用对应文件系统的跟随函数去寻找对应的目标。该目标存在nd->saved_names数组中。因为符号链接最大递归深度是8,saved_names数组为每一个递归深度的目标目录准备了存放的位置。
enum { MAX_NESTED_LINKS = 8 };
struct nameidata {
...
char *saved_names[MAX_NESTED_LINKS + 1];
RH_KABI_EXTEND(unsigned m_seq)
};
顺着符号链接找到目标后,就要开启新一轮的跟踪,因为这又是一个新的目录路径。
static __always_inline int __vfs_follow_link(struct nameidata *nd, const char *link)
{
int ret;
if (IS_ERR(link))
goto fail;
//如果当前找到的目录是绝对路径,就设置一下预设根目录和当前路径
if (*link == '/') {
set_root(nd);
path_put(&nd->path);
nd->path = nd->root;
path_get(&nd->root);
nd->flags |= LOOKUP_JUMPED;
}
nd->inode = nd->path.dentry->d_inode;
//接着调用路径查找函数link_path_walk
//别忘了,我们当前也还在link_path_walk函数中
//但是这并不是一次嵌套,调用nested_symlink才算试一次嵌套
ret = link_path_walk(link, nd);
return ret;
fail:
path_put(&nd->path);
return PTR_ERR(link);
}
其实当前我们仍然还在link_path_walk函数中,调用link_path_walk函数是因为符号链接指向的路径和最开始需要处理的目录一样,也需要遍历到最后一个分量。比如,一开始open的文件是/home/a/file,目录遍历到a/分量时,发现是个符号链接,那此时其实就是又要从open开始,执行打开/home/realdir/的操作了,类似的流程再来一遍。
最终,follow_link函数获取到符号链接的目标目录的最后一级目录项,返回后,会继续调用walk_component检查最后一级目录分量是否是符号链接,如果还是符号链接,那就进入下一级嵌套中。如果是个普通目录,那就将nd更新至目标dentry。符号链接的跟踪也算是完成了。
下面我们通过以下场景来具体阐述一下符号链接的处理流程。
场景描述:
open(/home/test/file, ……),其中路径分量test/是一个符号链接,指向/home/open/realdir/,home/、open/和realdir/目录都是普通目录,则此时代码路径如下:
path_openat
-> link_path_walk
-> walk_component(判断是否为符号链接)
-> should_follow_link(此时test/是个符号链接)
return follow
-> return 1
-> nested_symlink(跟踪符号链接)
-> do {
-> follow_link
-> __vfs_follow_link(获取到test/符号链接的目标:/home/open/realdir/)
-> link_path_walk
-> walk_component
-> should_follow_link(此时home/和open/都是普通目录)
-> return don't_follow
-> return 0
-> return 0
-> return 0
-> return 0
-> walk_component
-> should_follow_link(最后一级目标realdir/是普通目录)
-> return don't_follow
-> return 0
}while(如果符号链接的目标仍然是符号链接,继续跟踪)
-> return 0
-> return 0
-> do_something
主要流程:
1、通过link_path_walk对路径/home/test/进行查找,设该层遍历为A;
2、假定home/目录是普通目录,直到walk_component函数遍历到test/目录时,发现test/是个符号链接;
3、返回到A层遍历link_path_walk函数中,继续调用nested_symlink函数处理符号链接;
4、nested_symlink函数中通过follow_link函数获取到test/指向的文件路径为/home/open/realdir/,然后通过__vfs_follow_link函数对路径/home/open/realdir/进行新一轮的查找工作;
5、__vfs_follow_link函数中调用link_path_walk函数对路径/home/open/realdir/进行遍历(设该层遍历为B),最终获取路径的最后一个目录项realdir/,这里假定home/和open/目录都是普通目录;
6、返回到nested_symlink函数,调用walk_component函数判断最后一级目录项realdir/为普通目录,更新nd信息,完成符号链接的跟踪,并返回A层遍历的link_path_walk函数中。
如果目录项realdir/依然是个符号链接,那就只能在do-while循环里再走一遭,直到最终的目录不是符号链接为止。