欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 名人名企 > Linux内核学习之 -- 系统调用open()和write()的实现笔记

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

2024/10/24 19:22:56 来源:https://blog.csdn.net/xi_xix_i/article/details/138439493  浏览:    关键词:Linux内核学习之 -- 系统调用open()和write()的实现笔记

目录

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

环境

  • linux 4.19

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

1.open()系统调用。

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

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是64flags |= O_LARGEFILE;	// g, 如果64位架构,补上O_LARGEFILE标志return do_sys_open(AT_FDCWD, filename, flags, mode);
}

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

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替换原来的fdtif (fd >= 0) {struct file *f = do_filp_open(dfd, tmp, &op); // g, 该函数解析传入的文件路径名(主要是获取路径的最后一个分量对应的dentry以及inode),然后创建一个新的struct file结构体,使用找到的dentry和inode初始化这个fileif (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

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

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:

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

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

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[]都将指向同一个charlen = strncpy_from_user(kname, filename, EMBEDDED_NAME_MAX);  // g, 应该跟copy_from_user大同小异,是一个用户空间和内存空间的字符串拷贝函数,最大复制的字符数不能超过EMBEDDED_NAME_MAXif (unlikely(len < 0)) {__putname(result);		// g, 如果有问题,直接释放这个slabreturn 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_newif (unlikely(!result)) {__putname(kname);return ERR_PTR(-ENOMEM);}result->name = kname; 					// g,相当于result_new->name = result_oldlen = 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中分配的,分配通过一个宏:

include/linux/fs.h#define __getname()		kmem_cache_alloc(names_cachep, GFP_KERNEL)

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

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

1.2 get_unused_fd_flags()

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

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

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

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系统调用传入的是1024goto 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个才会更新一个一级bitmapif (flags & O_CLOEXEC)				// g, 这个标志使的新进程创建时在继承父进程的文件描述符表时,会关闭父进程打开过的文件__set_close_on_exec(fd, fdt);	// g, 通过current->files->fdt->close_on_exec,也是一个位图,记录在exec时要关闭的fdelse__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);}
#endifout: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:

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),0struct 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,并且已经为其完成了填充工作:

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;elseerror = -ESTALE;}return ERR_PTR(error);
}

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

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,暂时把目录结构体设置为nullnd->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_inodend->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()进行处理。对其实现方式进行了简单的分析:

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, 保存一下当前namend->last_type = type;					// g, 保存一下当前分量的hash值name += hashlen_len(hash_len);			// g, 跳过当前分量,现在name指向了下一个分量,比如/a/b/c,循环过程就是/a/b/c -> /b/c - >/cif (!*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;如果当前目录项为符号链接文件,则只更新nexterr = 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():

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->dentryreturn -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进行下一级目录的解析。
}

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

  • linux虚拟文件系统-文件的打开
  • Linux open系统调用(四)
  • LINUX VFS分析之五open接口分析与总结

解析完整个文件路径后,会保存最后一个分量的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()函数

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):

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()就非常简单了:

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);elsereturn -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 fdstruct 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_posret = vfs_write(f.file, buf, count, &pos);		// g, 调用虚拟文件系统提供的writeif (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。对于正常文件,则是与相应的文件系统有关,这个后面需要单独写一篇文章整理一下。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com