上一篇:https://blog.csdn.net/weixin_42523774/article/details/103341058

· 本文是Linux文件系列的第三篇,上文《系统调用如何进入内核层次,深入glibc寻找open函数真实实现。》,讲到了从应用程序到如何通过glibc中进入内核的过程,本文接着上文,讲系统调用在内核中如何实现,还是以open函数为例,后续会介绍read和write函数的内核实现。
· 本文采用linux3.2.1内核代码[下载处],并推荐使用Source Insight,这会让这个过程更容易。
· 我想先提醒一下,这些系统调用的操作者是某一个进程,open操作其实是在read和write操作之前的一个预备动作,如果是真实文件,这个操作就是将文件从硬盘(或flash)中读取到内存中,当然这个过程中要防止多个进程的同时操作,因此会有很多锁作为保护,这也是代码理解的难点。
· 首先将搜寻结构图展示出来,让大家先知其全貌,后续在一步一步细讲:(前面直接套用的函数就没写注释)

compat_sys_openat
|-->do_sys_open
	|-->do_filp_open
		|-->do_filp_open
			|-->path_openat
				|-->path_init  #  nd初始化。
				|-->link_path_walk  #  真实寻找。
				|	|-->for(;;) {
				|	|	|-->may_lookup  #  查询文件权限是否允许访。
				|	|	|-->hash = init_name_hash();  #  算出该文件名的哈希值,和文件名长度。
				|	|	|-->##  判断文件名是否使用了"."或者"..",来标明文件类型type  ##
				|	|	|-->d_hash # 查询是否有哈希表存在。
				|	|	|-->walk_component  #  依据刚刚识别的类型,做单次搜索。
				|	|	|	|-->handle_dots  #  "."".." 文件名处理。
				|	|	|	|-->do_lookup   # 其他文件的搜索。
				|	|	|	|	|-->__d_lookup_rcu   # 不带rcu搜寻。
				|	|	|	|	|-->__d_lookup      # 带rcu,可能引起阻塞搜寻。
				|	|	|	|	|-->d_alloc_and_lookup  # 上两步搜不到,就要通过硬盘文件系统搜寻。
				|	|	|	|-->should_follow_link #  查看是否可以继续链接文件,前面提到过,对链接次数有限制。
				|	|	|-->nested_symlink  # 限制递归调用不能超过8次,符号链接不能超过40次。
				|	|	|-->can_lookup  # 判断是否可以继续查找,可以则继续。
				|	|	|-->terminate_walk(nd);  #  查找完成操作,包括解RCU锁。
				|	|-->}
				|-->do_last #  查找完成,做打开文件操作

1.续接前文

· 书接上文,我们找到了内核代码位置include\asm-generic\unistd.h的如下语句:

#define __NR_openat 56
__SC_COMP(__NR_openat, sys_openat, compat_sys_openat)

· 根据这个分别找到了如下的宏定义:

#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _comp)
#define __SYSCALL(nr, call) [nr] = (call),

· 这里我们就明白了,我们这个宏定义,将 compat_sys_openat函数的地址赋给了一个变量,然后让swi指令去调用这个函数,我们在fs/compat.c找到了这个函数:

asmlinkage long
compat_sys_openat(unsigned int dfd, const char __user *filename, int flags, int mode)
{
	return do_sys_open(dfd, filename, flags, mode);
}

· asmlinkage 表示这个是通过汇编指令链接过来的,do_sys_open这就是open函数的真实实现,入参分别 AT_FDCWD, file, oflag, mode,下面我们看看它做了什么。

2.do_sys_open

· 本文将整个代码结构都展示出来,大部分内容通过代码中加注释的方式来解释,关键部分在代码后用文字说明。

long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
{
	struct open_flags op;  //  创建一个标志集合
	/*  从flags和mode中分离出lookup event bitmap,返回值lookup表示正在做的事件(寻找目录和结构),op中存有找到之后期望做的事件(执行或创建)*/
	int lookup = build_open_flags(flags, mode, &op);
	char *tmp = getname(filename); //  将文件名从用户空间复制到内核空间
	int fd = PTR_ERR(tmp);

	if (!IS_ERR(tmp)) {
		fd = get_unused_fd_flags(flags); //  根据flags获取一个对应类型的未使用的文件号
		if (fd >= 0) {
			struct file *f = do_filp_open(dfd, tmp, &op, lookup);  //  执行文件打开过程
			if (IS_ERR(f)) {  // 判断是否出错
				put_unused_fd(fd);  // 释放刚用get_unused_fd_flags申请的文件号
				fd = PTR_ERR(f);    // 将指针转化为错误码返回
			} else {
				fsnotify_open(f);   // 通知其他相关项,该文件已经打开
				fd_install(fd, f);  // 安装文件指针到fd数组
			}
		}
		putname(tmp); //  释放内核态缓存
	}
	return fd;
}

· do_sys_open函数首先做了做了设置open_flags标志和转换用户空间的文件名到内核空间,核心功能是调用do_filp_open,我们进一步寻找:

3.do_filp_open

struct file *do_filp_open(int dfd, const char *pathname,
		const struct open_flags *op, int flags)
{
	struct nameidata nd;
	struct file *filp;
	/* 核心就是path_openat,就是沿着打开文件名的整个路径,一层层解析,最后得到文件对象 */
	filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);
	if (unlikely(filp == ERR_PTR(-ECHILD)))
		filp = path_openat(dfd, pathname, &nd, op, flags);
	if (unlikely(filp == ERR_PTR(-ESTALE)))
		filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_REVAL);
	return filp;
}

· 这里的核心就是path_openat,就是沿着打开文件名的整个路径,一层层解析,最后得到文件对象。但是为什么最多会调用3次?
· 由操作系统发展趋势一文中讲过,我们讲过当前的操作系统的内存管理系统中,CPU只能间接通过将硬盘数据缓存到内存中才能使用。而用来缓存文件的区域叫dentry cache ,这是一个哈希表。
· 第一次搜索采用rcu-walk方式搜索dentry cache,这种方式不会阻塞等待(RCU才会阻塞),更高效,但是可能会失败(因为文件inode本身还有顺序锁和自旋锁的保护);
· 第二次搜索则是采用ref-walk方式搜索dentry cache,考虑rcu锁的情形,但是可能会阻塞。
· 如果这样仍然失败,说明内存中没有该文件,这样就需要第三次,直接通过硬盘文件系统,进入硬盘慢速搜索,要带LOOKUP_REVAL标志。

4.path_openat

static struct file *path_openat(int dfd, const char *pathname,
		struct nameidata *nd, const struct open_flags *op, int flags)
{
	struct file *base = NULL;
	struct file *filp;
	struct path path;
	int error;

	filp = get_empty_filp(); // 找到一个未使用的文件结构返回
	if (!filp)
		return ERR_PTR(-ENFILE);

    /*  初始化nd中的 intent结构的标志 */
	filp->f_flags = op->open_flag;
	nd->intent.open.file = filp;
	nd->intent.open.flags = open_to_namei_flags(op->open_flag);
	nd->intent.open.create_mode = op->mode;

	/*  初始化nd中的其他参数,通过开头是不是"/"和dfd是否是AT_FDCWD,识别是绝对路径还是相对路径,设置RCU */
	error = path_init(dfd, pathname, flags | LOOKUP_PARENT, nd, &base);
	if (unlikely(error))
		goto out_filp;
	current->total_link_count = 0;  //  重置链接的次数,后续做判断
	/*  命名解析函数,针对路径循环搜索,直到找到最后一个不是目录的文件,次步骤需要细讲---> */
	error = link_path_walk(pathname, nd);
	if (unlikely(error))
		goto out_filp;
	/*  处理打开文件的最后操作,如果这个文件是个链接文件,则需要找到真实文件之后再执行do_last */
	filp = do_last(nd, &path, op, pathname);
	while (unlikely(!filp)) { /* trailing symlink */
		struct path link = path;
		void *cookie;
		if (!(nd->flags & LOOKUP_FOLLOW)) {
			path_put_conditional(&path, nd);
			path_put(&nd->path);
			filp = ERR_PTR(-ELOOP);
			break;
		}
		nd->flags |= LOOKUP_PARENT;
		nd->flags &= ~(LOOKUP_OPEN|LOOKUP_CREATE|LOOKUP_EXCL);
		error = follow_link(&link, nd, &cookie);
		if (unlikely(error))
			filp = ERR_PTR(error);
		else
			filp = do_last(nd, &path, op, pathname);
		put_link(nd, &link, cookie);
	}
out:
	if (nd->root.mnt && !(nd->flags & LOOKUP_ROOT))
		path_put(&nd->root);
	if (base)
		fput(base);
	release_open_intent(nd);
	return filp;

out_filp:
	filp = ERR_PTR(error);
	goto out;
}

· nameidata是搜索用到的主要结构,首先要调用path_init将其初始化,然后调用link_path_walk搜寻文件,找到文件之后调用do_last来做文件打开操作,如果是个链接文件还需要找到真实文件,再执行do_last。
· 下面深入介绍 path_init 和 link_path_walk。

5.path_init

static int path_init(int dfd, const char *name, unsigned int flags,
		     struct nameidata *nd, struct file **fp)
{
	int retval = 0;
	int fput_needed;
	struct file *file;

	nd->last_type = LAST_ROOT; /* if there are only slashes... */
	nd->flags = flags | LOOKUP_JUMPED;
	nd->depth = 0;
	/* flags中表明是从根目录搜寻,然后查看文件权限上是否允许,如果允许,则对nd、RCU和顺序锁进行初始化后就返回 */
	if (flags & LOOKUP_ROOT) {
		struct inode *inode = nd->root.dentry->d_inode;
		if (*name) {
			if (!inode->i_op->lookup)
				return -ENOTDIR;
			retval = inode_permission(inode, MAY_EXEC);
			if (retval)
				return retval;
		}
		nd->path = nd->root;
		nd->inode = inode;
		if (flags & LOOKUP_RCU) {
			br_read_lock(vfsmount_lock);
			rcu_read_lock();
			nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
		} else {
			path_get(&nd->path);
		}
		return 0;
	}

	nd->root.mnt = NULL;
	/*通过字符的开头是"/",也可以表示绝对路径,则也要做相关初始化*/
	if (*name=='/') {
		if (flags & LOOKUP_RCU) {
			br_read_lock(vfsmount_lock);
			rcu_read_lock();
			set_root_rcu(nd);
		} else {
			set_root(nd);
			path_get(&nd->root);
		}
		nd->path = nd->root;
		/*开头不是"/",且dfd == AT_FDCWD表示使用相对路径*/
	} else if (dfd == AT_FDCWD) {
		if (flags & LOOKUP_RCU) {
			struct fs_struct *fs = current->fs;
			unsigned seq;

			br_read_lock(vfsmount_lock);
			rcu_read_lock();

			do {
				seq = read_seqcount_begin(&fs->seq);
				nd->path = fs->pwd;
				nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
			} while (read_seqcount_retry(&fs->seq, seq));
		} else {
			get_fs_pwd(current->fs, &nd->path);
		}
		/*最后一种特殊情况是依据文件号打开方式,本文不考虑此情况*/
	} else {
		struct dentry *dentry;

		file = fget_raw_light(dfd, &fput_needed);
		retval = -EBADF;
		if (!file)
			goto out_fail;

		dentry = file->f_path.dentry;

		if (*name) {
			retval = -ENOTDIR;
			if (!S_ISDIR(dentry->d_inode->i_mode))
				goto fput_fail;

			retval = inode_permission(dentry->d_inode, MAY_EXEC);
			if (retval)
				goto fput_fail;
		}

		nd->path = file->f_path;
		if (flags & LOOKUP_RCU) {
			if (fput_needed)
				*fp = file;
			nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
			br_read_lock(vfsmount_lock);
			rcu_read_lock();
		} else {
			path_get(&file->f_path);
			fput_light(file, fput_needed);
		}
	}

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

fput_fail:
	fput_light(file, fput_needed);
out_fail:
	return retval;
}

· 这里就是依据文件是绝对路径(从根目录开始搜寻)还是相对路径(从当前目录开始搜索)做区分,对nd、rcu和顺序锁做不同的初始化。AT_FDCWD在这里终于用到了,就是表示是相对路径的。
· 下面回到上一步,在看看初始化之后调用的link_path_walk做了什么:

6.link_path_walk

static int link_path_walk(const char *name, struct nameidata *nd)
{
	struct path next;
	int err;
	
	while (*name=='/') //  如果是/开头,这需要将其+到不是"/"为止
		name++;
	if (!*name) //  如果字符是无效值,则异常退出
		return 0;

	/* 到此,我们有了一个依据初始化的nameidata,开始搜寻循环 . */
	for(;;) {
		unsigned long hash;
		struct qstr this;
		unsigned int c;
		int type;

		err = may_lookup(nd);  //  查询文件权限是否允许访问
 		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);

		/*  判断文件名是否使用了"."或者"..",是则标明type  */
		type = LAST_NORM;
		if (this.name[0] == '.') switch (this.len) {
			case 2:
				if (this.name[1] == '.') {
					type = LAST_DOTDOT;
					nd->flags |= LOOKUP_JUMPED;
				}
				break;
			case 1:
				type = LAST_DOT;
		}
		/*  文件名没有"."或者"..",则是普通文件或目录,则查看是否有hash缓存表,有表示内存中存在缓存,没有直接退出  */
		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, nd->inode,
							   &this);
				if (err < 0)
					break;
			}
		}

		/* 如果尾部是"/",则open要打开的是目录,则直接退出 */
		if (!c)
			goto last_component;
		while (*++name == '/');  //  当有多个"/"时,则搜索时去掉
		if (!*name)
			goto last_component;

		/* 依据刚刚识别的类型,做不同的操作,此步骤再细说---> */
		err = walk_component(nd, &next, &this, type, LOOKUP_FOLLOW);
		if (err < 0)
			return err;

		/* 限制递归调用不能超过8次,符号链接不能超过40次 */
		if (err) {
			err = nested_symlink(&next, nd);
			if (err)
				return err;
		}
		
		/* 判断是否可以继续查找,可以则继续 */
		if (can_lookup(nd->inode))
			continue;
		err = -ENOTDIR; 
		break;
		/* here ends the main loop */

last_component:
		nd->last = this;
		nd->last_type = type;
		return 0;
	}
	terminate_walk(nd);  //  查找完成操作,包括解RCU锁
	return err;
}

· 到这里我们看到循环了,这就是逐次搜索文件名的循环。
· 循环中,首先判断文件名有没有".“或者”…",来设置文件类型;然后查看是否有hash表,有表示内存中存在缓存,没有直接退出。
· 然后继续调用walk_component做某一次的搜寻操作。

7.walk_component

static inline int walk_component(struct nameidata *nd, struct path *path,
		struct qstr *name, int type, int follow)
{
	struct inode *inode;
	int err;
	/* "." 和 ".." 符号是特殊的,".." 尤其特殊,这是因为必须知道当前文件所在目录的父目录  */
	if (unlikely(type != LAST_NORM))
		return handle_dots(nd, type);  //处理"."和".."文件名,成功则设置nd->path.dentry和nd->inode,如果父目录是挂载点找到挂载点目录再处理。
		
	/*  普通文件的搜索  */
	err = do_lookup(nd, name, path, &inode);
	if (unlikely(err)) {
		terminate_walk(nd);
		return err;
	}
	/*  如果没有找到,返回对应的错误码  */
	if (!inode) {
		path_to_nameidata(path, nd);
		terminate_walk(nd);
		return -ENOENT;
	}
	
	/*  查看是否链接次数超过最大值,前面提到过,对链接次数有限制  */
	if (should_follow_link(inode, follow)) {
		if (nd->flags & LOOKUP_RCU) {
			if (unlikely(unlazy_walk(nd, path->dentry))) {
				terminate_walk(nd);
				return -ECHILD;
			}
		}
		BUG_ON(inode != path->dentry->d_inode);
		return 1;
	}
	path_to_nameidata(path, nd); //  将path中的dentry放到nd->path中
	nd->inode = inode;
	return 0;
}

· 这里先判断文件名是不是".“和”. .",如果是的话就处理退出;
· 如果是普通文件,就执行do_lookup,后面继续深入分析;
· 然后就是针对搜寻情况做一些异常判断,请看代码注释。

8.do_lookup


static int do_lookup(struct nameidata *nd, struct qstr *name,
			struct path *path, struct inode **inode)
{
	struct vfsmount *mnt = nd->path.mnt;
	struct dentry *dentry, *parent = nd->path.dentry;
	int need_reval = 1;
	int status = 1;
	int err;

	/*  文件可能存在RCU锁,这表示可能有别的进程使用,则有可能被加载到 dcache 中	 */
	if (nd->flags & LOOKUP_RCU) {
		unsigned seq;
		*inode = nd->inode;
		dentry = __d_lookup_rcu(parent, name, &seq, inode); // 搜索,只使用内存屏障作为防护手段
		if (!dentry)
			goto unlazy;

		/* 在子目录的read_seqcount_begin的内存屏障就足够,这里判断父目录和子目录的seq是否一致*/
		if (__read_seqcount_retry(&parent->d_seq, nd->seq))
			return -ECHILD;
		nd->seq = seq;
		/* 找到的目录是否需要重新生效,需要就执行denter的对应函数*/
		if (unlikely(dentry->d_flags & DCACHE_OP_REVALIDATE)) {
			status = d_revalidate(dentry, nd);
			if (unlikely(status <= 0)) {
				if (status != -ECHILD)
					need_reval = 0;
				goto unlazy;
			}
		}
		/*  找到的目录是否需要搜索  */
		if (unlikely(d_need_lookup(dentry)))
			goto unlazy;
		path->mnt = mnt; //将搜索到的mnt和dentry值保存,下次继续搜索
		path->dentry = dentry;
		/*  判断是否此文件是挂载点,是则需要在mount_hashtable中找挂载点,再找子目录 */
		if (unlikely(!__follow_mount_rcu(nd, path, inode)))
			goto unlazy;
		if (unlikely(path->dentry->d_flags & DCACHE_NEED_AUTOMOUNT))
			goto unlazy;
		return 0;
unlazy:
		if (unlazy_walk(nd, dentry))
			return -ECHILD;
	} else {
		dentry = __d_lookup(parent, name); // 搜索,需要使用RCU锁,在hash表中搜索。
	}
    /*  如果搜到的dentry标记了要再搜一次,这种情况就是该文件是另一个文件系统的挂载点  */
	if (dentry && unlikely(d_need_lookup(dentry))) {
		dput(dentry);
		dentry = NULL;
	}
retry:  // 没有找到dentry,则需要再搜索一次
	if (unlikely(!dentry)) {
		struct inode *dir = parent->d_inode;
		BUG_ON(nd->inode != dir);

		mutex_lock(&dir->i_mutex);
		dentry = d_lookup(parent, name);  //  再搜索一次
		if (likely(!dentry)) {
			dentry = d_alloc_and_lookup(parent, name, nd);
			if (IS_ERR(dentry)) {
				mutex_unlock(&dir->i_mutex);
				return PTR_ERR(dentry);
			}
			/* known good */
			need_reval = 0;
			status = 1;
		} else if (unlikely(d_need_lookup(dentry))) {
			dentry = d_inode_lookup(parent, dentry, nd);
			if (IS_ERR(dentry)) {
				mutex_unlock(&dir->i_mutex);
				return PTR_ERR(dentry);
			}
			/* known good */
			need_reval = 0;
			status = 1;
		}
		mutex_unlock(&dir->i_mutex);
	}
	if (unlikely(dentry->d_flags & DCACHE_OP_REVALIDATE) && need_reval)
		status = d_revalidate(dentry, nd);
	if (unlikely(status <= 0)) {
		if (status < 0) {
			dput(dentry);
			return status;
		}
		if (!d_invalidate(dentry)) {
			dput(dentry);
			dentry = NULL;
			need_reval = 1;
			goto retry;
		}
	}

	path->mnt = mnt;
	path->dentry = dentry;
	err = follow_managed(path, nd->flags);
	if (unlikely(err < 0)) {
		path_put_conditional(path, nd);
		return err;
	}
	if (err)
		nd->flags |= LOOKUP_JUMPED;
	*inode = path->dentry->d_inode;
	return 0;
}

· 这里首先会调用__d_lookup_rcu 或者 __d_lookup进入dcache中搜索看看是否存在相同文件,如果没找到,则会调用d_lookup再搜索一遍确认一次(可能在阻塞的这段时间被添加进了dcache)。如果真的没有,就要调用d_alloc_and_lookup 和 d_inode_lookup 使用硬盘文件系统的接口做慢速搜索。

9. dentry cache搜索函数__d_lookup_rcu

struct dentry *__d_lookup_rcu(struct dentry *parent, struct qstr *name,
				unsigned *seq, struct inode **inode)
{
	unsigned int len = name->len;
	unsigned int hash = name->hash;
	const unsigned char *str = name->name;
	struct hlist_bl_head *b = d_hash(parent, hash);
	struct hlist_bl_node *node;
	struct dentry *dentry;

	/* hash表的循环查找,实质上是for循环,要经过多次的比较,如果某项不同就跳过,继续搜索*/
	hlist_bl_for_each_entry_rcu(dentry, node, b, d_hash) {
		struct inode *i;
		const char *tname;
		int tlen;

		if (dentry->d_name.hash != hash)
			continue;

seqretry:
		*seq = read_seqcount_begin(&dentry->d_seq);
		if (dentry->d_parent != parent)
			continue;
		if (d_unhashed(dentry))
			continue;
		tlen = dentry->d_name.len;
		tname = dentry->d_name.name;
		i = dentry->d_inode;
		prefetch(tname);

		if (read_seqcount_retry(&dentry->d_seq, *seq))
			goto seqretry;
		if (unlikely(parent->d_flags & DCACHE_OP_COMPARE)) {
			if (parent->d_op->d_compare(parent, *inode,
						dentry, i,
						tlen, tname, name))
				continue;
		} else {
			if (dentry_cmp(tname, tlen, str, len))
				continue;
		}

		*inode = i;
		return dentry;
	}
	return NULL;
}

· 这里就是dcache中的搜索函数,dcache hash表采用for循环,每一项要经过多次的比较,如果某项不同就跳过,继续搜索,直到找到全部符合的文件,否者就是没找到。搜不到这不代表这个文件就不在dcache中,因为条件中有顺序锁seq的判断,在SMP的多核架构中可能会出现这种情况,这就需要回到第3步的do_filp_open中继续第二次的path_openat,这是就会调用__d_lookup函数,各位可以自行看看,内容也就增加了rcu和自旋锁的限制,这会引起阻塞。

10.硬盘搜索函数d_alloc_and_lookup

static struct dentry *d_alloc_and_lookup(struct dentry *parent,
				struct qstr *name, struct nameidata *nd)
{
	struct inode *inode = parent->d_inode;
	struct dentry *dentry;
	struct dentry *old;

	/* Don't create child dentry for a dead directory. */
	if (unlikely(IS_DEADDIR(inode)))
		return ERR_PTR(-ENOENT);
    /*  这里首先创建一个dentry对象 */
	dentry = d_alloc(parent, name);
	if (unlikely(!dentry))
		return ERR_PTR(-ENOMEM);
	/*  然后调用文件系统的lookup函数搜索 */
	old = inode->i_op->lookup(inode, dentry, nd);
	if (unlikely(old)) {
		dput(dentry);
		dentry = old;
	}
	return dentry;
}

· 到这里就比较简单了,先分配一个dentry对象,然后调用硬盘文件系统的lookup接口去搜索。

11.总结

· 回过头来,我们最好再看看这个调用过程,核心就在最后。
· 经过这次的分析,我们实际上深入了虚拟文件系统的核心部分,知道了内核是如何搜寻一个文件的。爽得很!!!!!!!!!!(这么多感叹号,是为了发泄这两周对我的折磨)
· 但是爽归爽,其实中间有些地方一带而过了,其实我们中间忽略了VFS的一些基础性的架构介绍,需要把这条走过的路巩固一下,请见下篇《文件系统原理 和 VFS架构》(一股 百家讲坛 的感觉 ~~~)。

------------------如果有所收获的话,请帮忙点个赞吧!

下一篇:https://blog.csdn.net/weixin_42523774/article/details/103739139

Logo

更多推荐