上一篇博文我们分析了极其复杂的路径名查找流程,如果实在看不懂,还是用老方法,先理清相关的数据结构,然后再在纸上画一画。相信大家都欣赏到了Linux内核的复杂性了吧,不过事儿还没完呢,我们还留了两个尾巴,一个是父路径名查找问题,还有一个就是符号链接的查找问题,不过呢,如果上一篇文章你大致上读懂了话,这两个东西也不难。
1 父路径名查找
在很多情况下,查找操作的真正目的并不是路径名的最后一个分量,而是最后一个分量的前一个分量。例如,当文件被创建时,最后一个分量表示还不存在的文件的文件名,而路径名中的其余路径指定新链接必须插入的目录。因此,查找操作应当取回最后分量的前一个分量的目录项对象。另举一个例子,我们执行rm -f /foo/bar,这里路径名/foo/bar表示的文件bar拆分出来就包含从其目录foo中移去bar。因此,内核真正的兴趣在于访问文件目录foo而不是bar。
当查找操作必须解析的是包含路径名最后一个分量的目录而不是最后一个分量本身时,就使用LOOKUP_PARENT标志。
当LOOKUP_PARENT标志被设置时,link_path_walk()函数也在nameidata数据结构中建立last和last_type字段。last字段存放路径名中的最后一个分量名,我们再来回忆一下nameidate的结构:
struct nameidata {
struct dentry *dentry;
struct vfsmount *mnt;
struct qstr last;
unsigned int flags;
int last_type;
unsigned depth;
char *saved_names[MAX_NESTED_LINKS + 1];
/* Intent data */
union {
struct open_intent open;
} intent;
};
last_type字段标识最后一个分量的类型;可以把它置为如下所示的值之一:
LAST_NORM:最后一个分量是普通文件名
LAST_ROOT:最后一个分量是“/”(也就是整个路径名为“/”)
LAST_DOT:最后一个分量是“.”
LAST_DOTDOT:最后一个分量是“..”
LAST_BIND:最后一个分量是链接到特殊文件系统的符号链接
当整个路径名的查找操作开始时,LAST_ROOT标志是由path_lookup()设置的缺省值。如果路径名正好是“/”,则内核不改变last_type字段的初始值。
last_type字段的其他值在LOOKUP_PARENT标志置位时由link_path_walk()设置;在这种情况下,函数执行上篇博文描述的步骤,直到第7步。不过,从第7步往后,即检查lookup_flags变量中LOOKUP_PARENT标志的值,如果被置位,则跳到lookup_parent:
lookup_parent:
nd->last = this;
nd->last_type = LAST_NORM;
if (this.name[0] != '.')
goto return_base;
if (this.len == 1)
nd->last_type = LAST_DOT;
else if (this.len == 2 && this.name[1] == '.')
nd->last_type = LAST_DOTDOT;
else
goto return_base;
1. 把nd->last置为最后一个分量名。
2. 把nd->last_type初始化为LAST_NORM。
3. 如果最后一个分量名为“.”(一个圆点),则把nd->last_type置为LAST_DOT。
4. 如果最后一个分量名为“..”(两个圆点),则把nd->last_type置为LAST_DOTDOT。
5. 通过返回值0(无错误)终止。
你可以看到,最后一个分量根本就没有被解释。因此,当函数终止时,nameidata数据结构的dentry和mnt字段指向最后一个分量所在目录对应的对象。
2 符号链接的查找
回想一下,符号链接是一个普通文件,其中存放的是另一个文件的路径名。路径名可以包含符号链接,且必须由内核来解析。
例如,如果/foo/bar是指向(包含路径名)../dir的一个符号链接,那么,/foo/bar/file径名必须由内核解析为对/dir/file文件的引用。在这个例子中,内核必须执行两个不同的查找操作。第一个操作解析/foo/bar:当内一核发现bar是一个符号链接名时,就必须提取它的内容并把它解释为另一个路径名。第二个路径名操作从第一个操作所达到的目录开始,继续到符号链接路径名的最后一个分量被解析。接下来,原来的查找操作从第二个操作所达到的目录项恢复,且有了原目录名中紧随符号链接的分量。
对于更复杂的情景,含有符号链接的路径名可能包含其他的符号链接。你可能认为解析这类符号链接的内核代码是相当难理解的,但并非如此;代码实际上是相当简单的,因为它是递归的。
然而,难以驾驭的递归本质上是危险的。例如,假定一个符号链接指向自己。当然,解析含有这样符号链接的路径名可能导致无休止的递归调用流,这又依次引发内核栈的溢出。当前进程的描述符中的link_count字段用来避免这种问题:每次递归执行前增加这个字段的值,执行之后减少其值。如果该字段的值达到6,整个循环操作就以错误码结束。因此,符号链接嵌套的层数不超过5。
另外,当前进程的描述符中的total_link_count字段记录在原查找操作中有多少符号链接(甚至非嵌套的)被跟踪。如果这个计数器的值到40,则查找操作中止。没有这个计数器,怀有恶意的用户就可能创建一个病态的路径名,让其中包含很多连续的符号链接,使内核在无休止的查找操作中冻结。
这就是代码基本工作的方式:一旦link_path_walk()函数检索到与路径名分量相关的录项对象,就检查相应的索引节点对象是否有自定义的follow_link方法(参见上一篇博文中的第5k步和第11步)。如果是,索引节点就是一个符号链接,在原路径名的查找操作进行之前就必须先对这个符号链接进行解释。
在这种情况下,link_path_walk()函数调用do_follow_link(),前者传递给后者的参数为符号链接目录项对象的地址dentry和nameidata数据结构的地址nd:
static inline int do_follow_link(struct path *path, struct nameidata *nd)
{
int err = -ELOOP;
if (current->link_count >= MAX_NESTED_LINKS)
goto loop;
if (current->total_link_count >= 40)
goto loop;
BUG_ON(nd->depth >= MAX_NESTED_LINKS);
cond_resched();
err = security_inode_follow_link(path->dentry, nd);
if (err)
goto loop;
current->link_count++;
current->total_link_count++;
nd->depth++;
err = __do_follow_link(path, nd);
current->link_count--;
nd->depth--;
return err;
loop:
dput_path(path, nd);
path_release(nd);
return err;
}
do_follow_link()依次执行下列步骤:
1. 检查current->link_count小于MAX_NESTED_LINKS,一般来说是5;否则,返回错误码-ELOOP。
2. 检查current->total_link_count小于40;否则,返回错误码-ELOOP。
3. 如果当前进程需要,则调用cond_resched()进行进程交换(设置当前进程描述符thread_info中的TIF_NEED_RESCHED标志)。
4. 递增current->link_count、current->total_link_count和nd->depth的值。
5. 更新与要解析的符号链接关联的索引节点的访问时间。
6. 调用与具体文件系统相关的函数来实现follow_link方法,给它传递的参数为dentry和nd。它读取存放在符号链接索引节点中的路径名,并把这个路径名保存在nd->saved_names数组的合适项中。
7. 通过__do_follow_link调用__vfs_follow_link()函数,给它传递的参数为地址nd和nd->saved_names数组中(参见下面)路径名的地址:
static __always_inline int __do_follow_link(struct path *path, struct nameidata *nd)
{
int error;
void *cookie;
struct dentry *dentry = path->dentry;
touch_atime(path->mnt, dentry);
nd_set_link(nd, NULL);
if (path->mnt != nd->mnt) {
path_to_nameidata(path, nd);
dget(dentry);
}
mntget(path->mnt);
cookie = dentry->d_inode->i_op->follow_link(dentry, nd);
error = PTR_ERR(cookie);
if (!IS_ERR(cookie)) {
char *s = nd_get_link(nd);
error = 0;
if (s)
error = __vfs_follow_link(nd, s);
if (dentry->d_inode->i_op->put_link)
dentry->d_inode->i_op->put_link(dentry, nd, cookie);
}
dput(dentry);
mntput(path->mnt);
return error;
}
8. 如果定义了索引节点对象的put_link方法,就执行它,释放由follow_link方法分配的临时数据结构。
9. 减少current->link_count和nd->depth字段的值。
10. 返回由__vfs_follow_link()函数返回的错误码(0表示无错误):
static __always_inline int __vfs_follow_link(struct nameidata *nd, const char *link)
{
int res = 0;
char *name;
if (IS_ERR(link))
goto fail;
if (*link == '/') {
path_release(nd);
if (!walk_init_root(link, nd))
/* weird __emul_prefix() stuff did it */
goto out;
}
res = link_path_walk(link, nd);
out:
if (nd->depth || res || nd->last_type!=LAST_NORM)
return res;
/*
* If it is an iterative symlinks resolution in open_namei() we
* have to copy the last component. And all that crap because of
* bloody create() on broken symlinks. Furrfu...
*/
name = __getname();
if (unlikely(!name)) {
path_release(nd);
return -ENOMEM;
}
strcpy(name, nd->last.name);
nd->last.name = name;
return 0;
fail:
path_release(nd);
return PTR_ERR(link);
}
__vfs_follow_link()函数本质上依次执行下列操作:
1. 检查符号链接路径名的第一个字符是否是“/”:在这种情况下,已经找到一个绝对路径名,因此没有必要在内存中保留前一个路径的任何信息。如果是,对nameidata数据结构调用path_release(),因此释放由前一个查找步骤产生的对象;然后,设置nameidata数据结构的dentry和mnt字段,以使它们指向当前进程的根目录。
2. 调用link_path_walk()解析符号链的路径名,传递给它的参数为路径名和nd。
3. 返回从link_path_walk()取回的值。
当do_follow_link()最后终止时,它把局部变量next的dentry字段设置为目录项对象的地址,而这个地址由符号链接传递给原先就执行的link_path_walk()。link_path_walk()函数然后进行下一步。