1、符号链接与硬链接
首先我们来分清符号链接与硬链接的概念。
Linux 文件系统最重要的特点之一是它的文件链接。链接是对文件的引用,这样您可以让文件在文件系统中多处被看到。两种链接都 可以通过命令 ln 来创建。ln 默认创建的是硬链接。使用 -s 开关可以创建符号链接。但是它们在内核中使用的是两个不同的系统调用,分别对应内核的sys_link和sys_symlink两个函数。
1.1硬链接
对于硬链接来说,一个文件并没有自己的inode,它只有一个directory entry,它的inode直接索引到原始文件的inode。硬连接是不会建立inode的,他只是在文件原来的inode link count域再增加1而已,也因此硬链接是不可以跨越文件系统的。硬链接实际上是为文件建一个别名,链接文件和原文件实际上是同一个文件,因为它引用的就是是原始文件在文件系统中的物理索引( inode)。当移动或删除原始文件时,硬链接不会被破坏,因为它所引用的是文件的物理数据而不是文件在文件结构中的位置。硬链接的文件不需要用户有访问原始文件的权限,也不会显示原始文件的位置。如果您删除的文件有相应的硬链接,那么这个文件依然会保留,直到所有对它的引用(inode link count为0)都被删除。
对硬链接有一下两个限制:
1、不能对目录文件做硬链接。
2、不能在不同的文件系统之间做硬链接。也就是说,链接文件和被链接文件必须位于同一个文件系统中。
1.2符号链接
符号链接也称为软连接。其实就是新建立一个文件,这个文件就是专门用来指向别的文件的(那就和windows 下的快捷方式的那个文件有很接近的意味)。软连接产生的是一个新的文件,但这个文件的作用就是专门指向某个文件的,删了这个软连接文件,那就等于不需要这个连接,和原来的存在的实体原文件没有任何关系,但删除原来的文件,则相应的软连接不可用(cat那个软链接文件,则提示“没有该文件或目录“)。
在符号链接中,ext2文件系统会会这个文件创建一个inode节点,而这个inode节点中将会保存传入的符号的路径名称。对于常规的文件,它的inode使用经典的三级引用模型,这个存储单位为
struct ext2_inode_info {
__le32 i_data[15];
这个i_data保存的本来是13个1级文件,1个2级,一个三次间接索引。如果说一个符号链接中输入的字符串长多小于sizeof i_data=60个字符,符号字符串的地址就可以直接放在这里。符号链接,它真正保存的只是一个字符串,当连接建立之后,链接文件与原始文件之间是没有必然联系的。当删除原始文件之后,链接文件依然存在。或者符号链接建立开始的时候目的(原始文件)是一个文件,之后修改目的文件(原始文件)为目录,符号的源就会对应的变换为目录。
struct ext2_inode_info {
__le32 i_data[15];
这个i_data保存的本来是13个1级文件,1个2级,一个三次间接索引。如果说一个符号链接中输入的字符串长多小于sizeof i_data=60个字符,符号字符串的地址就可以直接放在这里。符号链接,它真正保存的只是一个字符串,当连接建立之后,链接文件与原始文件之间是没有必然联系的。当删除原始文件之后,链接文件依然存在。或者符号链接建立开始的时候目的(原始文件)是一个文件,之后修改目的文件(原始文件)为目录,符号的源就会对应的变换为目录。
最后总结两点:
1、硬链接本身就是一个目录项,(比如文件名+索引节点号),可以将文件名与文件的位置关联起来。
2、符号链接也称软链接,是指存储了一个字符串的文件。这个字符串可以在路径名解析的过程中,用于修改路径名。
2、do_follow_link
do_follow_link相当于一个接口函数,它将底层真正处理符号链接的函数做了封装,并从上面接收参数。下面从两大方面讲这个函数。
2.1判断和传参
首先,我们要明确一点,do_follow_link是处理符号链接的,因为它所封装的 _do_follow_link 就是基于符号链接的限制(链接数、层数)做处理的。
然后就是判断。判断符号链接的方法要尤其注意,代码中是这样写的:
if (inode->i_op->follow_link) {
err = do_follow_link(&next, nd);
... ...
也就是说如果 i_op->follow_link方法存在,就要进行do_follo_link。而这就是
判断标准,即如果索引节点有自定义的follow_link方法,那么这个
索引节点
就是一个
符号链接
,
在原路径名的查找操作就必须先对这个符号链接进行解释。这应该是在创建文件的时候就决定的。如果创建的文件是符号链接文件,那么文件系统就会把它的inode节点赋予一个follow_link方法。
最后就是传参的注意。我们知道在 __link_path_walk 中,每当一个循环结束,或是__link_path_walk函数结束,nd 中存放的是当前处理(循环)结束时,所找到的当前目标节点的信息。而在每个循环处理前,nd 也就是所要找的当前目标节点的父节点信息。在do_lookup 结束后,所找到的当前目标节点信息存放在 next 中 ,inode 用 next 赋值。见如下代码:
err = do_lookup(nd, &this, &next);
... ...
inode = next.dentry->d_inode;
err = do_lookup(nd, &this, &next);因此若当前节点不是符号链接,不需要处理,就要把目前还存储着所找到的当前目标节点父节点信息的 nd 转变为存储当前节点信息,于是就调用 path_to_nameidata(&next, nd);将存储着当前节点的 path 结构(名称为next)中的内容移到 nd 中去。而如果是符号链接,就调用 err = do_follow_link(&next, nd);传递进去的是当前所找到节点(符号链接节点)的path信息以及父节点的 nd。而 do_follow_link返回后,通过对符号链接地址解析后得到的最终目标节点就存放在 nd 中返回。这也就是为什么 inode 用nd 重新赋值。见如下代码:
if (inode->i_op->follow_link) {
err = do_follow_link(&next, nd);
... ...
inode = nd->path.dentry->d_inode;
2.2预处理
进入do_follow_link后,会先做一些预处理工作,主要包括这几个方面:
1、检查current->link_count是否大于等于MAX_NESTED_LINKS(当前内核定义为8),若是,则返回错误码-ELOOP。
2、检查current->total_link_count是否大于等于40,若是,则返回错误码-ELOOP。
3、若使当前进程需要,则调用cond_resched()进行进程交换(设置当前进程描述符thread_info中的TIF_NEED_RESCHED)。
4、调用security_inode_follow_link(path->dentry, nd)来对解析符号链接进行安全检查,若失败则返回错误码。
2、检查current->total_link_count是否大于等于40,若是,则返回错误码-ELOOP。
3、若使当前进程需要,则调用cond_resched()进行进程交换(设置当前进程描述符thread_info中的TIF_NEED_RESCHED)。
4、调用security_inode_follow_link(path->dentry, nd)来对解析符号链接进行安全检查,若失败则返回错误码。
5、递增current->link_count、current->total_link_count和nd->depth。
6、调用__do_follow_link(path, nd, &cookie)。
6、调用__do_follow_link(path, nd, &cookie)。
关于link的限制,补充如下:
1. 最大嵌套链接数: 8 (2.6.35)
示例:
link0 <- link01 <- link02 <- link03 <- link04 <- link05 <- link06 <- link07 <- link08 <- link09
root
@localhost
:/home/James/mnt1# ls link08
ls: 无法访问link08: 符号连接的层数过多
root
@localhost
:/home/James/mnt1# ls link07
link1
2. 即使链接非嵌套,允许的最多连接数是40 (2.6.35)
大概是如下形式...../link1/link2/link3/.../linkn/....
这是为了防止恶意软件使用一个非常长的lookup path来冻结kernel。
3 __do_follow_link
3.1 nd_set_link
nameidata的 saved_names 用于存放相应深度的符号链接的所指向的路径名,以深度为索引。nd_set_link 的作用就是把路径名字符串存放进 nd->saved_names 中。
当进入到具体文件系统 ext2_follow_link 中,它其实就是调用的nd_set_link。作用就是把符号链接索引节点的i_data(由i_block 所赋值)传给saved_names。因为前面已经讲过,符号链接索引节点的i_data不用存放文件数据(它没有文件内容),就是存放所链接路径名的字符串的。
3.2 nd_get_link
这个函数的作用就是把saved_names 中以深度为索引的存放有符号链接的所指向的路径名取出来。在_do_follow_link中它就是把之前存放进 nd 的符号链接中的路径名字符串取出来,传给 __vfs_follow_link 进行最终的解析。
3.3 __vfs_follow_link
进入到这个函数,就相当于嵌套的另起一个新的路径名解析了,传入的路径名就是符号链接所指代的路径名。所以它与do_path_lookup 的逻辑很类似,关键部分都是 先解析路径名的第一个分量,看是否是绝对路径“/”,若是则调用walk_init_root,将nd 设置为 nd->path = fs->root。随后便是调用 link_path_walk 开始解析路径名。
要注意,刚进入 _vfs_follow_link时,传递给它的nd 是还是符号链接节点父目录的nd,而当 link_path_walk 结束,随之_vfs_follow_link 结束返回时,nd 就变成路径分析完毕后,当前目标节点的 nd。
当 _vfs_follow_link 结束,随之do_follow_link 结束返回后,__link_path_walk(path_walk)的main loop就真的结束了。
4、关于文件路径查找的问题
在研究过程中,在网上找到一个关于路径查找的问题讨论,觉得很有启发和意义。问题描述大概如下:
假设我们有如下的目录结构:
/home/a/b/c
/home/1/2/3/4/5/6/7
现在我们使用命令 ln -s /home/1/2/3/ /home/a/b/c/d_ln 在/home/a/b/c下生成一个符号连接d_ln, 并让这个连接指向/home/1/2/3.
然后,
cd /home/a/b/c/d_ln/4/5/6/../../../../
这时我们的当前目录被设成了 /home/a/b/c,可是根据代码,我们的当前目录应该是/home/1/2,因为nd->path被current->fs->root覆盖了(请查看do_follow_link()->__do_follow_link()->__vfs_follow_link()->walk_init_root())。那内核是如何回到/home/a/b/c的呢?
/home/a/b/c
/home/1/2/3/4/5/6/7
现在我们使用命令 ln -s /home/1/2/3/ /home/a/b/c/d_ln 在/home/a/b/c下生成一个符号连接d_ln, 并让这个连接指向/home/1/2/3.
然后,
cd /home/a/b/c/d_ln/4/5/6/../../../../
这时我们的当前目录被设成了 /home/a/b/c,可是根据代码,我们的当前目录应该是/home/1/2,因为nd->path被current->fs->root覆盖了(请查看do_follow_link()->__do_follow_link()->__vfs_follow_link()->walk_init_root())。那内核是如何回到/home/a/b/c的呢?
分析的关键代码是下面这段:
static __always_inline void follow_dotdot(struct nameidata *nd)
{
.......
if (nd->path.dentry != nd->path.mnt->mnt_root) {
/* 在这里我们会把nd->path.dentry指向其父目录的dentry结构,那么
/home/a/b/c/d_ln/4 的d_parent到底是/home/a/b/c/d_ln 还是
/home/1/2/3 呢?
*/
nd->path.dentry = dget(nd->path.dentry->d_parent);
其实 /home/a/b/c/d_ln/4 的 d_parent 是 /home/1/2/3 这是毋庸置疑的,但是为什么会出现上述现象呢?这是因为 cd 命令虽然主要是调用 系统调用 ch_dir()(这个 API系统调用陷入内核后是调用的 sys_chdir()),但是它在这之前会做其它的一些处理。它会先将传入 cd 命令的路径名 在路径名字符串层面上先做解析,会将 “../ ”这样的路径分量直接合并,解释成上一层目录。因此 实际传入ch_dir()中的路径名是经过解析精简之后的(ch_dir(/home/a/b/c))。 通过 flw2 大神给出的验证代码可以看到这一点,如果是直接通过 ch_dir()调用:chdir("/home/a/b/c/d_ln/4/5/6/../../../../"); 那么调用结束得到的目录是 /home/1/2 。
关于 cd 命令的小补充。cd 命令的实现源代码可以查看 bash 的源码,其中 cd.def 就是它的实现。一般的 shell 命令如 cat、vim、ls 等可以通过 strace cat、 strace ls 这样的方式查看这些 shell 命令后台实现的所用到的系统调用过程。然而 cd 命令不可以,通过 type cd 可以看出,cd 命令是一个 内建shell(builtin)。
至此,Ext2 文件系统的路径名查找分析告一段落。这个过程涉及与文件系统有关的几乎所有数据结构以及这些数据结构之间的联系,搞懂了这个过程就对文件系统有了基本的理解。