Linux open系统调用(二)

注:本文分析基于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()检查父目录下挂载的文件系统是否为最新的文件系统,如果是则检查结束;否则,将继续检查;

走完上述流程,我们也就处理完“.”和“…”的情况,接下来我们来看下如果是普通目录的情况,这就是下次要说的了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值