Linux内核学习之 -- 系统调用open()和write()的实现笔记

目录

  • 环境
  • 1.open()系统调用。
    • [1.1 getname()](#1.1 getname())
    • [1.2 get_unused_fd_flags()](#1.2 get_unused_fd_flags())
    • [1.3 do_filp_open()](#1.3 do_filp_open())
    • [1.4 fd_install()](#1.4 fd_install())
  • [2. write()系统调用](#2. write()系统调用)

环境

  • linux 4.19

很多注释都写在了代码里,就不单独总结出来放到文章中了,看一下代码+注释回忆一下即可

1.open()系统调用。

用户空间使用open(),最终调用到内核的函数如下,至于系统调用的过程,请看我的另外一篇博客:Linux内核学习之 -- ARMv8架构的系统调用

c 复制代码
fs/open.c:
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode) // g,三个参数为filename, flags, mode
{
	if (force_o_largefile())	// g, (BITS_PER_LONG != 32),其中BITS_PER_LONG应该是定义long类型的大小,32位架构long就是32,64位架构long是64
		flags |= O_LARGEFILE;	// g, 如果64位架构,补上O_LARGEFILE标志

	return do_sys_open(AT_FDCWD, filename, flags, mode);
}

只调用了一个函数,就是do_sys_open(),进一步分析do_sys_open()函数:

c 复制代码
fs/open.c:
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)	
{
	struct open_flags op;							// g,strcut open_flags这个结构体会包含一些控制flag,根据用户传入的参数进行初始化,之后open的操作就不再依赖于用户参数,而是依赖于这个结构体

	int fd = build_open_flags(flags, mode, &op);	// g, 根据传入的flagse会mode,来初始化op结构体,open的模式,flag等都在open_flags op结构体中
	struct filename *tmp;							// g, struct filename会在open过程中记录处理过后的文件名信息。之后open对h文件名的获取依赖于这个结构体

	if (fd)
		return fd;

	// g, 根据一个字符串指针,返回一个struct filename*,该结构体的->name域所指内存存储着文件名
	tmp = getname(filename);  	
	if (IS_ERR(tmp))
		return PTR_ERR(tmp);

	fd = get_unused_fd_flags(flags);	// g, 分配一个没用过的,符合要求的fd号,同时更新current->files->fdt里面的三个重要bitmap,有必要的话会分配新的占用更多内存的fdt替换原来的fdt
	if (fd >= 0) {
		struct file *f = do_filp_open(dfd, tmp, &op); // g, 该函数解析传入的文件路径名(主要是获取路径的最后一个分量对应的dentry以及inode),然后创建一个新的struct file结构体,使用找到的dentry和inode初始化这个file
		if (IS_ERR(f)) {
			put_unused_fd(fd);
			fd = PTR_ERR(f);
		} else {
			fsnotify_open(f);			// g, 通知其他相关项如事件通知、audit机制、文件系统监控系统等等,该文件已经打开
			fd_install(fd, f);			// g, 把file和fd关联起来,建立了current(当前进程的tast_struct)->files->fdt->fd[fd]与f的联系(fdt->fd是个struct file**指针,其实就是current fd array)
		}
	}
	putname(tmp);
	return fd;
}

在这里做一个总结,do_sys_open()实现的功能可以概括为:

  1. 解析用户传入的各种参数,包括模式,权限,文件名
  2. 分配一个fd号
  3. 根据解析的参数,创建并初始化一个struct file结构体。
  4. 建立fd号与创建的struct file的联系,后续的write(),read()等可以直接通过fd找到对应的struct file

接下来对其中的几个重要函数进行分析,在此之前先整理一个调用层次:

bash 复制代码
do_sys_open()
	-->build_open_flags()		// 解析用户参数
	-->getname()				// 解析用户传入的文件名
		-->getname_flags()
	-->get_unused_fd_flags()	// 获取一个未使用过的fd
		-->__alloc_fd()
	-->do_filp_open()			// 打开文件,创建并初始化一个新的struct file结构体
		-->set_nameidata()
		-->path_openat()
			-->path_init()
			-->link_path_walk()
			-->do_last()
	-->fsnotify_open()			// 通知其他相关项如事件通知、audit机制、文件系统监控系统等等,该文件已经打开
	-->fd_install()				// 建立分配的fd号与创建的struct file的关系
		-->__fd_install()

所有函数调用并没有全部展示,只是列出了open()系统调用中几个重要的函数

1.1 getname()

该函数用来转换用户传入的路径名,解析用户传入的路径名来填充为一个内核中的结构体struct filename:

c 复制代码
fs/namei.c:
getname(const char __user * filename)
{
	return getname_flags(filename, 0, NULL);
}

其中只是单纯的调用了函数getname_flags():

c 复制代码
fs/namei.c:

struct filename *
getname_flags(const char __user *filename, int flags, int *empty)
{
	struct filename *result;
	char *kname;
	int len;
	BUILD_BUG_ON(offsetof(struct filename, iname) % sizeof(long) != 0);

	// g, 与linux的审计机制有关,会记录一些自己设置的规则信息,如果开启了此功能就可以从此模块获取filename结构体。
	// g, 需要开启一个宏CONFIG_AUDITSYSCALL才有效果,看了下.config中CONFIG_AUDITSYSCALL is not set,所以暂时不去研究这个审计模块
	result = audit_reusename(filename);		
	if (result)
		return result;

	result = __getname();			// g, 是个宏,= kmem_cache_alloc(names_cachep, GFP_KERNEL),该函数会从names_cachep的slab分配器里分配一个struct filename出来
	if (unlikely(!result))	
		return ERR_PTR(-ENOMEM);

	/*
	 * First, try to embed the struct filename inside the names_cache
	 * allocation
	 */
	kname = (char *)result->iname;
	result->name = kname;				// g, result->name和result->iname[]都将指向同一个char

	len = strncpy_from_user(kname, filename, EMBEDDED_NAME_MAX);  // g, 应该跟copy_from_user大同小异,是一个用户空间和内存空间的字符串拷贝函数,最大复制的字符数不能超过EMBEDDED_NAME_MAX
	if (unlikely(len < 0)) {
		__putname(result);		// g, 如果有问题,直接释放这个slab
		return ERR_PTR(len);
	}

	
	/* -------------------- 分割线,上面是文件名不太大的情况,下面是当文件名超过了EMBEDDED_NAME_MAX = 4096 - offsetof(struct filename, iname) 之后的操作
	 * Uh-oh. We have a name that's approaching PATH_MAX. Allocate a
	 * separate struct filename so we can dedicate the entire
	 * names_cache allocation for the pathname, and re-do the copy from
	 * userland.
	 */
	// g, note: likely和unlikely也有点说法,可以优化if和else的执行顺序
	if (unlikely(len == EMBEDDED_NAME_MAX)) { 					 // g, 如果文件名字符串很长,已经 >= EMBEDDED_NAME_MAX了
		// g, offsetof:返回iname[]在filenamed中的偏移值
		const size_t size = offsetof(struct filename, iname[1]); 
		kname = (char *)result;									// g, 使kname当temp指针记录一下result,暂时称为result_old

		/*
		 * size is chosen that way we to guarantee that
		 * result->iname[0] is within the same object and that
		 * kname can't be equal to result->iname, no matter what.
		 */
		result = kzalloc(size, GFP_KERNEL);		// g,分配一段&(struct filename.iname[1]) - &(struct filemae)这么大小的内存,此时result指向新的内存空间了,暂时称为result_new
		if (unlikely(!result)) {
			__putname(kname);
			return ERR_PTR(-ENOMEM);
		}
		result->name = kname; 					// g,相当于result_new->name = result_old
		len = strncpy_from_user(kname, filename, PATH_MAX);	 // g, 然后覆盖result_old,相当于把原来从slab中分配的内存全部当做文件名的缓存区了,result_old不再是一个struct filename结构体,而是变成了一段内存
		if (unlikely(len < 0)) {
			__putname(kname);
			kfree(result);
			return ERR_PTR(len);
		}
		if (unlikely(len == PATH_MAX)) {			// g, 文件名
			__putname(kname);
			kfree(result);
			return ERR_PTR(-ENAMETOOLONG);
		}
	}

	result->refcnt = 1;			// g,可能是个标记位置
	/* The empty path is special. */
	if (unlikely(!len)) {
		if (empty)
			*empty = 1;
		if (!(flags & LOOKUP_EMPTY)) {
			putname(result);
			return ERR_PTR(-ENOENT);
		}
	}

	result->uptr = filename; 	// g,保存上层传过来的指针
	result->aname = NULL;
	audit_getname(result);
	return result;
}

该函数会把用户空间的文件名内存copy到内核内存中。这片内核内存可能存在于在struct filename结构体内部,也可能是一段单独分配的内核内存,也就是会内嵌或者外挂一段内存来存放文件路径名。 对于这一段代码,解释如下:

因为strcut filename中最后一个域->iname是一个空数组iname[],所以可以存放一定长度的文件名,如果文件名不长,则文件名所占用的内存完全可以由一个struct filename消化掉。

但是struct filenamed最后总size不能超过4096,如果sizeof(struct filename) + 文件名长度不超过4096,那就用iname[]来存储文件名,然后使filename->name指向filename->iname[]

如果文件名比较长,文件名大小+sizeof(struct filename)超过了4096,就不用iname[]来存储,需要另外申请一段内存来存储文件名,然后使filename->name指向另外申请的内存段

有4096大小的限制是因为struct filename是从slab中分配的,分配通过一个宏:

c 复制代码
include/linux/fs.h

#define __getname()		kmem_cache_alloc(names_cachep, GFP_KERNEL)

在vfs初始化的时候,调用vfs_caches_init()创建这个slab时是以4096字节创建的node:

c 复制代码
fs/dcache.c:

void __init vfs_caches_init(void)
{
	... 
	names_cachep = kmem_cache_create_usercopy("names_cache", PATH_MAX, 0,	// g, PATH_MAX = 4096
			SLAB_HWCACHE_ALIGN|SLAB_PANIC, 0, PATH_MAX, NULL);
	...
}

1.2 get_unused_fd_flags()

该函数用于根据一些规则,获取一个未使用过的fd号:

c 复制代码
int get_unused_fd_flags(unsigned flags)
{
	// g, RLIMIT_NOFILE作为一个数组的索引号:rlimit(RLIMIT_NOFILE) = current->signal->rlim[RLIMIT_NOFILE].rlim_cur,
	// g, rlim[7]存放着最大文件数限制,默认为1024
	return __alloc_fd(current->files, 0, rlimit(RLIMIT_NOFILE), flags); // g,传入task_struct的files域,即struct files_struct,保存与该进程有关的文件信息,比如保存着打开的文件/文件描述符等等
}

其中单纯的调用了__alloc_fd()函数,并且设置了进程可以打开的最大文件数的限制,默认为1024。__alloc_fd()的实现如下:

c 复制代码
int __alloc_fd(struct files_struct *files,					// g,传入task_struct的fils域,即struct files_struct,保存与该进程有关的文件信息,比如保存着打开的文件/文件描述符等等			
	       unsigned start, unsigned end, unsigned flags)
{
	unsigned int fd;
	int error;
	struct fdtable *fdt;

	spin_lock(&files->file_lock);		// g, 本进程files->file_lock上锁
repeat:
	fdt = files_fdtable(files);			// g, 找到task_struct->files域中的fdtable,sh起就是直接获取(files)->fdt,但是这个宏会检查files->file_lock是否锁住了
	fd = start;
	if (fd < files->next_fd)			// g, files->next_fd缓存着下一个可用的fd,至少从->next_fd起才可用,传入的start不一定有效
		fd = files->next_fd;

	if (fd < fdt->max_fds)				// g, 如果fd没有超过最大打开文件数量限制,current->files->fdt在初始化的时候会设置max_fdset = 1024(最大打开的文件数),max_fds初始化 = 32。max_fds会动态变化
		fd = find_next_fd(fdt, fd);		// g, 共两级bitmap查找能用的fd,第一级map一个bit表示64个文件,第二级一个bit表示一个文件,先找到第一级的空bit,再去找对应的第二级bitmap

	/*
	 * N.B. For clone tasks sharing a files structure, this test
	 * will limit the total number of files that can be opened.
	 */
	error = -EMFILE;
	if (fd >= end)		// g,这个end完全看调用方传入,不一定是最大打开文件数量限制(默认1024),你想传入多少传入多少,不过open系统调用传入的是1024
		goto out;


	// g, 关于这个max_fds有必要说两点,这个max_fds,表示了当前fdt可以容纳的数量,会动态分配,否则浪费内存。比如说初始化为32,当fd>=32时会再加一点,然后再加一点
	// g, 主要通过三个函数:
	// g, 1.new_fdt = alloc_fdtable(fd)--申请足以表示fd个文件描述符的内存空间;2.copy_fdtable(new_fdt, cur_fdt)--文件描述符信息拷贝;3.rcu_assign_pointer(files->fdt, new_fdt)--文件描述符表指针更新
	error = expand_files(files, fd);
	if (error < 0)
		goto out;

	/*
	 * If we needed to expand the fs array we
	 * might have blocked - try again.
	 */
	if (error)
		goto repeat;		// g, 扩展fdt_table成功,重新到repeat处,重新分配fd(因为刚才没扩展之前,fd没有 < fdt->max_fds,跳过了fd的分配)

	if (start <= files->next_fd)
		files->next_fd = fd + 1;		// g, 更新缓存域,而且就是单纯的递增,表示下一个可以用的fd至少是刚分配的fd + 1

	__set_open_fd(fd, fdt);				// g, 更新fdt表,是bitmap实现的,会使用__set_bit来更新fdt->open_fds域和fdt->full_fds_bits域,即一级bitmap和二级bitmap,二级bitmap没满64个才会更新一个一级bitmap
	if (flags & O_CLOEXEC)				// g, 这个标志使的新进程创建时在继承父进程的文件描述符表时,会关闭父进程打开过的文件
		__set_close_on_exec(fd, fdt);	// g, 通过current->files->fdt->close_on_exec,也是一个位图,记录在exec时要关闭的fd
	else
		__clear_close_on_exec(fd, fdt);
	error = fd;
#if 1
	/* Sanity check */
	if (rcu_access_pointer(fdt->fd[fd]) != NULL) {
		printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
		rcu_assign_pointer(fdt->fd[fd], NULL);
	}
#endif

out:
	spin_unlock(&files->file_lock);
	return error;
}

该函数主要就是对分配的fd是否满足要求进行检查(是否大于最大可分配fd,是否大于当前进程允许分配的fd等等),检查通过后进行分配。

分配主要依赖于一个二级bitmap结构,第一级为粗粒度bitmap数组,一个bit可以表示64个fd,第二级bitmap数组为细粒度,一个bit表示一个fd。

同时会缓存下一个可用的fd,从实现中可以看到,fd的分配顺序是递增的。上一次分配了n,则下一次就会分配n+1。

1.3 do_filp_open()

该函数是open()系统调用中作用最大的函数,大部分工作都在这个函数中完成。该函数负责解析传入的文件路径名(主要是获取路径的最后一个分量对应的dentry以及inode),然后创建一个新的struct file结构体,使用找到的dentry和inode初始化这个新创建的struct file:

c 复制代码
struct file *do_filp_open(int dfd, struct filename *pathname,
		const struct open_flags *op)
{
	struct nameidata nd;					// nameidata类型的nd在整个路径查找过程中充当中间变量,它既可以为当前查找输入数据,又可以保存本次查找的结果。但是看上去该函数执行完后nd会被释放
	int flags = op->lookup_flags;			// g, op->lookup_flags可能有三种:LOOKUP_DIRECTORY(对应用户传参O_DIRECTORY),LOOKUP_FOLLOW(对应用户传参O_NOFOLLOW),0
	struct file *filp;

	set_nameidata(&nd, dfd, pathname);		// g, 初始化新的nameidata,并且使current->nameidata(新)->save = current->nameidata(旧), current->nameidata(新) = nd, 。dfd默认-100

	// g, path_openat有可能会被调用三次。通常内核为了提高效率,会首先在RCU模式下进行文件打开操作
	// g, 该函数会分配一个struct file结构体,并且初始化这个新的struct file结构体,过程比较复杂
	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;
}

其中struct nameidata作为一个解析路径的辅助结构体,协助path_openat()函数进行路径解析。path_openat()有可能会被调用三次。通常内核为了提高效率,会首先在RCU模式下进行文件打开操作。而path_openat()函数则会创建并返回一个struct file,并且已经为其完成了填充工作:

c 复制代码
static struct file *path_openat(struct nameidata *nd,
			const struct open_flags *op, unsigned flags)
{
	struct file *file; // g, 虽然是要返回一个file结构体,但是整个过程其实是围绕nameidata *nd进行的,可以认为是open上下文中比较重要的一个结构体
	int error;

	file = alloc_empty_file(op->open_flag, current_cred());		// g, 分配file结构体,并且会检查一些文件最大数之类的,检查通过才会分配。分配方式是slab,并初始化了部分成员变量
	if (IS_ERR(file))
		return file;

	// g, 接下来根据file->f_flags执行不同的操作,file->f_flags在alloc_empty_file中被初始化为op->open_flag,op->open_flag根据用户传入的flag和mode初始化的
	if (unlikely(file->f_flags & __O_TMPFILE)) {		// g, __O_TMPFILE表示匿名文件,文件描述符关闭文件自动删除
		error = do_tmpfile(nd, flags, op, file);		// g, 匿名文件指的是:能够像普通的文件一样进行读写,但是不会再磁盘上创建对应的文件(磁盘上找不到对应的文件名),就是匿名inode。
	} else if (unlikely(file->f_flags & O_PATH)) {		// g, O_PATH不会真正打开一个文件,只是准备好该文件的文件描述符,也就是说O_PATH表示仅仅获取一个fd,没有真正打开文件
		error = do_o_path(nd, flags, file);
	} else {
		// g, 大部分open应该都会走到这里,上面那俩flag不常用。path_init函数做了两个工作:
		// 1.获取要打开的文件路径(早已储存在nd->name中) 
		// 2.初始化nd的一些重要成员,若s为相对路径:nd->path = current->fs->pwd,nd->inode = current->fs->pwd.dentry->d_inode;因为fs->pwd保存着进程当前所处的工作目录
		//   若s为绝对路径:nd->root = fs->root
		//   然后接下来的路径搜索,就会以此为起点(nd->path)
		const char *s = path_init(nd, flags);

		// g, 关于这个do_last函数,是设置struct file* file的主要函数,最终会将file->path.dentry指向link_path_walk()解析出的最后一个分量的dentry,而且会找到该dentry指向的inode,使file->inode = 这个inode
		//	  这样就实现了struct file与struct dentry + struct inode的关联。解析过程较为复杂。
		// g, 其中有一步关键函数:vfs_open()->do_entry_open():
		//	  1.如果找到的inode->file_operations不为空,则会设置这个file->file_operations等于inode->file_operations。
		//    2.调用一次inode->file_operations->open(),所以说在用户空间open一个设备驱动,最终能调用到驱动绑定的file_ops->open。
		while (!(error = link_path_walk(s, nd)) &&	// g, 是解析路径的主要函数,用于将pathname转换成最终该文件对应的的dentry,也就是路径最后一个分量对应的dentry。并存储于nd->path.dentry中,交由do_last()进行处理
			(error = do_last(nd, file, op)) > 0) 	// g, open的最后一步。创建(对于一个新的文件来说)或者获取文件对应的inode对象(通过上一步得到的dentry来获取对应的inode),并且初始化file对象
		{		
			nd->flags &= ~(LOOKUP_OPEN|LOOKUP_CREATE|LOOKUP_EXCL);
			s = trailing_symlink(nd);				// g, 这里面会设置nd->flags |= LOOKUP_PARENT,检查当前打开的文件是否是符号链接,以及是否有权限来得到符号链接的真实链接
		}
		terminate_walk(nd);
	}
	if (likely(!error)) {							// g, 如果到这里没啥问题
		if (likely(file->f_mode & FMODE_OPENED))	// g, 并且已经打开成功
			return file;							// g, 可以顺利返回了
		WARN_ON(1);
		error = -EINVAL;
	}
	fput(file);
	if (error == -EOPENSTALE) {
		if (flags & LOOKUP_RCU)
			error = -ECHILD;
		else
			error = -ESTALE;
	}
	return ERR_PTR(error);
}

该函数中首先调用了path_init()函数,该函数决定了当前的路径解析会以哪个路径作为起点:

c 复制代码
static const char *path_init(struct nameidata *nd, unsigned flags)
{
	const char *s = nd->name->name;	// g, nameidata->name已经在初始化的时候设置为了filename->name,实际上也就是用户态传来的路径名

	if (!*s)
		flags &= ~LOOKUP_RCU;
	if (flags & LOOKUP_RCU)
		rcu_read_lock();

	nd->last_type = LAST_ROOT; /* if there are only slashes... */
	nd->flags = flags | LOOKUP_JUMPED | LOOKUP_PARENT;
	nd->depth = 0;
	if (flags & LOOKUP_ROOT) {			// g, note: 没见过有这个flags哈,所以这个if应该是不执行的,具体这个flag代表什么意思后续可以研究下
		struct dentry *root = nd->root.dentry;
		struct inode *inode = root->d_inode;
		if (*s && unlikely(!d_can_lookup(root)))
			return ERR_PTR(-ENOTDIR);
		nd->path = nd->root;
		nd->inode = inode;
		if (flags & LOOKUP_RCU) {
			nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
			nd->root_seq = nd->seq;
			nd->m_seq = read_seqbegin(&mount_lock);
		} else {
			path_get(&nd->path);
		}
		return s;
	}

	nd->root.mnt = NULL;
	nd->path.mnt = NULL;
	nd->path.dentry = NULL;					// g, 如果没有LOOK_UP flags,暂时把目录结构体设置为null

	nd->m_seq = read_seqbegin(&mount_lock);
	if (*s == '/') {						// g, '/'起手,说明传入的是绝对路径
		set_root(nd);
		if (likely(!nd_jump_root(nd)))
			return s;
		return ERR_PTR(-ECHILD);
	} else if (nd->dfd == AT_FDCWD) {			// g, 这个if是要执行的,因为dfd在系统调用的第一步就被初始化为了AT_FDCWD,该flag表示这个相对路径是以当前路径pwd作为起始的
		if (flags & LOOKUP_RCU) {				// g, 如果使用RCU锁
			struct fs_struct *fs = current->fs;	// g, 获取当前task_strcut的fs域,该域包含了文件系统相关的内容,比如当前正在执行的文件,当前工作目录(pwd),根目录等
			unsigned seq;						// g, note: seq是和顺序读取/顺序锁有关的域,早期版本的linux内核中fs_struct中是没有这个域的,什么作用后续再看

			do {
				seq = read_seqcount_begin(&fs->seq);
				nd->path = fs->pwd;						// g, pwd域即为当前工作目录,是一个struct path结构体,里面记录了该路径对应的目录项dentry结构体
				nd->inode = nd->path.dentry->d_inode;	// g, 算是拐弯抹角的获取到了对应的inode了,其实也就是current->fs->pwd.dentry->d_inode
				nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
			} while (read_seqcount_retry(&fs->seq, seq));	// g, note: 关于顺序锁,不是很熟悉,后续再研究一下
		} else {
			get_fs_pwd(current->fs, &nd->path);
			nd->inode = nd->path.dentry->d_inode;
		}
		return s;								// g, 到这里应该就返回了
	} 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 && unlikely(!d_can_lookup(dentry))) {
			fdput(f);
			return ERR_PTR(-ENOTDIR);
		}

		nd->path = f.file->f_path;
		if (flags & LOOKUP_RCU) {
			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;
	}
}

之后便是解析路径的主要函数link_path_walk(),该函数比较复杂, 是解析路径的主要函数,用于将pathname转换成最终该文件对应的的dentry,也就是路径最后一个分量对应的dentry。并存储于nd->path.dentry中,交由do_last()进行处理。对其实现方式进行了简单的分析:

c 复制代码
fs/namei.c:
static int link_path_walk(const char *name, struct nameidata *nd)	// g, name:用户传入的路径
{
	int err;

	if (IS_ERR(name))
		return PTR_ERR(name);
	while (*name=='/')		// g, 如果是根目录的话跳过根目录
		name++;
	if (!*name)
		return 0;

	/* At this point we know we have a real path component. */
	// g, 通过循环,将用户所指定的路径name从头至尾进行了搜索,至此nd保存了最后一个目录项的信息(i比如./abc/a/b,最后会保存b这个目录项的信息),但是内核并没有确定最后一个目录项是否真的存在,这些工作将在do_last()中进行
	for(;;) {
		u64 hash_len;
		int type;

		err = may_lookup(nd);			// g, 检查存放的索引节点的访问模式和运行进程的权限,就是检查当前进程对nd中保存的inode的各种权限。
		if (err)
			return err;

		hash_len = hash_name(nd->path.dentry, name);	// g, 在用nd->path.dentry和name来计算一个hash值。这是文件系统维护的通过散列表搜索dentry的方式

		type = LAST_NORM;
		if (name[0] == '.') switch (hashlen_len(hash_len)) {	// g, 如果传入的路径第一个是'.'(意味着是相对路径)
			case 2:
				if (name[1] == '.') {
					type = LAST_DOTDOT;							// g, 相对路径情况1:上一层路径
					nd->flags |= LOOKUP_JUMPED;					// g, 设置一层LOOKUP_JUMPED flag
				}
				break;											// g, 如果是..,则需要跳出循环,尝试返回父目录,但是好像没有return啊?这搞集贸啊
			case 1:											
				type = LAST_DOT;								// g, 相对路径情况2:本层路径
		}
		if (likely(type == LAST_NORM)) {						// g, 如果是正常分量,不是'.'或者'..'
			struct dentry *parent = nd->path.dentry;
			nd->flags &= ~LOOKUP_JUMPED;						// g, 清空LOOKUP_JUMPED标志
			if (unlikely(parent->d_flags & DCACHE_OP_HASH)) {	// g,warn: 如果d_flags里面没有DCACHE_OP_HASH标志,个人猜测就是没加入到散列表缓存中,所以需要加进去
				struct qstr this = { { .hash_len = hash_len }, .name = name };	// g, 其实struct dentry的d_name域就是一个struct qstr结构体,里面记录着目录项的名字
				err = parent->d_op->d_hash(parent, &this);		// g, d_hash()一般是由文件系统来实现,作用是把目标dentry加入到散列表中,这样后面用d_lookup()查找散列表时可以快速地返回对应的dentry结构体,省时省力
				if (err < 0)
					return err;
				hash_len = this.hash_len;
				name = this.name;
			}
		}

		// g, 保存一下当前的分量的各个属性,因为要遍历到下一个了
		nd->last.hash_len = hash_len;			// g, 保存一下当前分量计算得到的hash值
		nd->last.name = name;					// g, 保存一下当前name
		nd->last_type = type;					// g, 保存一下当前分量的hash值

		name += hashlen_len(hash_len);			// g, 跳过当前分量,现在name指向了下一个分量,比如/a/b/c,循环过程就是/a/b/c -> /b/c - >/c
		if (!*name)
			goto OK;
		/*
		 * If it wasn't NUL, we know it was '/'. Skip that
		 * slash, and continue until no more slashes.
		 */
		do {
			name++;
		} while (unlikely(*name == '/'));		// g, 依然是跳过'/',找下一级分量
		if (unlikely(!*name)) {
OK:
			/* pathname body, done */
			if (!nd->depth)
				return 0;
			name = nd->stack[nd->depth - 1].name;	// g, nd还维护了一个name栈,就是在解析路径时的每一个分量吧
			/* trailing symlink, done */
			if (!name)
				return 0;
			/* last component of nested symlink */
			// g, note:没太看懂这个函数,涉及的太多了,看别人介绍的:walk_component()处理当前目录项,更新nd和next;如果当前目录项为符号链接文件,则只更新next
			err = walk_component(nd, WALK_FOLLOW);		// g, 主要就是不断查找路径是否正确。还是以/a/b/c为例,每次循环该函数都会查找:/下是否有a,/a下是否有b, /a/b下是否有c
		} else {
			/* not the last component */
			err = walk_component(nd, WALK_FOLLOW | WALK_MORE);
		}
		if (err < 0)
			return err;

		if (err) {
			const char *s = get_link(nd);

			if (IS_ERR(s))
				return PTR_ERR(s);
			err = 0;
			if (unlikely(!s)) {
				/* jumped */
				put_link(nd);
			} else {
				nd->stack[nd->depth - 1].name = name;
				name = s;
				continue;
			}
		}
		if (unlikely(!d_can_lookup(nd->path.dentry))) {
			if (nd->flags & LOOKUP_RCU) {
				if (unlazy_walk(nd))
					return -ECHILD;
			}
			return -ENOTDIR;
		}
	}
}

其中最重要的函数是walk_component():

c 复制代码
fs/namei.c:
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.
	 */
	if (unlikely(nd->last_type != LAST_NORM)) {			// g, 如果刚才的分量不是NORMAL,那就说明是"."或者"..",那就要单独处理了
		err = handle_dots(nd, nd->last_type);
		if (!(flags & WALK_MORE) && nd->depth)
			put_link(nd);
		return err;
	}
	err = lookup_fast(nd, &path, &inode, &seq);			// g, lookup_fast查询dentry中的缓存,看一下是否命中,如果没有命中,则会用lookup_slow下降到文件系统层进行路径查找。
	if (unlikely(err <= 0)) {
		if (err < 0)
			return err;
		path.dentry = lookup_slow(&nd->last, nd->path.dentry,
					  nd->flags);
		if (IS_ERR(path.dentry))
			return PTR_ERR(path.dentry);

		path.mnt = nd->path.mnt;
		err = follow_managed(&path, nd);				// g, follow_managed函数会检查当前dentry是否是个挂载点,如果是就跟下去
		if (unlikely(err < 0))
			return err;

		if (unlikely(d_is_negative(path.dentry))) {
			path_to_nameidata(&path, nd);				// g, 至此,如果当前目录项查找成功,则通过path_to_nameidata()更新nd,更新nd->path.dentry = path->dentry
			return -ENOENT;
		}

		seq = 0;	/* we are already out of RCU mode */
		inode = d_backing_inode(path.dentry);
	}
	
	return step_into(nd, &path, flags, inode, seq);		// g, step_into进行下一级目录的解析。
}

过于复杂,没有深入分析。该函数就是来解析目录是否存在的。这些文章对该函数进行了比较深入的分析,后续有时间可以再研究一下:

解析完整个文件路径后,会保存最后一个分量的dentry到nd->path.dentry,接下来就交由do_last()函数做最后的处理。

关于这个do_last函数,是设置struct file* file的主要函数,最终会将file->path.dentry指向link_path_walk()解析出的最后一个分量的dentry,如果最后一个分量的类型是NORMAL,也就是说是正常文件而不是"."或者"...",就会找到该dentry指向的inode。如果inode不存在,并且传入了O_CREATE标志,则会创建一个inode。使file->inode指向这个inode。这样就实现了struct file与struct dentry + struct inode的关联。

这个过程较为复杂,能力不够,没法深入去了解。后面有时间再研究一下。

但是其中有一步关键函数:vfs_open()->do_entry_open()。就是如果找到的inode->file_operations不为空,则会设置这个file->file_operations等于inode->file_operations。

然后会调用一次inode->file_operations->open(),所以说在用户空间open一个设备驱动,最终能调用到驱动绑定的file_ops->open()。

当做完这一切之后,如果成功,返回成功结果,也就是返回新创建并初始化完的struct file

1.4 fd_install()

该函数单一的调用了__fd_install()函数

c 复制代码
fs/file.c: 
void fd_install(unsigned int fd, struct file *file)
{
	__fd_install(current->files, fd, file);
}

__fd_install()函数的实现就比较简单,就是把file和fd关联起来,建立了current(当前进程的tast_struct)->files->fdt->fd[fd]与f的联系(fdt->fd是个struct file**指针,其实就是current fd array):

c 复制代码
void __fd_install(struct files_struct *files, unsigned int fd,
		struct file *file)
{
	struct fdtable *fdt;

	rcu_read_lock_sched();

	if (unlikely(files->resize_in_progress)) {
		rcu_read_unlock_sched();
		spin_lock(&files->file_lock);
		fdt = files_fdtable(files);
		BUG_ON(fdt->fd[fd] != NULL);
		rcu_assign_pointer(fdt->fd[fd], file); // g, 建立fdt->fd数组中index为fd的位置与file结构体的对应关系
		spin_unlock(&files->file_lock);
		return;
	}
	/* coupled with smp_wmb() in expand_fdtable() */
	smp_rmb();
	fdt = rcu_dereference_sched(files->fdt);
	BUG_ON(fdt->fd[fd] != NULL);
	rcu_assign_pointer(fdt->fd[fd], file);
	rcu_read_unlock_sched();
}

这样用户空间的write()/read()等函数,就可以根据fd号,找到对应的struct file了。

2. write()系统调用

与open()相比,write()就非常简单了:

c 复制代码
fs/read_write.c:
ssize_t __vfs_write(struct file *file, const char __user *p, size_t count,
		    loff_t *pos)
{
	if (file->f_op->write)
		return file->f_op->write(file, p, count, pos);
	else if (file->f_op->write_iter)
		return new_sync_write(file, p, count, pos);
	else
		return -EINVAL;
}
...
...
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
	ssize_t ret;

	// g, 这里做权限检查,查一下是否有读写权限。file->f_mod应该是在open的时候创建struct fd的时候就初始化了
	if (!(file->f_mode & FMODE_WRITE))
		return -EBADF;
	if (!(file->f_mode & FMODE_CAN_WRITE))
		return -EINVAL;
	if (unlikely(!access_ok(VERIFY_READ, buf, count)))
		return -EFAULT;

	ret = rw_verify_area(WRITE, file, pos, count);	// g, 在确认文件可读写的区域
	if (!ret) {
		if (count > MAX_RW_COUNT)					// g, 最大写入量有限制
			count =  MAX_RW_COUNT;
		file_start_write(file);
		ret = __vfs_write(file, buf, count, pos);
		if (ret > 0) {
			fsnotify_modify(file);
			add_wchar(current, ret);
		}
		inc_syscw(current);
		file_end_write(file);
	}

	return ret;
}
...
...
ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{
	// g, 最终也是通过fd找到current->files->fdt->fd[fd],然后把struct file强行转为struct fd
	struct fd f = fdget_pos(fd);		// g, 根据文件描述符fd获取结构体类型struct fd,里面包含两个域:struct file域(其中的f_path域包含struct dentry,也就是该fd对应的目录项)和uint flags域
	ssize_t ret = -EBADF;

	if (f.file) {
		loff_t pos = file_pos_read(f.file);				// g, 读取当前f_pos
		ret = vfs_write(f.file, buf, count, &pos);		// g, 调用虚拟文件系统提供的write
		if (ret >= 0)
			file_pos_write(f.file, pos);				// g, 重新记录f_pos,光标偏移
		fdput_pos(f);
	}

	return ret;
}
...
...
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
		size_t, count)
{
	return ksys_write(fd, buf, count);		// g,这就是write最终的系统调用了
}

不过多解释,对于字符设备来说最终也是调用到设备的inode实现的write,也就是字符设备的write。对于正常文件,则是与相应的文件系统有关,这个后面需要单独写一篇文章整理一下。

相关推荐
Spring_java_gg6 分钟前
如何抵御 Linux 服务器黑客威胁和攻击
linux·服务器·网络·安全·web安全
✿ ༺ ོIT技术༻6 分钟前
Linux:认识文件系统
linux·运维·服务器
会掉头发34 分钟前
Linux进程通信之共享内存
linux·运维·共享内存·进程通信
鸭鸭梨吖35 分钟前
产品经理笔记
笔记·产品经理
Chef_Chen36 分钟前
从0开始学习机器学习--Day13--神经网络如何处理复杂非线性函数
神经网络·学习·机器学习
我言秋日胜春朝★37 分钟前
【Linux】冯诺依曼体系、再谈操作系统
linux·运维·服务器
齐 飞1 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
饮啦冰美式1 小时前
22.04Ubuntu---ROS2使用rclcpp编写节点
linux·运维·ubuntu
wowocpp1 小时前
ubuntu 22.04 server 安装 和 初始化 LTS
linux·运维·ubuntu