Linux Open系统调用 篇二

3,982 阅读12分钟

Linux Open系统调用篇一

Linux Open系统调用篇二

Linux Open系统调用篇三

Linux Open系统调用篇四

上篇中我们已经获取到路径名 filename(用户空间传递过来的),fd 文件描述符。 如果获取的 fd 有效,那么接着调用 do_filp_open() 函数。

struct file * do_filp_open(int dfd, struct filename * pathname,
	const struct open_flags * op) {
	struct nameidata nd;
	int flags = op->lookup_flags;
	struct file * filp;
	set_nameidata(&nd, dfd, pathname);
	filp = path_openat(&nd, op, flags | LOOKUP_RCU);

	if (unlikely(filp == ERR_PTR(-ECHILD)))
		filp = path_openat(&nd, op, flags);

	if (unlikely(filp == ERR_PTR(-ESTALE)))
		filp = path_openat(&nd, op, flags | LOOKUP_REVAL);

	restore_nameidata();
	return filp;
}

参数 dfd 是相对路径的基准目录对应的文件描述符,参数 name 指向文件路径,参数 op 是查找标志。

在路径查找中有个很重要的数据结构 nameidata 用来向解析函数传递参数,保存解析结果。

struct nameidata {
	struct path path;
	struct qstr last;
	struct path root;
	struct inode * inode; //path.dentry.d_inode
	unsigned int flags;
	unsigned seq, m_seq;
	int last_type;
	unsigned depth;
	int total_link_count;


	struct saved {
		struct path link;
		void * cookie;
		const char * name;
		struct inode * inode;
		unsigned seq;
	} * stack, internal[EMBEDDED_LEVELS];


	struct filename * name;
	struct nameidata * saved;
	unsigned root_seq;
	int dfd;
};

成员 last 存放需要解析的文件路径的分量(以前提到的组件),是一个快速字符串(quick string), 不仅包字符串,还包含长度和散列值。

成员 path 存放解析得到的挂载描述符和目录项,成员 iode 存放目录项对应的索引节点。 path 保存已经成功解析到的信息,last 用来存放当前需要解析的信息,如果 last 解析成功 那么就会更新 path

如果文件路径的分量是一个符号链接,那么接下来需要解析符号链接的目标。成员 stack 是一个栈,用来保存文件路径没有解析的部分。成员 depth 是的深度。假设目录b 是一个符号链接,目标是 “e/f”,解析文件路径 “a/b/c/d.txt”,解析到 b,发现 b 是符号链 接下来要解析符号链接 b 的目标 “e/f”,需要把文件路径中没有解析的部分 “c/d.txt” 保存到栈中 ,等解析完符号链接后继续解析。

函数 do_flp_open 三次调用函数 path_openat以解析文件路径。

  • 第一次解析传入标志 LOOKUP_RCU,使用 RCU 查找(rcu-walk)方式。在散列 表中根据{父目录, 名称}查找目录的过程中,使用 RCU 保护散列桶的链表,使用 序列号保护目录,其他处理器可以并行地修改目录, RCU 查找方式速度最快。

  • 如果在第一次解析的过程中发现其他处理器修改了正在查找的目录,返回错误号 -ECHILD,那么第二次使用引用查找(ref-walk)REF 方式,在散列表中根据{父目录, 名称} 查找目录的过程中,使用 RCU 保护散列桶的链表,使用自旋锁保护目录,并且把目录的引用计数加1。 引用查找方式速度比较慢。

  • 网络文件系统的文件在网络的服务器上,本地上次查询得到的信息可能过期,和服务器的当前状态 不一致。如果第二次解析发现信息过期,返回错误号 -ESTALE,那么第三次解析传入标志 LOOKUP_REVAL, 表示需要重新确认信息是否有效。

static void set_nameidata(struct nameidata * p, int dfd, struct filename * name)
{
	struct nameidata * old = current->nameidata;
	p->stack = p->internal;
	p->dfd = dfd;
	p->name = name;
	p->total_link_count = old ? old->total_link_count: 0;
	p->saved = old;
	current->nameidata = p;
}

调用 set_nameidata() 保护当前进程现场信息。 接着调用 filp = path_openat(&nd, op, flags | LOOKUP_RCU);

static struct file * path_openat(struct nameidata * nd,
	const struct open_flags * op, unsigned flags) {
	const char * s;
	struct file * file;
	int opened = 0;
	int error;
  	// 获取一个空的 file 描述符。
	file = get_empty_filp();

  	//获取失败,返回。
	if (IS_ERR(file))
		return file;
  	//设置 file 描述符的查找标志。
	file->f_flags = op->open_flag;

  	// 如果是本次目标是创建一个临时文件,这里就不深入了,只研究正常的文件打开操作。
	if (unlikely(file->f_flags & __ O_TMPFILE)) {
		error = do_tmpfile(nd, flags, op, file, &opened);
		goto out2;
	}
  	// 路径初始化,确定查找的起始目录,初始化结构体 nameidata 的成员 path。
	s = path_init(nd, flags);

	//如果获取的路径(待查找的路径)无效,则释放 file 描述符,返回错误。
	if (IS_ERR(s)) {
		put_filp(file);
		return ERR_CAST(s);
	}
  	// 调用函数 link_path_walk 解析文件路径的每个分量,最后一个分量除外。
  	// 调用函数 do_last,解析文件路径的最后一个分量,并且打开文件。
	while (! (error = link_path_walk(s, nd)) && (error = do_last(nd, file, op, &opened)) > 0) {
		nd->flags &= ~(LOOKUP_OPEN | LOOKUP_CREATE | LOOKUP_EXCL);
    	// 如果最后一个分量是符号链接,调用 trailing_symlink 函数进行处理
    	// 读取符号链接文件的数据,新的文件路径是符号链接链接文件的数据,然后继续 while
    	// 循环,解析新的文件路径。
		s = trailing_symlink(nd);

		if (IS_ERR(s)) {
			error = PTR_ERR(s);
			break;
		}
	}
  	// 结束查找,释放解析文件路径的过程中保存的目录项和挂载描述符。
	terminate_walk(nd);

out2:

	if (! (opened & FILE_OPENED)) {
		BUG_ON(!error);
		put_filp(file);
	}

	if (unlikely(error)) {
		if (error == -EOPENSTALE) {
			if (flags & LOOKUP_RCU)
				error = -ECHILD;
			else
				error = -ESTALE;
		}

		file = ERR_PTR(error);
	}

	return file;
}

从路径初始化函数 path_init 开始分析。

static const char * path_init(struct nameidata * nd, unsigned flags)
{
	int retval = 0;
  	//获取路径名(用户空间传递过来的)
	const char * s = nd->name->name;
  	// 如果路径名为空,清除 LOOKUP_RCU 标志。
	if (! *s)
		flags &= ~LOOKUP_RCU;

	nd->last_type = LAST_ROOT;						// if there are only slashes...
	nd->flags = flags | LOOKUP_JUMPED | LOOKUP_PARENT;
	nd->depth = 0;

	// 如果设置 `LOOKUP_ROOT`,表示 nameidata 中的 root 字段是由调用者提供的,因此即使在
  	// 不需要它的时候也不应该释放它。暂时跳过该分支。
	if (flags & LOOKUP_ROOT) {
		struct dentry * root = nd->root.dentry;
		struct vfsmount * mnt = nd->root.mnt;
		struct inode * inode = root->d_inode;

		if (*s) {
			if (!d_can_lookup(root))
				return ERR_PTR(-ENOTDIR);

			retval = inode_permission2(mnt, inode, MAY_EXEC);

			if (retval)
				return ERR_PTR(retval);
		}

		nd->path = nd->root;
		nd->inode = inode;
    	// 如果是 RCU 模式,则保存序列锁(处理竞争)。
		if (flags & LOOKUP_RCU) {
			rcu_read_lock();
			nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
			nd->root_seq = nd->seq;
			nd->m_seq = read_seqbegin(&mount_lock);
		}
		else {
      		//如果是 REF 模式,则获取 path 的计数引用(处理竞争)。
			path_get(&nd->path);
		}

		return s;
	}

	nd->root.mnt = NULL;

	// mount_lock 是一个全局 seqlock,有点像 rename_lock。它可以用来检查任何
  	// 挂载点的任何修改。
	nd->m_seq = read_seqbegin(&mount_lock);

  	// 如果是以 / 开头,也就是说明是绝对路径。
	if (*s == '/') {
    		// RCU 模式。
		if (flags & LOOKUP_RCU) {
			//开始 RCU 的读临界区。
			rcu_read_lock();

			//把 nd->root 设置为当前进程的根目录。
			set_root_rcu(nd);
			nd->seq = nd->root_seq;
		}
		else {
			//以自旋锁的方式(阻塞方式)设置 nd->root 为 fs->root。
			set_root(nd);
			path_get(&nd->root);
		}

		//保存已解析到的 挂载描述符和目录项。
		nd->path = nd->root;
	}
	else if (nd->dfd == AT_FDCWD) {
		//如果为相对路径,没有指定了相对路径的基准目录。
		if (flags & LOOKUP_RCU) {
			struct fs_struct * fs = current->fs;
			unsigned seq;

			rcu_read_lock();

			do {
				seq = read_seqcount_begin(&fs->seq);
				//获取当前进程的工作目录。
				nd->path = fs->pwd;
				// 初始化 nd->seq 的值为 d_seq,用于后面的同步。
				nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
			}while(read_seqcount_retry(&fs->seq, seq));// 检查期间 fs->pwd 有没有发生改变,如果有返回 1,然后重试。
		}
		else {
			// 如果为 REF 模式,则以自旋锁的方式设置 nd->path,并且增加 current->fs 的引用计数。
			get_fs_pwd(current->fs, &nd->path);
		}
	}
	else {
		//如果为相对路径,并且已经指定了相对路径的基准目录。
		// Caller must check execute permissions on the starting path component
		struct fd f = fdget_raw(nd->dfd);
		struct dentry * dentry;

		if (!f.file)
			return ERR_PTR(-EBADF);

		dentry = f.file->f_path.dentry;

		if (*s) {
				// 检查是否为目录。
			if (!d_can_lookup(dentry)) {
				fdput(f);
				return ERR_PTR(-ENOTDIR);
			}
		}

		nd->path = f.file->f_path;

		if (flags & LOOKUP_RCU) {
			rcu_read_lock();
			nd->inode = nd->path.dentry->d_inode;
			nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
		}
		else {
			// 增加引用计数。
			path_get(&nd->path);
			nd->inode = nd->path.dentry->d_inode;
		}

		fdput(f);
		return s;
	}

	nd->inode = nd->path.dentry->d_inode;

	if (! (flags & LOOKUP_RCU))
		return s;
	// 检查期间 nd->path.dentry 有没有发生改变。
	if (likely(!read_seqcount_retry(&nd->path.dentry->d_seq, nd->seq)))
		return s;

	if (! (nd->flags & LOOKUP_ROOT))
		nd->root.mnt = NULL;

	rcu_read_unlock();
	return ERR_PTR(-ECHILD);
}

这里,我们发现 “如果为相对路径,并且已经指定了相对路径的基准目录。” 这种情况,并没有退出 RCU 的读临界区(调用 rcu_read_unlock();)。

接下来就是 link_path_walk 的调用,这个函数也是路径查找的核心函数。


/*
 * Name resolution.
 * This is the basic name resolution function, turning a pathname into
 * the final dentry. We expect 'base' to be positive and a directory.
 *
 * Returns 0 and nd will have valid dentry and mnt on success.
 * Returns error and drops reference to input namei data on failure.
 */
 static int link_path_walk(const char * name, struct nameidata * nd)
 {
 	int err;

 	while (*name == '/')
 		name++;

 	if (! *name)
 		return 0;

 	// At this point we know we have a real path component.
 	for (; ; ) {
 		u64 hash_len;
 		int type;
                 //假设当前目录分量的父目录为 t,这里检查目录 t 的访问权限。
 		err = may_lookup(nd);

 		if (err)
 			return err;
                //根据目录 t 和当前分量,计算当前分量的散列值和长度
 		hash_len = hash_name(name);

 		type = LAST_NORM;
                //如果当前分量的第一个字符为
                // “.”,那么当前分量有可能是:.(当前目录)、..(上一层目录)
 		if (name[0] == '.')
      		        //获取当前分量的长度
 			switch (hashlen_len(hash_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;
			// 判断父目录是否需要做 hash 操作,这里我的猜测是某些文件系统有加密的
			// 行为,需要使用自己的 d_hash 操作才能获取正确的名字与长度这些信息。
 			if (unlikely(parent->d_flags & DCACHE_OP_HASH)) {
 				struct qstr this = {
 					{
 						.hash_len = hash_len
 					},
 					.name = name
 				};


 				err = parent->d_op->d_hash(parent, &this);

 				if (err < 0)
 					return err;

 				hash_len = this.hash_len;
 				name = this.name;
 			}
 		}
      	        //设置当前要解析分量的长度
 		nd->last.hash_len = hash_len;
      	        //设置当前当前要解析分量的路径名
 		nd->last.name = name;
                //设置当前要解析分量的类型
 		nd->last_type = type;

                //name 指针加上当前分量的长度
 		name += hashlen_len(hash_len);

      	        //如果此时 * name 为 '\0' 表示 last 指向的分量是最后一个分量,没有下一个分量可解析了。
 		if (! *name)
 			goto OK;


 		// If it wasn't NUL, we know it was '/'. Skip that
 		// slash, and continue until no more slashes.
 		// 否则跳过 “/”,name 指向下一个分量的首字符。

 		do {
 			name++;
 		}
 		while(unlikely(*name == '/'));

    	        // 如果解析的路径类似: “a/b/c/” 这种以 “/” 结尾的路径。
		// 那么跳过 “/” 后,name 指向 “\0” ,所以说处理方式跟 “a/b/c” 是一样的。
		// 这个分支处理的是:name 没有指向下一个分量。
 		if (unlikely(! *name)) {

 OK:
    		        // 这里如果为 0,表示当前解析的是路径的最后一个分量(已经处理完所有的符号链接了)。
    		        // 那么处理完成,最后一个分量交给函数 do_last 来处理。
    		        // 这边我们只是给 nd->last 赋值了。  
 			if (!nd->depth)
 				return 0;

			// 流程到这里,我们知道目前处理的是某个符号链接所指向路径的最后一个分量了。

                        // 否则取出上一层未解析的剩余路径赋值给 name
                        // 比如 “a/b/c/d.txt”,如果 “c” 是符号链接并且指向 “e/f”。
                        // 此时我们处于解析符号链接 “c” 的过程并且当前分量 nd->last 指向 “f” 分量。
                        // 那么把 name 指向下一个分量 “d.txt” 。             
 			name = nd->stack[nd->depth - 1].name;

 			// trailing symlink, done
                        // 对于上一个例子,如果符号链接 “c” 后面没有 “d.txt”,那么 “f”
                        // 就为最后一个分量了,于是在这里返回,然后交给 do_last 函数处理分量 “f”。
 			if (!name)
 				return 0;

 		 	// last component of nested symlink
                        // 调用函数 walk_component 解析分量 “f”。  
 			err = walk_component(nd, WALK_GET | WALK_PUT);
 		}
 		else {
			// 这个分支处理的是:name 有指向下一个分量。
        	        // 解析路径(包括符号链接指向的路径)的某一个中间节点,比如:“c”。
 			err = walk_component(nd, WALK_GET);
 		}

 		if (err < 0)
 			return err;

		// 这里 err 的值为 1,后面会分析到。
 		if (err) {
                        //如果当前的分量是符号链接,那么获取符号链接指向的路径。
                        //对于上面的例子,有两种情况:      
                        //(1)当前分量是 “c”,我们知道它是个符号链接,取出指向的路径 “e/f。
                        //(2)当前分量是 “f” 也是一个符号链接。那么取出“e” 指向的路径(假设为 “g/h”) ,嵌套符号链接的情况。
 			const char * s = get_link(nd);

 			if (IS_ERR(s))
 				return PTR_ERR(s);

 			err = 0;
                        // 符号链接 “c” 或者 “e” 指向 “\0”,那么跳过不处理,一般来说不会出现这种情况。
 			if (unlikely(!s)) {
 				// --nd->depth
 				put_link(nd);
 			}
 			else {
          		        // 对于情况 (1),我们保存 “d.txt”,情况 (2):由于把
				// nd->stack[nd->depth - 1].name 取出来赋值给了name ,所以还是保存 “d.txt”。
 				nd->stack[nd->depth - 1].name = name;
 				name = s;
 				continue;
 			}
 		}
                //如果当前分量不是目录,那么停止解析,返回错误。
 		if (unlikely(!d_can_lookup(nd->path.dentry))) {
 			if (nd->flags & LOOKUP_RCU) {
				// 切换到 REF 模式进行查找。
 				if (unlazy_walk(nd, NULL, 0))
 					return - ECHILD;
 			}

 			return -ENOTDIR;
 		}
		// 如果 walk_component 没有返回错误,比如处理完目录“f”(情况 1),或者“h”(情况 2),
		// 此时 name 指向 “d.txt”,所以接下来解析 “d.txt”。最后 !nd->depth 条件成立,返回
		// 0, 交给 do_last 函数处理。
 	}
 }

现在,我们可以肯定当前的子路径一定是一个中间节点(文件夹或符号链接),既然是中间节点, 那么就需要“走过”这个节点。咱们来看看 walk_component 是怎么“走过”中间节点的。

static int walk_component(struct nameidata * nd, int flags)
{
	struct path path;
	struct inode * inode;
	unsigned seq;
	int err;


	// "." and ".." are special - ".." especially so because it has
	// to be able to know about the current root directory and
	// parent relationships.
	// 如果不是 LAST_NORM 类型,就交由 handle_dots 处理。
	if (unlikely(nd->last_type != LAST_NORM)) {
		err = handle_dots(nd, nd->last_type);

		if (flags & WALK_PUT)
			put_link(nd);

		return err;
	}
	// 快速查找
	err = lookup_fast(nd, &path, &inode, &seq);

	if (unlikely(err)) {
		if (err < 0)
			return err;
		// 如果快速查找模式失败,则进行慢速查找模式。
		err = lookup_slow(nd, &path);

		if (err < 0)
			return err;
		// 此时我们已经退出 RCU 模式了。
		seq = 0;									// we are already out of RCU mode
		err = -ENOENT;

		// 判断是否是一个 negative 目标,比如当文件被删除后就会成为 negative。
		if (d_is_negative(path.dentry))
			goto out_path_put;

		inode = d_backing_inode(path.dentry);
	}

	// 这种情况处于解析符号链接指向目标的最后一个分量,所以处理完后释放符号链接。
	if (flags & WALK_PUT)
		put_link(nd);

	// 如果查找到的分量又是一个符号链接,是否跟下去。这里 flags & WALK_GET 为 true。
	err = should_follow_link(nd, &path, flags & WALK_GET, inode, seq);

	if (unlikely(err))
		return err;

	// 把 path 更新到 nd->path。
	path_to_nameidata(&path, nd);
	nd->inode = inode;
	nd->seq = seq;
	return 0;


out_path_put:
	path_to_nameidata(&path, nd);
	return err;
}

walk_component 中有几个比较重要的函数,一个是 RCU 模式下的快速查找函数 lookup_fast, 一个是 REF 模式下的 lookup_slow 函数,以及处理目录“..”或者“.”函数 handle_dots。 以及符号链接处理函数 should_follow_link

static inline void path_to_nameidata(const struct path * path,
	struct nameidata * nd)
{
	// 如果不是出于 RCU 模式,那么就减少 nd->path 指向旧的 dentry 和 mnt 的引用计数。
	if (! (nd->flags & LOOKUP_RCU)) {
		dput(nd->path.dentry);

		if (nd->path.mnt != path->mnt)
			mntput(nd->path.mnt);
	}
	// 更新 nd->path。
	nd->path.mnt = path->mnt;
	nd->path.dentry = path->dentry;
}

下一篇介绍 Linux 的虚拟文件系统。