linux文件系统--从路径名到目录节点

本博客先介绍几个函数的代码,主要是两个函数,即path_init和path_walk以及它们下面的一些底层函数。目的在于帮助读者加深对文件系统内部结构的理解,同时也为以后的代码阅读做些准备,因为以这两个函数为入口的操作比较大,并且很重要,在后面的博客中常常要用到。这两个函数通常是在一起调用的,二者合在一起就可以根据给定的文件路径名在内存中找到或者建立代表着目标文件或者目录的dentry结构和inode结构。在老的一些版本中,这一部分功能一直是通过一个namei的函数完成的,现在则有了新的实现。与namei相对应,现在有一个函数__user_walk将path_init和path_walk包装在一起。不过,内核代码中直接调用这两个函数的地方也不少。本博客涉及的代码基本都在fs/namei.c中。

先看外层包装,__user_walk:

/*
 *	namei()
 *
 * is used by most simple commands to get the inode of a specified name.
 * Open, link etc use their own routines, but this is enough for things
 * like 'chmod' etc.
 *
 * namei exists in two versions: namei/lnamei. The only difference is
 * that namei follows links, while lnamei does not.
 * SMP-safe
 */
int __user_walk(const char *name, unsigned flags, struct nameidata *nd)
{
	char *tmp;
	int err;

	tmp = getname(name);
	err = PTR_ERR(tmp);
	if (!IS_ERR(tmp)) {
		err = 0;
		if (path_init(tmp, flags, nd))
			err = path_walk(tmp, nd);
		putname(tmp);
	}
	return err;
}

其中调用参数name指向在用户空间中的路径名,flags的内容则是一些标志位,定义于文件include/linux/fs.h:

/*
 * The bitmask for a lookup event:
 *  - follow links at the end
 *  - require a directory
 *  - ending slashes ok even for nonexistent files
 *  - internal "there are more path compnents" flag
 */
#define LOOKUP_FOLLOW		(1)
#define LOOKUP_DIRECTORY	(2)
#define LOOKUP_CONTINUE		(4)
#define LOOKUP_POSITIVE		(8)
#define LOOKUP_PARENT		(16)
#define LOOKUP_NOALT		(32)

这些标志位都是对怎样寻找目标的指示。例如LOOKUP_DIRECTORY表示要寻找的目标必须是一个目录;而LOOKUP_FOLLOW表示如果找到的目标指示符号链接到其他文件或者目录的一个目录项,则要顺着链接一直找到终点。所谓链接是指一个节点直接指向另一个节点,成为另一个节点的代表。注意符号链接与普通链接不同,普通的链接只能建立在同一个存储设备上面,而符号链接可以跨设备的;内核提供了两个不同的系统调用link和symlink。分别是普通链接和符号链接,所以符号链接可能会落空。当路径中包含符号链接时,对于是否继续顺着链接往下搜索,则另有一些附加规定。我看下注释:

/* [Feb-Apr 2000 AV] Complete rewrite. Rules for symlinks:
 *	inside the path - always follow.
 *	in the last component in creation/removal/renaming - never follow.
 *	if LOOKUP_FOLLOW passed - follow.
 *	if the pathname has trailing slashes - follow.
 *	otherwise - don't follow.
 * (applied in that order).
 *
 * [Jun 2000 AV] Inconsistent behaviour of open() in case if flags==O_CREAT
 * restored for 2.4. This is the last surviving part of old 4.2BSD bug.
 * During the 2.4 we need to fix the userland stuff depending on it -
 * hopefully we will be able to get rid of that wart in 2.5. So far only
 * XEmacs seems to be relying on it...
 */

注释谈到,如果在一个路径名内部的某个中间存在符号链接,那就总是要跟随;而创建、删除、改名操作中如果路径名的最后一个节点是符号链接则不要跟随。其他标志位的用途,看到再解释。

struct nameidata {
	struct dentry *dentry;
	struct vfsmount *mnt;
	struct qstr last;
	unsigned int flags;
	int last_type;
};

这种数据结构是临时性的,只用来返回搜索的结果。成功返回时,其中的指针dentry指向所找到的dentry结构,而在该dentry结构中则有指针指向相应的inode结构。指针mnt则指向一个vfsmount数据结构,它记录这所属文件系统的安装信息,例如文件系统的安装点、文件系统的根节点等等。

 回到__user_walk,先通过getname在系统空间分配一个页面,并从用户空间把文件名复制到这个页面中。由于分配的是一个页面,所以这个路径名可以长达4k字节。同时,因为这块空间是动态分配的,所以在是用完之后要通过putname将其释放,代码中用到的PTR_ERR、IS_ERR都是inline函数,均在fs.h中:

static inline long PTR_ERR(const void *ptr)
{
	return (long) ptr;
}

static inline long IS_ERR(const void *ptr)
{
	return (unsigned long)ptr > (unsigned long)-1000L;
}

剩下的是紧挨在一起的path_init和path_walk两个函数了。先看path_init的代码:


/* SMP-safe */
int path_init(const char *name, unsigned int flags, struct nameidata *nd)
{
	nd->last_type = LAST_ROOT; /* if there are only slashes... */
	nd->flags = flags;
	if (*name=='/')
		return walk_init_root(name,nd);
	read_lock(&current->fs->lock);
	nd->mnt = mntget(current->fs->pwdmnt);
	nd->dentry = dget(current->fs->pwd);
	read_unlock(&current->fs->lock);
	return 1;
}

首先将nameidata结构中的last_type字段设置成LAST_ROOT。这个字段可能有的值定于与fs.h中:

/*
 * Type of the last component on LOOKUP_PARENT
 */
enum {LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT, LAST_BIND};

在搜索过程中,这个字段的值会随着路径名的当前搜索结构而变。例如:如果成功地找到了目标文件,那么这个字段的值就变成了LAST_NORM;而如果最后停留在一个"."上,则变成LAST_DOT。

下面就取决于路径名是否以"/"开头了。

我们先看相对路径名,即不以"/"开头的情况,以前讲过,进程的task_struct结构中有个指针fs指向一个fs_struct结构。在fs_struct结构中有个指针pwd指向进程的当前工作目录的dentry结构。相对路径是从当前工作目录开始的,所以将nameidata结构中的指针dentry也设置成指向这个当前工作目录的dentry结构,表示在虚拟的绝对路径中这个节点以及所有在此之前的节点都已经解决了同时这个具体的dentry结构现在多了一个用户,所以要调用dget递增其共享计数。除此以外,fs_struct结构中还有个指针pwdmnt指向一个vfsmount结构。每当将一个存储设备安装到现有的文件系统中的某个节点时,内核就要为之建立一个vfsmount结构,这个结构中既包含有关该设备的信息,也包含有关安装点的信息,系统中的每个文件系统,包括根设备上的文件系统,都要经过安装,所以fs_struct结构中的指针pwdmnt总是指向一个vfsmount结构。具体其他博客会讲解。相应的,在nameidata结构中也有一个指针mnt,要把它设置成指向同一个vfsmount结构。这样,对路径搜索的准备工作,即nameidata结构的初始化就完成了。

可是,如果路径名是以"/"开头的绝对路径,那就要通过walk_init_root从根节点开始查找(fs/namei.c):


/* SMP-safe */
static inline int
walk_init_root(const char *name, struct nameidata *nd)
{
	read_lock(&current->fs->lock);
	if (current->fs->altroot && !(nd->flags & LOOKUP_NOALT)) {
		nd->mnt = mntget(current->fs->altrootmnt);
		nd->dentry = dget(current->fs->altroot);
		read_unlock(&current->fs->lock);
		if (__emul_lookup_dentry(name,nd))
			return 0;
		read_lock(&current->fs->lock);
	}
	nd->mnt = mntget(current->fs->rootmnt);
	nd->dentry = dget(current->fs->root);
	read_unlock(&current->fs->lock);
	return 1;
}

如果当前进程并未通过chroot系统调用设置自己的替换根目录, 则代码中if语句里的current->fs->altroot为0,所以把nameidata中的两个指针分别设置成指向当前进程的根目录的dentry结构以及所在设备的vfsmount结构。反之,如果已经设置了替换根目录,那就要看当初调用path_init的参数flags中的标志位LOOKUP_NOALT是否为1了。通常这个标志位为0,所以如果已经设置了替换根目录就会通过__emul_lookup_dentry将nameidata结构中的指针设置成指向替换根目录。

 这个替换根目录到底是怎么回事呢?原来,在有些Unix变种中,可以在文件系统中通常在/usr下面,创建一棵子树,例如/usr/altroot/home/user1/...。然后,当用户调用chroot设置其自己的根目录时,系统会自动将该进程的fs_struct结构中的altroot和altrootmnt两个指针设置成给定路径名在前述子树中的对应节点,那个对应节点就成为了替换根目录。不过在i386处理器上的linux目录并不支持这种功能,所以这里if语句中的current->fs->altroot总是为NULL,因为不起作用。

从path_init成功返回时,nameidata结构中的指针dentry指向路径搜索的起点,接着就是通过path_walk顺着路径名的指引进行搜索了。这个函数比较大,所以我们逐段分析:

int path_walk(const char * name, struct nameidata *nd)
{
	struct dentry *dentry;
	struct inode *inode;
	int err;
	unsigned int lookup_flags = nd->flags;

	while (*name=='/')
		name++;
	if (!*name)
		goto return_base;

	inode = nd->dentry->d_inode;
	if (current->link_count)
		lookup_flags = LOOKUP_FOLLOW;

如果路径名以"/"开头的,就把它跳过去,因为在这种情况下nameidata结构中的指针dentry已经指向本进程的根目录了。注意,多个连续的"/"与一个"/"字符是等价的。如果路径名中仅含有"/"字符的话,name其目标就是根目录,所以任务已经完成,可以返回了。不然,就继续搜索。

进程的task_struct结构中有个计数器link_count。在搜索过程中有可能碰到一个节点(目录项)只是指向另一个节点的连接,此时就用这计数器来对链的长度进行计算,这样,当链的长度达到某个值就可以终止搜索而返回失败,以防止循环。另一方面,当顺着符号链接进入另一个设备上的文件系统时,有可能会递归的调用path_walk。所以进入path_walk后,如果发现这个计数器非0,那就表示正在顺着符号链接递归调用path_walk往前搜索的过程中。此时不管怎样都把LOOKUP_FOLLOW标志位设成1。这里还要指出,作为path_walk起点的节点必定是一个目录,一定有相应的索引节点存在,所以指针inode一定是有效的,不可能是空指针。

接下去是一个对路径中的节点所作的for循环,由于循环体比较大,我们也只好分段来看。

	/* At this point we know we have a real path component. */
	for(;;) {
		unsigned long hash;
		struct qstr this;
		unsigned int c;

		err = permission(inode, MAY_EXEC);
		dentry = ERR_PTR(err);
 		if (err)
			break;

		this.name = name;
		c = *(const unsigned char *)name;

		hash = init_name_hash();
		do {
			name++;
			hash = partial_name_hash(c, hash);
			c = *(const unsigned char *)name;
		} while (c && (c != '/'));
		this.len = name - (const char *) this.name;
		this.hash = end_name_hash(hash);

		/* remove trailing slashes? */
		if (!c)
			goto last_component;
		while (*++name == '/');
		if (!*name)
			goto last_with_slashes;

首先检查当前进程对当前节点的访问权限。函数permission的代码与作用于访问权限与文件安全性有关,放在其他博客讲解。这所检查的是对路径中各层目录的访问权限。注意,对于中间节点所需的权限为执行权,即MAY_EXEC。如果不符,则permission返回一个出错代码,从而通过break语句结束循环,搜索就失败了。

循环体中的局部变量this是个qstr数据结构,用来存放路径名中当前节点的杂凑值以及节点名长度,这个数据结构的定义在dcache.h中:

/*
 * "quick string" -- eases parameter passing, but more importantly
 * saves "metadata" about the string (ie length and the hash).
 */
struct qstr {
	const unsigned char * name;
	unsigned int len;
	unsigned int hash;
};

回到代码中的第453-457行,这几行的作用就是逐个字符地计算出当前节点名的杂凑值,至于具体的杂凑函数,我们就不关心了。

路径名中的节点是以"/"字符分隔的,所以紧随当前节点名的字符只有两种可能:

(1)是"\0",就是说当前节点已经是路径名中的最后一节,所以转入last_component。

(2)是个"/"字符,这里又有两种可能,第一种情况是当前节点实际上已是路径名中的最后一个节点,只不过在此后面又多添加了若干个"/"字符。这种情况常常发生在用户界面上,特别是shell的命令中,例如ls /usr/include/ ,这是允许的。但是当然最后的节点必须是一个目录,所以此时转入到last_with_slashes。第二种情况就是当前节点为中间节点(包括起始节点),所以"/"字符后面还有其他字符。这种情况下就将其跳过。继续往下执行。

现在,要回过头来看当前节点了。记住,这个节点一定是中间节点或起始节点(否则就转到last_component去了),这种节点一定是一个目录,对于代表着文件的节点名来说,以"."开头表示这个是隐藏的文件,而对于代表着目录的节点名则只有在两种情况下才是允许的。一种是节点名为".",表示当前目录,即不改变目录。另一种就是"..",表示当前目录的父目录。

继续往下看:

		/*
		 * "." and ".." are special - ".." especially so because it has
		 * to be able to know about the current root directory and
		 * parent relationships.
		 */
		if (this.name[0] == '.') switch (this.len) {
			default:
				break;
			case 2:	
				if (this.name[1] != '.')
					break;
				follow_dotdot(nd);
				inode = nd->dentry->d_inode;
				/* fallthrough */
			case 1:
				continue;
		}

就是说,如果当前节点名的第一个字符是".",则节点名的长度只能是1或者2,并且当长度为2时第二字符也必须时".",否则搜索就失败了。

如果当前节点名真的是"..",那就要往上跑到当前已经到达的节点nd->dentry的父目录去。这是由

follow_dotdot完成的。

 
static inline void follow_dotdot(struct nameidata *nd)
{
	while(1) {
		struct vfsmount *parent;
		struct dentry *dentry;
		read_lock(&current->fs->lock);
		if (nd->dentry == current->fs->root &&
		    nd->mnt == current->fs->rootmnt)  {//检查是否是根设备
			read_unlock(&current->fs->lock);
			break;
		}
		read_unlock(&current->fs->lock);
		spin_lock(&dcache_lock);
		if (nd->dentry != nd->mnt->mnt_root) {//第二种情况,直接访问父目录
			dentry = dget(nd->dentry->d_parent);
			spin_unlock(&dcache_lock);
			dput(nd->dentry);
			nd->dentry = dentry;
			break;
		}
		parent=nd->mnt->mnt_parent;
		if (parent == nd->mnt) {
			spin_unlock(&dcache_lock);
			break;
		}
		mntget(parent);
		dentry=dget(nd->mnt->mnt_mountpoint);
		spin_unlock(&dcache_lock);
		dput(nd->dentry);
		nd->dentry = dentry;
		mntput(nd->mnt);
		nd->mnt = parent;
	}
}

但是这里又要分三种情况:

第一种情况,已到达节点nd->dentry就是本进程的根节点,这时不能在往上跑了,所以保持nd->dentry不变。

第二种情况,已到达节点nd->dentry与其父节点在同一个设备上。在这种情况下,既然已经到达的这个节点的dentry结构已经建立,则其父节点的dentry结构也必然已经建立在内存中,而且dentry结构中的指针d_parent就指向其父节点,所以往上跑一层是很简单的事情。

最后一种情况,已到达节点nd->dentry就是其所在设备上的根节点,往上跑就要跑到另一个设备上去了。如前所述,当将一个存储设备安装到另一个设备上的某个节点时,内核会分配和设置一个vfsmount结构,通过这个结构将两个设备以及两个节点连接起来,但是根设备的这指针则指向自己,因为它再没有父设备了,而另一个指针mnt_mountpoint,则指向代表着安装点(一定是个目录)结构。从文件系统的角度来看,安装点与所安装设备的根目录是等价的。我们已经在当前设备的根目录中,所以从这里往上跑一层就是要跑到安装点的上一层目录中(而不是安装点本身)。

先检查当前的vfsmount结构是否代表着根目录,如果是的话,立刻通过389行的break语句结束while循环。这样,nameidata结构中的dentry和mnt保持不变。这种情况相当于在根目录下执行命令 cd .. 或则和cd ../..等等,读者可以试验一下,看看结果如何。

反之,要是当前设备不是根设备,那就把nameidata结构中的两个指针分别设置成指向上一层设备的vfsmount结构以及该设备上的安装点的上一层目录(dentry结构),然后回到while循环的开始处。一般来说,安装点不会是一个设备上的根目录,所以这一次循环会将nameidata结构中的指针dentry指向安装点的父目录。可是,万一安装点真的就是上一层设备上的根目录(当然,必定是空的)呢?那也不要紧,只不过是再循环一次,在往上跑一层而已。

回到path_walk的代码中,注意case 2 的末尾没有break语句,所以会落入case 1中通过continue语句回到for(;;)循环的开头,继续处理路径中的下一个节点名。

当然,多数情况下节点名都不是以"."开头的,就是说多数情况下总是顺着路径名逐层往下跑,而不是往上跑的。我们继续往下看对正常节点名的流程:

		/*
		 * See if the low-level filesystem might want
		 * to use its own hash..
		 */
		if (nd->dentry->d_op && nd->dentry->d_op->d_hash) {
			err = nd->dentry->d_op->d_hash(nd->dentry, &this);
			if (err < 0)
				break;
		}
		/* This does the actual lookups.. */
		dentry = cached_lookup(nd->dentry, &this, LOOKUP_CONTINUE);
		if (!dentry) {
			dentry = real_lookup(nd->dentry, &this, LOOKUP_CONTINUE);
			err = PTR_ERR(dentry);
			if (IS_ERR(dentry))
				break;
		}
		/* Check mountpoints.. */
		while (d_mountpoint(dentry) && __follow_down(&nd->mnt, &dentry))
			;

		err = -ENOENT;
		inode = dentry->d_inode;
		if (!inode)
			goto out_dput;
		err = -ENOTDIR; 
		if (!inode->i_op)
			goto out_dput;

		if (inode->i_op->follow_link) {
			err = do_follow_link(dentry, nd);
			dput(dentry);
			if (err)
				goto return_err;
			err = -ENOENT;
			inode = nd->dentry->d_inode;
			if (!inode)
				break;
			err = -ENOTDIR; 
			if (!inode->i_op)
				break;
		} else {
			dput(nd->dentry);
			nd->dentry = dentry;
		}
		err = -ENOTDIR; 
		if (!inode->i_op->lookup)
			break;
		continue;
		/* here ends the main loop */

有些文件系统通过dentry_operations结构中的指针d_hash提供它自己专用的杂凑函数,所以在这种情况下就通过这个函数再计算一遍当前节点的杂凑值。

至此,所有的准备工作都已完成,接下来就要去开始搜索了。

对当前节点的搜索是通过cached_lookup和real_lookup两个函数进行的,先通过cached_lookup在内存中寻找该节点业已存在dentry结构。内核中有个杂凑表dentry_hashtable,是一个list_head指针数组,一旦内存中建立起一个目录节点的dentry结构,就根据其节点名的杂凑值挂入杂凑表中的某个队列中,需要寻找时则还是根据杂凑值从杂凑表按着手。当路径名中的某个节点变成path_walk的当前节点时,位于其上游的所有节点必定都已经有dentry内存结构,而当前节点本身以及其下一个节点则不一定。如果在内存中找不到当前节点的dentry结构,那就要进一步通过real_lookup到磁盘上通过其所在的目录寻找,找到后在内存中为其建立起dentry结构并将其挂入杂凑表的某个队列中。这里说的杂凑表就是哈希表。

内核中有一个队列dentry_unused,凡是已经没有用户,即共享计数为0的dentry结构就通过结构中的另一个list_head挂入这个队列。这个队列是一个LRU队列,当需要回收已经不在使用中的dentry结构的空间时,就从这个队列中找到已经空闲最久的dentry结构,再把这个结构从杂凑表队列中脱链而加以释放。所以,dentry_unused是为缓冲存储而设置的辅助性的队列。不过,在一些特殊的情况下,可能会把一个还在使用中的dentry结构从杂凑表脱链,迫使以后要访问这个节点的进程重新根据磁盘上的内容另行构筑一个dentry结构,而已经脱链的那个数据结构则由最后调用dput使其共享计数变成0的进程负责将其释放。

事实上,dentry结构中有6个list_head,即d_vfsmnt、d_hash、d_lru、d_child、d_subdirs、d_alias。注意list_head既可以用来作为一个队列的头部,也可以用来将其所在的数据结构挂入某个队列中。其中d_vfsmnt仅在其作为一个安装点才使用。一个dentry结构一经建立就通过其d_hash挂入dentry_hashtable中的某个队列里,当共享计数变成0时通过d_lru挂入LRU队列dentry_unused中。同时dentry结构通过d_child挂入父节点的d_subdirs中,同时又通过指针d_parent指向其父目录的dentry结构。而自己各个子目录的dentry结构则在它本身的d_subdirs队列里。

一个有效的dentry结构必定有一个相应的inode结构,这是因为一个目录项要么就代表这一个文件,要么就代表着一个目录,而目录实际上也是文件。所以,只要是有效的dentry结构,则其指针d_inode必定指向一个inode结构。可是,反过来一个inode却可能对应着不止一个dentry结构,也就是说,一个文件可以有不止一个文件名。这是因为一个已经建立的文件可以被链接到其他文件名。所以,在inode结构中有个队列i_dentry,凡是代表着这个文件的所有目录项都通过其dentry结构中的d_alias挂入相应inode结构中的i_dentry队列。此外,dentry结构中还有个指针d_sb,指向其所在设备的超级块的super_block数据结构,以及指针d_op指向特定文件系统的dentry_operations结构。也就是说,dentry结构是文件系统的核心数据结构,也是文件访问和为文件访问而做的文件路径搜索操作的枢纽。

下面做个总结:

  • 每个dentry结构都通过队列头d_hash链入到dentry_hashtable中的某个队列中。
  • 共享计数为0的dentry结构都通过队列头d_lru链入LRU队列dentry_unused,在队列中等待释放或者东山再起。
  • 每个dentry结构都通过指针d_inode指向一个inode数据结构。但是多个dentry结构可以指向同一个inode数据结构。
  • 指向同一个inode数据结构的dentry结构都通过队列头d_alias链接在一起,都在该inode结构的i_dnetry队列中
  • 每个dentry结构都通过指针d_parent指向其父目录节点的dentry结构,并通过队列头d_child跟同一目录的其他节点的dentry结构链接在一起,都在父目录节点的d_subdirs队列中
  • 每个dentry结构都通过指针d_sb指向一个super_block结构
  • 每个dentry结构都有一个队列头d_vsfmnt,用于文件系统的安装,后面博客会讲解。

接下来我们看cached_lookup实现:

/*
 * Internal lookup() using the new generic dcache.
 * SMP-safe
 */
static struct dentry * cached_lookup(struct dentry * parent, struct qstr * name, int flags)
{
	struct dentry * dentry = d_lookup(parent, name);

	if (dentry && dentry->d_op && dentry->d_op->d_revalidate) {
		if (!dentry->d_op->d_revalidate(dentry, flags) && !d_invalidate(dentry)) {
			dput(dentry);
			dentry = NULL;
		}
	}
	return dentry;
}

这里主要是d_lookup,在杂凑表中寻找,其代码如下:


/**
 * d_lookup - search for a dentry
 * @parent: parent dentry
 * @name: qstr of name we wish to find
 *
 * Searches the children of the parent dentry for the name in question. If
 * the dentry is found its reference count is incremented and the dentry
 * is returned. The caller must use d_put to free the entry when it has
 * finished using it. %NULL is returned on failure.
 */
 
struct dentry * d_lookup(struct dentry * parent, struct qstr * name)
{
	unsigned int len = name->len;
	unsigned int hash = name->hash;
	const unsigned char *str = name->name;
	struct list_head *head = d_hash(parent,hash);
	struct list_head *tmp;

	spin_lock(&dcache_lock);
	tmp = head->next;
	for (;;) {
		struct dentry * dentry = list_entry(tmp, struct dentry, d_hash);
		if (tmp == head)
			break;
		tmp = tmp->next;
		if (dentry->d_name.hash != hash)
			continue;
		if (dentry->d_parent != parent)
			continue;
		if (parent->d_op && parent->d_op->d_compare) {
			if (parent->d_op->d_compare(parent, &dentry->d_name, name))
				continue;
		} else {
			if (dentry->d_name.len != len)
				continue;
			if (memcmp(dentry->d_name.name, str, len))
				continue;
		}
		__dget_locked(dentry);
		dentry->d_flags |= DCACHE_REFERENCED;
		spin_unlock(&dcache_lock);
		return dentry;
	}
	spin_unlock(&dcache_lock);
	return NULL;
}

参数parent指向上一层节点的dentry结构,而name指向刚才在path_walk中建立的qstr结构。首先要根据节点名的杂凑值从杂凑表中找到相应的队列指针,本来,以已经计算好的杂凑值作为下标从list_head指针数组dentry_hashtable中找相应的表项是再简单不过的,可是这里719行还要再通过一个函数d_hash来做这件事,让我们看看为什么:

static inline struct list_head * d_hash(struct dentry * parent, unsigned long hash)
{
	hash += (unsigned long) parent / L1_CACHE_BYTES;
	hash = hash ^ (hash >> D_HASHBITS) ^ (hash >> D_HASHBITS*2);
	return dentry_hashtable + (hash & D_HASHMASK);
}

就是说在已经根据节点名计算好的杂凑值基础上还要再进行一次杂凑,把父节点的dentry结构的地址也结合进杂凑值中。这无疑是很巧妙的做法。试想一下学校的计算机实验室,那里的系统可能为上百个学生分别在/home下面建立了子目录,而每个同学的子目录下可能都一个子目录project1。如果光是对节点名project1做杂凑(哈希),则势必至少有上百个dentry结构都挂在同一个队列中而需要线性搜索。即使把父节点名也一起杂凑也还是解决不了问题,因为每个学生都可能会有例如proceject1/src这样的路径,所以此类路径中的src节点又会在同一个队列中,对全路径名进行杂凑当然可以解决问题,但是那样代价又太高了。

找到了相应的队列头部以后,d_lookup中的for循环是简单的,唯一特殊的地方是具体的文件系统可能通过其dentry_operations结构提供自己的节点名比对函数(比方说,有些文件系统可能在此比时跳过所有的空格),没有的话就用普通的memcmp。
回到cached_lookup函数中,具体的文件系统可能通过detnry_operations结构提供一个对找到的dentry结构进行验证的函数,如果验证失败就要通过d_invalidate将这个数据结构从杂凑表的队列中脱链,这种安排对有些文件系统是必要的,例如在网络文件系统NFS中,如果一个远程的进程是唯一的用户,又有很长时间没有访问这个结构了,那就应该将其视作无效,而根据磁盘上的父目录内容来重新构建。具体的函数由该文件系统的dentry_operations结构中通过函数指针d_revalidate提供,最后则根据验证的结果返回一个dentry指针或者出错码。不过,有的文件系统根本就不提供detnry_operations数据结构,所以其dentry结构中的 d_op是0,表示按linux默认的方式处理各项目录操作。事实上,ext2就不提供其自己的detnry_operations结构,也并不一定提供自己的d_revalidate操作。所以,代码中要先对这两个指针逐一检查。由于ext2并不提供其自己的detnry_operations结构,我们就把它跳过了。

至此,cached_lookup就结束了。

如果所需的dentry结构不在杂凑表队列中,或者已经无效,则返回NULL。那样,就要进一步通过real_lookup从父目录在磁盘上的内容找到本节点的目录项,再根据其内容在内存中为之建立一个dentry结构。下面就是real_lookup的代码:


/*
 * This is called when everything else fails, and we actually have
 * to go to the low-level filesystem to find out what we should do..
 *
 * We get the directory semaphore, and after getting that we also
 * make sure that nobody added the entry to the dcache in the meantime..
 * SMP-safe
 */
static struct dentry * real_lookup(struct dentry * parent, struct qstr * name, int flags)
{
	struct dentry * result;
	struct inode *dir = parent->d_inode;

	down(&dir->i_sem);
	/*
	 * First re-do the cached lookup just in case it was created
	 * while we waited for the directory semaphore..
	 *
	 * FIXME! This could use version numbering or similar to
	 * avoid unnecessary cache lookups.
	 */
	result = d_lookup(parent, name);
	if (!result) {
		struct dentry * dentry = d_alloc(parent, name);
		result = ERR_PTR(-ENOMEM);
		if (dentry) {
			lock_kernel();
			result = dir->i_op->lookup(dir, dentry);
			unlock_kernel();
			if (result)
				dput(dentry);
			else
				result = dentry;
		}
		up(&dir->i_sem);
		return result;
	}

	/*
	 * Uhhuh! Nasty case: the cache was re-populated while
	 * we waited on the semaphore. Need to revalidate.
	 */
	up(&dir->i_sem);
	if (result->d_op && result->d_op->d_revalidate) {
		if (!result->d_op->d_revalidate(result, flags) && !d_invalidate(result)) {
			dput(result);
			result = ERR_PTR(-ENOENT);
		}
	}
	return result;
}

建立dentry结构的过程不容许受到其他进程的干扰,所以必须通过信号量放在临界区中进行,但是,在通过down进入临界区时可能会经历一段睡眠等待时间,而其它进程可能已经在这段时间内把所需的dentry结构建立好,再建立一个就重复了。所以再进入临界区以后,还要再用d_lookup确认一下所需的dentry结构确实不在杂凑表中。这是一种规范性的处理方式。万一真的发生了这种情况,那就根据具体文件系统的要求而调用一个函数进行一些验证和处理。当然,发生这种情况的概率时很低的。在多数情况下是要建立dentry结构。

要建立一个dentry结构,首先当然要为之分配空间并初始化,上面的代码调用了d_alloc。

其代码如下:


/**
 * d_alloc	-	allocate a dcache entry
 * @parent: parent of entry to allocate
 * @name: qstr of the name
 *
 * Allocates a dentry. It returns %NULL if there is insufficient memory
 * available. On a success the dentry is returned. The name passed in is
 * copied and the copy passed in may be reused after this call.
 */
 
struct dentry * d_alloc(struct dentry * parent, const struct qstr *name)
{
	char * str;
	struct dentry *dentry;

	dentry = kmem_cache_alloc(dentry_cache, GFP_KERNEL); 
	if (!dentry)
		return NULL;

	if (name->len > DNAME_INLINE_LEN-1) {
		str = kmalloc(NAME_ALLOC_LEN(name->len), GFP_KERNEL);
		if (!str) {
			kmem_cache_free(dentry_cache, dentry); 
			return NULL;
		}
	} else
		str = dentry->d_iname; 

	memcpy(str, name->name, name->len);
	str[name->len] = 0;

	atomic_set(&dentry->d_count, 1);
	dentry->d_flags = 0;
	dentry->d_inode = NULL;
	dentry->d_parent = NULL;
	dentry->d_sb = NULL;
	dentry->d_name.name = str;
	dentry->d_name.len = name->len;
	dentry->d_name.hash = name->hash;
	dentry->d_op = NULL;
	dentry->d_fsdata = NULL;
	INIT_LIST_HEAD(&dentry->d_vfsmnt);
	INIT_LIST_HEAD(&dentry->d_hash);
	INIT_LIST_HEAD(&dentry->d_lru);
	INIT_LIST_HEAD(&dentry->d_subdirs);
	INIT_LIST_HEAD(&dentry->d_alias);
	if (parent) {
		dentry->d_parent = dget(parent);
		dentry->d_sb = parent->d_sb;
		spin_lock(&dcache_lock);
		list_add(&dentry->d_child, &parent->d_subdirs);
		spin_unlock(&dcache_lock);
	} else
		INIT_LIST_HEAD(&dentry->d_child);

	dentry_stat.nr_dentry++;
	return dentry;
}

从这段代码中我们可以看到,dentry数据结构是通过kmem_cache_alloc从为这种数据结构专设的slab队列中分配的。当节点名较短时,dentry结构中有个字符数组d_iname用来保存节点名,不然就要另行为之分配空间。不管怎样,dentry结构中的d_name.name总是指向这个字符串。此外,dentry结构中指向超级块结构的指针d_sb是从父节点继承下来的。每当建立一个dentry结构时,就要将其父节点('/'除外,它没有父节点)的共享计数通过dget递增,所以这个新建的dentry结构就成了其父节点的dentry结构的一个用户,并且要挂入其父节点的d_subdirs队列中,注意父节点的d_subdirs队列中只包含在内存中建有的dentry结构的目录项。

回到real_lookup代码中。分配了空间以后,就要从磁盘上由父节点代表的那个目录中寻找当前节点的目录项并设置结构中的其他信息。如果寻找失败,就通过dput撤销已经分配的空间的dentry结构。如果成功,就通过函数real_lookup放入295行的return语句返回指向该dentry结构的指针。

从磁盘上寻找的过程因文件系统而异,所以要通过父节点inode结构中的指针i_op找到对应的inode_operations数据结构。对于代表着目录的inode和代表着文件的inode,其inode_operations结构通常是不同的。就ext2而言,对于目录节点的函数跳转结构为ext2_dir_inode_operations,定义如下:

/*
 * directories can handle most operations...
 */
struct inode_operations ext2_dir_inode_operations = {
	create:		ext2_create,
	lookup:		ext2_lookup,
	link:		ext2_link,
	unlink:		ext2_unlink,
	symlink:	ext2_symlink,
	mkdir:		ext2_mkdir,
	rmdir:		ext2_rmdir,
	mknod:		ext2_mknod,
	rename:		ext2_rename,
};

可见,具体函数为ext2_lookup,其代码在同一个文件中:

path_walk=>real_lookup=>ext2_lookup


static struct dentry *ext2_lookup(struct inode * dir, struct dentry *dentry)
{
	struct inode * inode;
	struct ext2_dir_entry_2 * de;
	struct buffer_head * bh;

	if (dentry->d_name.len > EXT2_NAME_LEN)
		return ERR_PTR(-ENAMETOOLONG);

	bh = ext2_find_entry (dir, dentry->d_name.name, dentry->d_name.len, &de);
	inode = NULL;
	if (bh) {
		unsigned long ino = le32_to_cpu(de->inode);
		brelse (bh);
		inode = iget(dir->i_sb, ino);

		if (!inode)
			return ERR_PTR(-EACCES);
	}
	d_add(dentry, inode);
	return NULL;
}

 这里先由ext2_find_entry从磁盘上面找到并读入当前节点的目录项,然后通过iget根据索引节点号从磁盘读入相应索引节点并在内存中建立相对应的inode结构,最后,由d_add完成dentry结构的设置并将其挂入杂凑表中的某个队列。

path_walk=>real_lookup=>ext2_lookup=>ext2_find_entry:


/*
 *	ext2_find_entry()
 *
 * finds an entry in the specified directory with the wanted name. It
 * returns the cache buffer in which the entry was found, and the entry
 * itself (as a parameter - res_dir). It does NOT read the inode of the
 * entry - you'll have to do that yourself if you want to.
 */
static struct buffer_head * ext2_find_entry (struct inode * dir,
					     const char * const name, int namelen,
					     struct ext2_dir_entry_2 ** res_dir)
{
	struct super_block * sb;
	struct buffer_head * bh_use[NAMEI_RA_SIZE];
	struct buffer_head * bh_read[NAMEI_RA_SIZE];
	unsigned long offset;
	int block, toread, i, err;

	*res_dir = NULL;
	sb = dir->i_sb;

	if (namelen > EXT2_NAME_LEN)
		return NULL;

	memset (bh_use, 0, sizeof (bh_use));
	toread = 0;
	for (block = 0; block < NAMEI_RA_SIZE; ++block) {
		struct buffer_head * bh;

		if ((block << EXT2_BLOCK_SIZE_BITS (sb)) >= dir->i_size)
			break;
		bh = ext2_getblk (dir, block, 0, &err);
		bh_use[block] = bh;
		if (bh && !buffer_uptodate(bh))
			bh_read[toread++] = bh;
	}

	for (block = 0, offset = 0; offset < dir->i_size; block++) {
		struct buffer_head * bh;
		struct ext2_dir_entry_2 * de;
		char * dlimit;

		if ((block % NAMEI_RA_BLOCKS) == 0 && toread) {
			ll_rw_block (READ, toread, bh_read);
			toread = 0;
		}
		bh = bh_use[block % NAMEI_RA_SIZE];
		if (!bh) {
#if 0
			ext2_error (sb, "ext2_find_entry",
				    "directory #%lu contains a hole at offset %lu",
				    dir->i_ino, offset);
#endif
			offset += sb->s_blocksize;
			continue;
		}
		wait_on_buffer (bh);
		if (!buffer_uptodate(bh)) {
			/*
			 * read error: all bets are off
			 */
			break;
		}

		de = (struct ext2_dir_entry_2 *) bh->b_data;
		dlimit = bh->b_data + sb->s_blocksize;
		while ((char *) de < dlimit) {
			/* this code is executed quadratically often */
			/* do minimal checking `by hand' */
			int de_len;

			if ((char *) de + namelen <= dlimit &&
			    ext2_match (namelen, name, de)) {
				/* found a match -
				   just to be sure, do a full check */
				if (!ext2_check_dir_entry("ext2_find_entry",
							  dir, de, bh, offset))
					goto failure;
				for (i = 0; i < NAMEI_RA_SIZE; ++i) {
					if (bh_use[i] != bh)
						brelse (bh_use[i]);
				}
				*res_dir = de;
				return bh;
			}
			/* prevent looping on a bad block */
			de_len = le16_to_cpu(de->rec_len);
			if (de_len <= 0)
				goto failure;
			offset += de_len;
			de = (struct ext2_dir_entry_2 *)
				((char *) de + de_len);
		}

		brelse (bh);
		if (((block + NAMEI_RA_SIZE) << EXT2_BLOCK_SIZE_BITS (sb)) >=
		    dir->i_size)
			bh = NULL;
		else
			bh = ext2_getblk (dir, block + NAMEI_RA_SIZE, 0, &err);
		bh_use[block % NAMEI_RA_SIZE] = bh;
		if (bh && !buffer_uptodate(bh))
			bh_read[toread++] = bh;
	}

failure:
	for (i = 0; i < NAMEI_RA_SIZE; ++i)
		brelse (bh_use[i]);
	return NULL;
}

这段程序涉及文件的读操作,后面的博客会讲解文件的读写,这里只做必要的说明。目录其实只是一种特殊格式的文件,就ext2文件系统而言,目录文件的内容在概念上就是一个ext2_dir_entry_2结构数组,其目的在于根据节点名(最长可达255个字符)得到相应的索引节点号,所以从逻辑上讲其实是很简单的。为什么只是说概念上是ext2_dir_entry_2数组呢?因为实际上不是。前面讲过ext2_dir_entry_2结构的长度不是固定的(最大255,通常就几个字符,而一个文件系统中也许有数万个目录项),结构中有个字段rec_len指明本结构的长度。既然不是固定长度的,就不能像真正的数组那样通过下标来计算出具体元素的位置,而只好采用线性搜索的方式(时间换空间)。不过为了避免因目录项跨磁盘记录块而造成处理上的不便,ext2文件系统在为目录项分配磁盘空间时不让跨记录块。如果一个记录块中剩下的空间已经不足就另起一个记录块。

不同的处理器在存取数据时在字节的排列次序上有大小端之分。i386使用的是小端格式。索引节点号是32位的整数存储在磁盘上的,而同一磁盘既可以安装在采用大端格式的CPU的机器上,也可以安装到小端格式的CPU的机器上,所以要选择一种形式作为标准。事实上,ext2采用的标准就是小端格式,所以在使用存储在磁盘上大于8位的整数时先要通过le32_to_cpu、le16_to_cpu等函数将这些数据从小端格式转换成具体CPU使用的格式。因此i386实际没做任何转换。

由于磁盘的物理特性,从磁盘读一个记录块需要一定的时间,而这些时间主要消耗在准备工作上。一旦准备好了,读一个记录块和读多个记录块所需时间其实相差不大。所以比较好的办法是既然读了就往前预读几个块,因为紧挨着的这些记录块很可能马上就要用到。另一方面,从磁盘读记录块的操作一经启动便由磁盘自行完成,而无需CPU介入。所以,从读的第一批记录块到位以后,CPU对记录块的处理就可以跟后续记录块的读入相平行,从而形成流水操作。那么往前多少块比较合适呢?那要看具体情况了,对于从磁盘读入目录内容和这个特定的目的,代码中定义了几个常数:

/*
 * define how far ahead to read directories while searching them.
 */
#define NAMEI_RA_CHUNKS  2
#define NAMEI_RA_BLOCKS  4
#define NAMEI_RA_SIZE        (NAMEI_RA_CHUNKS * NAMEI_RA_BLOCKS)
#define NAMEI_RA_INDEX(c,b)  (((c) * NAMEI_RA_BLOCKS) + (b))

所以这里的预读的提前量是8个记录块,也就是说,估计在读入一个记录块所需的时间内CPU可以处理8个记录块。

对于从磁盘读入的记录块都要在前面加上一个头部,即buffer_head数据结构以便管理。由于从磁盘读入一个记录块的代价不小,对已经读入的记录块都不是用了即扔的,而是要在内存中加以缓冲存储。所以,有时候并不需要真的到磁盘上面去读。但是,这样一来有时候缓冲存储着的记录块与磁盘上的记录块就可能不一致了。

代码中为记录块设置了两个指针数组,一个是bh_use,另一个是bh_read,大小都是8。首先通过个for循环,调用ext2_getblk从缓冲着的记录块中找到给定目录文件的开头8个逻辑记录块,或者就为之分配缓冲区,并将它们的buffer_head结构指针写入数组bh_use,将bh_use填满。这就是要搜索的第一批次。当然,如果这个目录文件的大小还不够8个记录块那就另当别论(注意,参数dir指向其inode结构,而不是dentry结构)。在这8个记录块中,如果有的已经与磁盘上不一致(见85行),则要在另一个数组bh_read中记录下来,这就是真正要从磁盘上面读的。至于新分配的缓冲区,那当然与磁盘上不一致。

接着是对目录文件中的所有记录块的for循环,对目标节点的搜索就是扫描所有记录块中的所有目录项。循环从block 0开始,每隔8个就启动一次读磁盘操作(如果需要的话),每次最多读取8个块,而数组bh_read则给出所需记录块的名单。第一次把8个缓冲区填满以后,再往后的从磁盘读取与CPU的处理就可以形成一种流水线式的操作了。由于从磁盘读入是异步的,CPU在每处理一个记录块之前都要通过wait_on_buffer等待该记录块到位。但是只要预读的参数合适就可以达到基本上不需要什么等待的程度。

至于在记录块中搜索的过程,那就很简单了(见116-144行)。虽然ext2的目录项是可变大小的,但是却不会跨记录块存储,所以每个记录块的开始必然是一个目录项的开始(见116行),而一个记录块内有几个目录项那就不一定了。在找到了所需的目录项之后,要将其他的记录块缓冲区释放,只留下该目录项所在的那个记录块(见130-133行)。最后返回目录项所在的记录块,并通过参数res_dir返回目录项指针。

回到ext2_lookup代码中,下一步是根据查得的索引节点号通过iget找到或者建立起所需的inode结构,这里的iget是个inline函数,定义如下:

path_walk=>real_lookup=>ext2_lookup=>iget:

static inline struct inode *iget(struct super_block *sb, unsigned long ino)
{
	return iget4(sb, ino, NULL, NULL);
}

path_walk=>real_lookup=>ext2_lookup=>iget=>iget4:

struct inode *iget4(struct super_block *sb, unsigned long ino, find_inode_t find_actor, void *opaque)
{
	struct list_head * head = inode_hashtable + hash(sb,ino);
	struct inode * inode;

	spin_lock(&inode_lock);
	inode = find_inode(sb, ino, head, find_actor, opaque);
	if (inode) {
		__iget(inode);
		spin_unlock(&inode_lock);
		wait_on_inode(inode);
		return inode;
	}
	spin_unlock(&inode_lock);

	/*
	 * get_new_inode() will do the right thing, re-trying the search
	 * in case it had to block at any point.
	 */
	return get_new_inode(sb, ino, head, find_actor, opaque);
}

同样,目标节点的inode结构也可能已经在内存中,也可能需要从磁盘上读入其索引节点后在内存中建立。就像dentry结构有个杂凑表dentry_hashtable一样,inode结构也有个杂凑表inode_hashtable,已经建立的inode结构都通过结构中的i_hash挂载杂凑表的某个队列中,所以首先要通过find_inode在杂凑表中寻找。找到后就通过__iget递增其共享计数。由于索引节点号只在同一个设备上才是惟一的,查找杂凑计算的时候要把所在设备的super_block结构的地址也结合进去。

要是杂凑表的队列中找不到所需的inode结构,那就要通过get_new_inode从磁盘上读入相应的索引节点并建立起一个inode结构,其代码如下:

path_walk=>real_lookup=>ext2_lookup=>iget=>get_new_inode:

/*
 * This is called without the inode lock held.. Be careful.
 *
 * We no longer cache the sb_flags in i_flags - see fs.h
 *	-- [email protected]
 */
static struct inode * get_new_inode(struct super_block *sb, unsigned long ino, struct list_head *head, find_inode_t find_actor, void *opaque)
{
	struct inode * inode;

	inode = alloc_inode();
	if (inode) {
		struct inode * old;

		spin_lock(&inode_lock);
		/* We released the lock, so.. */
		old = find_inode(sb, ino, head, find_actor, opaque);
		if (!old) {
			inodes_stat.nr_inodes++;
			list_add(&inode->i_list, &inode_in_use);
			list_add(&inode->i_hash, head);
			inode->i_sb = sb;
			inode->i_dev = sb->s_dev;
			inode->i_ino = ino;
			inode->i_flags = 0;
			atomic_set(&inode->i_count, 1);
			inode->i_state = I_LOCK;
			spin_unlock(&inode_lock);

			clean_inode(inode);
			sb->s_op->read_inode(inode);

			/*
			 * This is special!  We do not need the spinlock
			 * when clearing I_LOCK, because we're guaranteed
			 * that nobody else tries to do anything about the
			 * state of the inode when it is locked, as we
			 * just created it (so there can be no old holders
			 * that haven't tested I_LOCK).
			 */
			inode->i_state &= ~I_LOCK;
			wake_up(&inode->i_wait);

			return inode;
		}

		/*
		 * Uhhuh, somebody else created the same inode under
		 * us. Use the old inode instead of the one we just
		 * allocated.
		 */
		__iget(old);
		spin_unlock(&inode_lock);
		destroy_inode(inode);
		inode = old;
		wait_on_inode(inode);
	}
	return inode;
}

这个函数的代码与前面的dentry结构的分配和建立很相似,至于从磁盘读入索引节点的过程则取决于具体的文件系统。有的文件系统在磁盘上可能并不存在索引节点这么一说,但是在概念上必有相通之处。所以,对有的文件系统来说是读入索引节点,而对有的文件系统则是将磁盘上的相关信息变换成一个索引节点。其实,所谓超级块、目录项也莫不如是。注意代码中对新创建inode结构中i_dev等字段的设置。前面讲过,inode结构中的i_dev表示这个结构所代表的的文件所在的设备,这里可以看到它的值来自所在设备的super_block数据结构,是在从设备上读入索引节点之前就设置好了的。还有,索引节点号ino仅在同一个设备上才是唯一的,所以要与设备号(或者super_block结构)合在一起才能在全系统范围中唯一的确定一个索引节点及其inode结构。这也是为什么find_inode的参数表中包含了sb和ino的原因。

对索引节点的读入,具体的函数是通过函数跳转表super_operations结构中的函数指针read_inode提供的。每个设备的super_block结构中都有一个指针s_op,指向具体的跳转表。对于ext2来说,这个跳转表就是ext2_sops,具体的函数则是ext2_read_inode:

path_walk=>real_lookup=>ext2_lookup=>iget=>get_new_inode=>ext2_read_inode


void ext2_read_inode (struct inode * inode)
{
	struct buffer_head * bh;
	struct ext2_inode * raw_inode;
	unsigned long block_group;
	unsigned long group_desc;
	unsigned long desc;
	unsigned long block;
	unsigned long offset;
	struct ext2_group_desc * gdp;

	if ((inode->i_ino != EXT2_ROOT_INO && inode->i_ino != EXT2_ACL_IDX_INO &&
	     inode->i_ino != EXT2_ACL_DATA_INO &&
	     inode->i_ino < EXT2_FIRST_INO(inode->i_sb)) ||
	    inode->i_ino > le32_to_cpu(inode->i_sb->u.ext2_sb.s_es->s_inodes_count)) {
		ext2_error (inode->i_sb, "ext2_read_inode",
			    "bad inode number: %lu", inode->i_ino);
		goto bad_inode;
	}
	block_group = (inode->i_ino - 1) / EXT2_INODES_PER_GROUP(inode->i_sb);
	if (block_group >= inode->i_sb->u.ext2_sb.s_groups_count) {
		ext2_error (inode->i_sb, "ext2_read_inode",
			    "group >= groups count");
		goto bad_inode;
	}
	group_desc = block_group >> EXT2_DESC_PER_BLOCK_BITS(inode->i_sb);
	desc = block_group & (EXT2_DESC_PER_BLOCK(inode->i_sb) - 1);
	bh = inode->i_sb->u.ext2_sb.s_group_desc[group_desc];
	if (!bh) {
		ext2_error (inode->i_sb, "ext2_read_inode",
			    "Descriptor not loaded");
		goto bad_inode;
	}

	gdp = (struct ext2_group_desc *) bh->b_data;
	/*
	 * Figure out the offset within the block group inode table
	 */
	offset = ((inode->i_ino - 1) % EXT2_INODES_PER_GROUP(inode->i_sb)) *
		EXT2_INODE_SIZE(inode->i_sb);
	block = le32_to_cpu(gdp[desc].bg_inode_table) +
		(offset >> EXT2_BLOCK_SIZE_BITS(inode->i_sb));
	if (!(bh = bread (inode->i_dev, block, inode->i_sb->s_blocksize))) {
		ext2_error (inode->i_sb, "ext2_read_inode",
			    "unable to read inode block - "
			    "inode=%lu, block=%lu", inode->i_ino, block);
		goto bad_inode;
	}
	offset &= (EXT2_BLOCK_SIZE(inode->i_sb) - 1);
	raw_inode = (struct ext2_inode *) (bh->b_data + offset);

	inode->i_mode = le16_to_cpu(raw_inode->i_mode);
	inode->i_uid = (uid_t)le16_to_cpu(raw_inode->i_uid_low);
	inode->i_gid = (gid_t)le16_to_cpu(raw_inode->i_gid_low);
	if(!(test_opt (inode->i_sb, NO_UID32))) {
		inode->i_uid |= le16_to_cpu(raw_inode->i_uid_high) << 16;
		inode->i_gid |= le16_to_cpu(raw_inode->i_gid_high) << 16;
	}
	inode->i_nlink = le16_to_cpu(raw_inode->i_links_count);
	inode->i_size = le32_to_cpu(raw_inode->i_size);
	inode->i_atime = le32_to_cpu(raw_inode->i_atime);
	inode->i_ctime = le32_to_cpu(raw_inode->i_ctime);
	inode->i_mtime = le32_to_cpu(raw_inode->i_mtime);
	inode->u.ext2_i.i_dtime = le32_to_cpu(raw_inode->i_dtime);
	/* We now have enough fields to check if the inode was active or not.
	 * This is needed because nfsd might try to access dead inodes
	 * the test is that same one that e2fsck uses
	 * NeilBrown 1999oct15
	 */
	if (inode->i_nlink == 0 && (inode->i_mode == 0 || inode->u.ext2_i.i_dtime)) {
		/* this inode is deleted */
		brelse (bh);
		goto bad_inode;
	}
	inode->i_blksize = PAGE_SIZE;	/* This is the optimal IO size (for stat), not the fs block size */
	inode->i_blocks = le32_to_cpu(raw_inode->i_blocks);
	inode->i_version = ++event;
	inode->u.ext2_i.i_flags = le32_to_cpu(raw_inode->i_flags);
	inode->u.ext2_i.i_faddr = le32_to_cpu(raw_inode->i_faddr);
	inode->u.ext2_i.i_frag_no = raw_inode->i_frag;
	inode->u.ext2_i.i_frag_size = raw_inode->i_fsize;
	inode->u.ext2_i.i_file_acl = le32_to_cpu(raw_inode->i_file_acl);
	if (S_ISDIR(inode->i_mode))
		inode->u.ext2_i.i_dir_acl = le32_to_cpu(raw_inode->i_dir_acl);
	else {
		inode->u.ext2_i.i_high_size = le32_to_cpu(raw_inode->i_size_high);
		inode->i_size |= ((__u64)le32_to_cpu(raw_inode->i_size_high)) << 32;
	}
	inode->i_generation = le32_to_cpu(raw_inode->i_generation);
	inode->u.ext2_i.i_block_group = block_group;

	/*
	 * NOTE! The in-memory inode i_data array is in little-endian order
	 * even on big-endian machines: we do NOT byteswap the block numbers!
	 */
	for (block = 0; block < EXT2_N_BLOCKS; block++)
		inode->u.ext2_i.i_data[block] = raw_inode->i_block[block];

	if (inode->i_ino == EXT2_ACL_IDX_INO ||
	    inode->i_ino == EXT2_ACL_DATA_INO)
		/* Nothing to do */ ;
	else if (S_ISREG(inode->i_mode)) {
		inode->i_op = &ext2_file_inode_operations;
		inode->i_fop = &ext2_file_operations;
		inode->i_mapping->a_ops = &ext2_aops;
	} else if (S_ISDIR(inode->i_mode)) {
		inode->i_op = &ext2_dir_inode_operations;
		inode->i_fop = &ext2_dir_operations;
	} else if (S_ISLNK(inode->i_mode)) {
		if (!inode->i_blocks)
			inode->i_op = &ext2_fast_symlink_inode_operations;
		else {
			inode->i_op = &page_symlink_inode_operations;
			inode->i_mapping->a_ops = &ext2_aops;
		}
	} else 
		init_special_inode(inode, inode->i_mode,
				   le32_to_cpu(raw_inode->i_block[0]));
	brelse (bh);
	inode->i_attr_flags = 0;
	if (inode->u.ext2_i.i_flags & EXT2_SYNC_FL) {
		inode->i_attr_flags |= ATTR_FLAG_SYNCRONOUS;
		inode->i_flags |= S_SYNC;
	}
	if (inode->u.ext2_i.i_flags & EXT2_APPEND_FL) {
		inode->i_attr_flags |= ATTR_FLAG_APPEND;
		inode->i_flags |= S_APPEND;
	}
	if (inode->u.ext2_i.i_flags & EXT2_IMMUTABLE_FL) {
		inode->i_attr_flags |= ATTR_FLAG_IMMUTABLE;
		inode->i_flags |= S_IMMUTABLE;
	}
	if (inode->u.ext2_i.i_flags & EXT2_NOATIME_FL) {
		inode->i_attr_flags |= ATTR_FLAG_NOATIME;
		inode->i_flags |= S_NOATIME;
	}
	return;
	
bad_inode:
	make_bad_inode(inode);
	return;
}

在ext2格式的磁盘上,有些索引节点是有特殊用途的,

/*
 * Special inodes numbers
 */
#define	EXT2_BAD_INO		 1	/* Bad blocks inode */
#define EXT2_ROOT_INO		 2	/* Root inode */
#define EXT2_ACL_IDX_INO	 3	/* ACL inode */
#define EXT2_ACL_DATA_INO	 4	/* ACL inode */
#define EXT2_BOOT_LOADER_INO	 5	/* Boot loader inode */
#define EXT2_UNDEL_DIR_INO	 6	/* Undelete directory inode */

这些索引节点是为系统保留的,对它们的访问都不通过目录项而直接通过定义的节点号进行。其中EXT2_ACL_IDX_INO和EXT2_ACL_DATA_INO用于访问控制,是为改善文件系统的安全性而设置的。磁盘设备的super_block结构中提供磁盘上第一个供常规用途的索引节点的节点号以及索引节点的总数,这两项参数被用于对节点号的范围检查。

从概念上说,ext2格式的磁盘设备上,除引导块和超级块之外,就分成索引节点和数据两部分,但是,出于访问效率的考虑,实际上把整个磁盘先分成若干记录块组,然后再将每个记录块组分成索引节点和数据两部分。与此相应,ext2磁盘的超级块中则提供有关这种划分的参数,如磁盘上有多少个组,每个组中有多少个记录块,有多少个索引节点等等,同时每个块组还有一个组描述符结构,也可以通过super_block结构访问。所以,先要根据索引节点号算出该节点所在的记录块组(见980行)以及在节点组内的位移(999行),然后再算出节点所在的记录块号(1001行)。知道了记录块号以后,就可以通过设备驱动程序bread读入该记录块。从磁盘读入的索引节点为ext2_inode数据结构,读者已经看过它的定义。索引节点中的信息是原始的,未经加工的,所以代码中称之为raw_inode数据结构,相比之下内存中inode结构中的信息则分成两部分,一部分是属于vfs层,适用于所有的文件系统,另一部分则属于具体的文件系统,这就是那个union,因具体文件系统的不同而赋予不同的解释。对ext2来说,这部分数据形成一个ext2_inode_info结构,定义如下:


/*
 * second extended file system inode data in memory
 */
struct ext2_inode_info {
	__u32	i_data[15];
	__u32	i_flags;
	__u32	i_faddr;
	__u8	i_frag_no;
	__u8	i_frag_size;
	__u16	i_osync;
	__u32	i_file_acl;
	__u32	i_dir_acl;
	__u32	i_dtime;
	__u32	not_used_1;	/* FIX: not used/ 2.2 placeholder */
	__u32	i_block_group;
	__u32	i_next_alloc_block;
	__u32	i_next_alloc_goal;
	__u32	i_prealloc_block;
	__u32	i_prealloc_count;
	__u32	i_high_size;
	int	i_new_inode:1;	/* Is a freshly allocated inode */
};

结构中的i_data是一块很重要的数据。对于有存储内容的文件(普通文件和目录文件),这里存放着一些指针,直接或间接地指向磁盘上存储着该文件内容的所有记录块。所谓索引节点即因此而得名。至于代表着符号链接的节点,则并没有文件内容,所以正好用这块空间来存储链接目标的路径名。这块空间的大小是15个32位整数,即60字节,虽然节点名最长可达255字节,但是一般不会很长,将作为符号链接目标的路径名限制在60个字节不至于引起问题。代码中通过一个for循环将这15个整数复制到inode结构的union中。

接着就是根据由索引节点所提供的信息设置inode结构中的inode_operations结构指针和file_operations结构指针,完成具体文件系统与虚拟文件系统vfs之间的连接。有的人曾把这两个比喻成接口卡与总线,但是这是从系统结构的角度而言的,实际上文件系统中的每一个节点都有从vfs层连接到具体文件系统的问题,就好像每个节点都有这么一条总线一样。

目前linux2.4版不支持acl,所以代码中只是为之留下了位置,而暂时不做任何处理。除此以外,就通过检查inode结构中的mode字段来确定该节点是否常规文件(S_ISREG)、目录(S_ISDIR)、符号链接(S_ISLNK)或特殊文件而作不同处理和设置。例如对ext2文件系统目录节点就将i_op和i_fop分别设置成指向ext2_dir_inode_operations、ext2_dir_operations。对于常规文件则除i_op、i_fop以外还有另一个指针a_ops,它指向一个address_space_operations数据结构,用于文件到内存空间的映射或缓冲。对特殊文件则通过init_special_inode加以检查和处理,以后我们会经常回过来看这个函数。

在找到了或者建立了所需的inode结构以后,就返回到ext2_lookup,在那里还要通过d_add将inode和dentry结构挂上钩,并将dentry结构挂入杂凑表中的某个队列。这里的d_add是个inline函数,定义如下:

path_walk=>real_lookup=>ext2_lookup=>d_add


/**
 * d_add - add dentry to hash queues
 * @entry: dentry to add
 * @inode: The inode to attach to this dentry
 *
 * This adds the entry to the hash queues and initializes @inode.
 * The entry was actually filled in earlier during d_alloc().
 */
 
static __inline__ void d_add(struct dentry * entry, struct inode * inode)
{
	d_instantiate(entry, inode);
	d_rehash(entry);
}

函数d_instantiate使dentry结构互相挂钩,其代码如下:

path_walk=>real_lookup=>ext2_lookup=>d_add=>d_instantiate:


/**
 * d_instantiate - fill in inode information for a dentry
 * @entry: dentry to complete
 * @inode: inode to attach to this dentry
 *
 * Fill in inode information in the entry.
 *
 * This turns negative dentries into productive full members
 * of society.
 *
 * NOTE! This assumes that the inode count has been incremented
 * (or otherwise set) by the caller to indicate that it is now
 * in use by the dcache.
 */
 
void d_instantiate(struct dentry *entry, struct inode * inode)
{
	spin_lock(&dcache_lock);
	if (inode)
		list_add(&entry->d_alias, &inode->i_dentry);
	entry->d_inode = inode;
	spin_unlock(&dcache_lock);
}

两个数据结构是相互的。一方面dentry结构中的指针d_inode指向inode结构,这是一对一的关系,因为一个目录项只能代表一个文件。可是,反过来就不一样了,同一个文件可以有多个不同的文件名或路径(通过系统调用link建立,可是注意与符号链接的区别,那是由symlink建立的),所以从inode结构到dentry结构的方向可以是一对多的关系。因此,inode结构中的i_dentry是个队列,dentry结构通过其队列头部d_alias挂入相应的inode结构的队列中。

至于d_rehash则将dentry结构挂入杂凑队列中。代码如下:

path_walk=>real_lookup=>ext2_lookup=>d_add=>d_rehash:


/**
 * d_rehash	- add an entry back to the hash
 * @entry: dentry to add to the hash
 *
 * Adds a dentry to the hash according to its name.
 */
 
void d_rehash(struct dentry * entry)
{
	struct list_head *list = d_hash(entry->d_parent, entry->d_name.hash);
	spin_lock(&dcache_lock);
	list_add(&entry->d_hash, list);
	spin_unlock(&dcache_lock);
}

回到real_lookup函数中,现在已经找到了或者建立所需的dentry结构,接着就返回到path_walk的代码中。

当前节点的dentry结构是有了,但是这个节点会不会是个安装点呢?所以在503行调用d_mountpoint加以检查。

path_walk=>d_mountpoint

static __inline__ int d_mountpoint(struct dentry *dentry)
{
	return !list_empty(&dentry->d_vfsmnt);
}

如果是安装点,就调用__follow_down前进到所安装设备的根节点。这两个函数的代码分析其他博客会讲。

最后,当前节点会不会只是代表着一个链接呢?对这种情况的检验取决于具体的文件系统。有些文件系统根本就不支持链接,那就已经最终找到了所需的节点,此时需要调用dput递减dentry结构中的共享计数,因为此后path_walk不再使用这个数据结构了。如果具体的文件系统支持链接,那就通过do_follow_link处理。

path_walk=>do_follow_link

static inline int do_follow_link(struct dentry *dentry, struct nameidata *nd)
{
	int err;
	if (current->link_count >= 8)
		goto loop;
	current->link_count++;
	UPDATE_ATIME(dentry->d_inode);
	err = dentry->d_inode->i_op->follow_link(dentry, nd);
	current->link_count--;
	return err;
loop:
	path_release(nd);
	return -ELOOP;
}

对链接的长度有个限制,否则就有可能陷入死循环,这个上限是8。具体对链接的跟随由相应inode_operations结构中的函数指针follow_link所提供的函数完成。就ext2文件系统而言,这个函数就是ext2_fast_symlink_inode_operations的ext2_follow_link:

path_walk=>do_follow_link=>ext2_follow_link

static int ext2_follow_link(struct dentry *dentry, struct nameidata *nd)
{
	char *s = (char *)dentry->d_inode->u.ext2_i.i_data;
	return vfs_follow_link(nd, s);
}

对于ext2文件系统,链接目标的路径名是在ext2_inode_info结构中的i_data中。代表着链接的节点并没有文件内容(数据),所以在索引节点中不需要存储相关各个存储区间的信息。而这些空间正好可以用来存储链接目标的路径名。这部分信息在前面的ext2_read_inode中作为ext2_inode_info结构的一部分被复制到inode结构里面的union u中。现在就以此为目标调用vfs_follow_link来达到目的。

函数vfs_follow_link的代码如下。

path_walk=>do_follow_link=>ext2_follow_link=>vfs_follow_link

int vfs_follow_link(struct nameidata *nd, const char *link)
{
	return __vfs_follow_link(nd, link);
}

值得注意的是,这里从ext2_follow_link调用vfs_follow_link意味着从较低层次回到了更高的vfs层。为什么呢?这是因为符号链接的目标有可能在另一个格式不同的文件系统中。可想而知,在vfs_follow_link中势必又要调用path_walk来找到代表着链接对象的dentry结构。事实也正是如此。
path_walk=>do_follow_link=>ext2_follow_link=>vfs_follow_link=>__vfs_follow_link


static 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 = path_walk(link, nd);
out:
	if (current->link_count || 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 (IS_ERR(name))
		goto fail_name;
	strcpy(name, nd->last.name);
	nd->last.name = name;
	return 0;
fail_name:
	link = name;
fail:
	path_release(nd);
	return PTR_ERR(link);
}

至此,对一个中间节点的搜索过程就完成了。回到原先path_walk的代码中来,那里的533行的continue语句使执行又回到439行的for循环开始处,继续处理路径名中的下一个节点。到最后一个节点时,就会转到标号为last_component或者last_with_slashes处。我们继续在path_walk的代码中往下看:

last_with_slashes:
		lookup_flags |= LOOKUP_FOLLOW | LOOKUP_DIRECTORY;
last_component:
		if (lookup_flags & LOOKUP_PARENT)
			goto lookup_parent;
		if (this.name[0] == '.') switch (this.len) {
			default:
				break;
			case 2:	
				if (this.name[1] != '.')
					break;
				follow_dotdot(nd);
				inode = nd->dentry->d_inode;
				/* fallthrough */
			case 1:
				goto return_base;
		}
		if (nd->dentry->d_op && nd->dentry->d_op->d_hash) {
			err = nd->dentry->d_op->d_hash(nd->dentry, &this);
			if (err < 0)
				break;
		}
		dentry = cached_lookup(nd->dentry, &this, 0);
		if (!dentry) {
			dentry = real_lookup(nd->dentry, &this, 0);
			err = PTR_ERR(dentry);
			if (IS_ERR(dentry))
				break;
		}
		while (d_mountpoint(dentry) && __follow_down(&nd->mnt, &dentry))
			;
		inode = dentry->d_inode;
		if ((lookup_flags & LOOKUP_FOLLOW)
		    && inode && inode->i_op && inode->i_op->follow_link) {
			err = do_follow_link(dentry, nd);
			dput(dentry);
			if (err)
				goto return_err;
			inode = nd->dentry->d_inode;
		} else {
			dput(nd->dentry);
			nd->dentry = dentry;
		}
		err = -ENOENT;
		if (!inode)
			goto no_inode;
		if (lookup_flags & LOOKUP_DIRECTORY) {
			err = -ENOTDIR; 
			if (!inode->i_op || !inode->i_op->lookup)
				break;
		}
		goto return_base;
no_inode:
		err = -ENOENT;
		if (lookup_flags & (LOOKUP_POSITIVE|LOOKUP_DIRECTORY))
			break;
		goto return_base;
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;
return_base:
		return 0;
out_dput:
		dput(dentry);
		break;
	}
	path_release(nd);
return_err:
	return err;
}

路径名的末尾有个"/"字符,意味着路径的终点是个目录,并且,如果这个节点代表一个链接就一定要前进到所链接的对象(也是个目录)。所以,在这种情况下把标志位LOOKUP_FOLLOW和LOOKUP_DIRECTORY都设置为1。

调用参数中的LOOKUP_PARENT标志位1 表示要寻找的并不是路径中的终点,而是它的上一层,所以转到593行的lookup_parent标号处,根据终点的节点名把nameidata结构中的last_type设置成LAST_NORM、LAST_DOT或者LAST_DOTDOT。但是nameidata结构中的指针dentry此时仍然指向上一层节点的dentry结构。

不过,一般情况下LOOKUP_PARENT标志位都是0,要找的是路径名中的终点。将代码中的541~581行与473~509行做一比较,就可以发现这两部分几乎是一样的,所不同的是:

  1. 对于中间节点调用cached_lookup和real_lookup时标志LOOKUP_CONTINUE为1,而对于终结节点调用这两个函数时这个标志为0。但是,这个标志位仅在所属文件系统提供目录项验证函数时才有用,ext2文件系统并不提供这个函数,并不对所找到的dentry结构加以验证,所以这个因素不起作用。
  2. 当中间节点代表着符号链接时,对do_follow_link的调用是无条件的(只要提供了相关函数)。相比之下,当终结节点代表着符号链接时,则仅当LOOKUP_FOLLOW标志位为1时才调用这个函数。
  3. 如果一个中间节点的dentry结构尚未与一个inode结构挂上钩,则搜索就无法进行下去了。所以path_walk立即返回出错,出错代码 -ENOENT。可是,对于终结节点就不同了,在有些情况系允许代表常规文件终结节点的dentry结构没有与之挂钩的inode结构。我们称这样的dentry结构为“negative”,反之则为“positive”。有一个标志位称为LOOKUP_POSITIVE,就是表示所欲寻找的节点必须有用inode结构。所以,对于终结节点,当不存在inode结构时就转向584行的标号no_inode,在那里根据LOOKUP_POSITIVE和LOOKUP_DIRECTORY这两个标志来决定是出错还是正常返回。
  4. 最后,如果节点是一个目录,那就要依靠文件系统通过其inode_operations结构中的指针lookup提供的函数读入一个目录,并在这个目录中搜索。要是这函数指针为NULL,name搜索也就不能继续了。对于中间节点这意味着搜索失败(531行),而对于终结节点则只有在所要求的是个目录LOOKUP_DIRECTORY标志位为1时才意味着失败(582~587行)。

从path_walk返回时,函数值为0表示搜索成功,此时nameidata结构中的指针dentry指向目标节点(不一定是终结节点)的dentry结构,指针mnt指向目标节点所在设备的安装结构。同时这个结构中的last_type表示这一个节点的类型,节点名则在qstr结构last中,如果失败的话,则函数值为已负的出错码,而nameidata结构中则提供失败的节点名等信息。

根据给定路径名找到目标节点的dentry结构(以及inode结构)的过程,涉及与文件系统有关的几乎所有数据结构以及这些数据结构间的联系,搞懂了这个过程对文件系统就有了基本的理解。同时,path_init和path_walk(以及将这两者包装在一起的user_walk)又是在各种文件系统有关的系统调用中最广泛使用的函数。我们举个例子看看:

sys_chdir:


asmlinkage long sys_chdir(const char * filename)
{
	int error;
	struct nameidata nd;
	char *name;

	name = getname(filename);
	error = PTR_ERR(name);
	if (IS_ERR(name))
		goto out;

	error = 0;
	if (path_init(name,LOOKUP_POSITIVE|LOOKUP_FOLLOW|LOOKUP_DIRECTORY,&nd))
		error = path_walk(name, &nd);
	putname(name);
	if (error)
		goto out;

	error = permission(nd.dentry->d_inode,MAY_EXEC);
	if (error)
		goto dput_and_out;

	set_fs_pwd(current->fs, nd.mnt, nd.dentry);

dput_and_out:
	path_release(&nd);
out:
	return error;
}

这是系统调用chdir的代码。这里的permission检查访问权限,函数set_fs_pwd将当前进程的fs_struct结构中的指针pwd和pwdmnt分别设置成有nameidata中提供的dentry指针和vfsmount指针。

sys_chdir=>set_fs_pwd

/*
 * Replace the fs->{pwdmnt,pwd} with {mnt,dentry}. Put the old values.
 * It can block. Requires the big lock held.
 */

static inline void set_fs_pwd(struct fs_struct *fs,
	struct vfsmount *mnt,
	struct dentry *dentry)
{
	struct dentry *old_pwd;
	struct vfsmount *old_pwdmnt;
	write_lock(&fs->lock);
	old_pwd = fs->pwd;
	old_pwdmnt = fs->pwdmnt;
	fs->pwdmnt = mntget(mnt);
	fs->pwd = dget(dentry);
	write_unlock(&fs->lock);
	if (old_pwd) {
		dput(old_pwd);
		mntput(old_pwdmnt);
	}
}

从路径名到目录节点的逻辑基本上就是这些了。

猜你喜欢

转载自blog.csdn.net/guoguangwu/article/details/119961172