LINUX devtmpfs设备文件系统分析 (设备文件创建、删除、访问等)

mac2022-06-30  26

在之前几篇文章中,我们介绍了文件系统的注册、超级块的创建、dentry、inode创建、文件描述符,以及这些结构体之间的关联,文件系统模块与进程模块之间的关联,本文介绍dev文件系统,该文件系统涉及设备文件的创建、访问以及对设备文件的访问操作等。

       本篇文章主要包括如下小节(代码基于linux3.10版本)

1.设备文件系统的注册

2.设备文件系统的挂载

3.设备文件的创建

4.设备文件的删除

5.devtmpfsd线程事务处理

6.设备访问与VFS文件系统之间的关联

7.设备注册时inode节点创建的方式。

 

对于devtmpfs模块,使用了完成量,而针对完成量的介绍,会单独介绍,本文则不再细述。

一、设备文件系统的注册

在之前几篇文章的说明中,已经介绍了文件系统相关的数据结构,此处即不再赘述,针对设备文件系统而言,其文件系统变量定义如下:

static struct file_system_type dev_fs_type = {

.name = "devtmpfs",

.mount = dev_mount,/*(用于超级块、根dentry、根inode)*/

.kill_sb = kill_litter_super,

};

文件系统的名称为“devtmpfs”,该文件系统的mount接口函数为dev_mount,而其kill_sb接口则为kill_litter_super.

 

在设备文件系统初始化接口devtmpfs_init中,通过调用register_filesystem,完成文件系统的注册(关于文件系统注册函数的分析,已在之前分析过,可在该链接中查看https://www.toutiao.com/i6731356430449771016/)

 

二、设备文件系统的挂载

在应用层挂载文件系统,通过mount命令即调用内核的系统调用函数sys_mount(通过调用do_mount实现挂载),实现挂载操作;

而在内核中,子模块同样可调用sys_mount接口实现文件系统的挂载操作。

 

sys_mount接口分析

sys_mount接口的流程图如下,其主要功能如下:

对于新挂载操作,则调用do_new_mount接口进行挂载操作,进行超级块、根目录的dentry、根目录的inode变量的创建(针对根目录的dentry、inode创建,则是通过调各文件系统注册的mount接口实现,对devtmpfs而言,即为dev_mount,而dev_mount则最终调用shmem_fill_super进行根目录的dentry、inode的创建)。

 

 

对于devtmpfs,会挂载两次,在devtmpfs模块初始化时,在其处理线程中,通过调用sys_mount将其挂载至"/"目录下,其代码如下:

*err = sys_mount("devtmpfs", "/", "devtmpfs", MS_SILENT, options);

 

而在prepare_namespace接口中,通过调用devtmpfs_mount接口,将其挂载至/dev目录下,其代码如下:

/*

挂载devtmpfs_mount函数。

该接口主要调用sys_mount接口实现sys_mount接口的挂载。

目前该接口被prepare_namespace接口调用,进行devtmpfs接口的二次挂载操作,且挂载点为/dev目录。

*/

int devtmpfs_mount(const char *mntdir) { int err; if (!mount_dev) return 0; if (!thread) return 0; err = sys_mount("devtmpfs", (char *)mntdir, "devtmpfs", MS_SILENT, NULL); if (err) printk(KERN_INFO "devtmpfs: error mounting %i\n", err); else printk(KERN_INFO "devtmpfs: mounted\n"); return err; }

关于第一次挂载的具体意义,我目前还没有搞清楚。

 

根目录的inode的inode_ops接口如下所示,对于devtmpfs而言,且目录的inode节点的inode_ops均使用下面定义的接口函数。

static const struct inode_operations shmem_dir_inode_operations = { #ifdef CONFIG_TMPFS .create                = shmem_create, .lookup                = simple_lookup, .link                = shmem_link, .unlink                = shmem_unlink, .symlink        = shmem_symlink, .mkdir                = shmem_mkdir, .rmdir                = shmem_rmdir, .mknod                = shmem_mknod, .rename                = shmem_rename, #endif #ifdef CONFIG_TMPFS_XATTR .setxattr        = shmem_setxattr, .getxattr        = shmem_getxattr, .listxattr        = shmem_listxattr, .removexattr        = shmem_removexattr, #endif #ifdef CONFIG_TMPFS_POSIX_ACL .setattr        = shmem_setattr, #endif };

 

 

 

三、设备文件及目录创建对外接口

devtmpfs模块对外提供了devtmpfs_create_node接口,用于linux内核其他子模块调用,创建设备文件及目录

该接口的主要功能如下:

1.根据传递的struct device*类型的变量dev,获取要创建的文件的路径名;

2.创建struct req 类型的变量req,用于存储本次创建设备文件所需的参数;

3.将struct req 类型的变量req与全局变量requests链接;

4.调用wake_up_process,唤醒devtmpfsd线程,由devtmpfsd线程实现设备文件的创建操作

int devtmpfs_create_node(struct device *dev) { const char *tmp = NULL; struct req req; if (!thread) return 0; req.mode = 0; req.uid = GLOBAL_ROOT_UID;/*该文件所属的user*/ req.gid = GLOBAL_ROOT_GID;/*该文件所属的group*/     /*设置要创建文件的名称*/ req.name = device_get_devnode(dev, &req.mode, &req.uid, &req.gid, &tmp); if (!req.name) return -ENOMEM; if (req.mode == 0) req.mode = 0600;     /*设置设备文件的类型(字符设备/块设备)*/ if (is_blockdev(dev)) req.mode |= S_IFBLK; else req.mode |= S_IFCHR; req.dev = dev;     /*初始化req的完成量变量*/ init_completion(&req.done); spin_lock(&req_lock); req.next = requests; requests = &req; spin_unlock(&req_lock);     /*唤醒devtmpfsd线程,用于处理文件的创建*/ wake_up_process(thread);     /*等待文件创建完成*/ wait_for_completion(&req.done); kfree(tmp); return req.err; }  

 

四、设备文件及目录删除对外接口

devtmpfs模块对外提供了devtmpfs_delete_node接口,用于linux内核其他子模块调用,删除设备文件及目录

该接口与devtmpfs_create_node口类似,其主要功能如下:

1.根据传递的struct device*类型的变量dev,获取要创建的文件的路径名;

2.创建struct req 类型的变量req,用于存储本次删除设备文件所需的参数;

3.将struct req 类型的变量req与全局变量requests链接;

4.调用wake_up_process,唤醒devtmpfsd线程,由devtmpfsd线程实现设备文件的删除操作

int devtmpfs_delete_node(struct device *dev) { const char *tmp = NULL; struct req req; if (!thread) return 0; req.name = device_get_devnode(dev, NULL, NULL, NULL, &tmp); if (!req.name) return -ENOMEM; req.mode = 0; req.dev = dev; init_completion(&req.done); spin_lock(&req_lock); req.next = requests; requests = &req; spin_unlock(&req_lock); wake_up_process(thread); wait_for_completion(&req.done); kfree(tmp); return req.err; }  

 

 

五、devtmpfsd线程事务处理

 

该线程主要用于devtmpfs的挂载,以及设备文件或目录的创建与删除。其主要的功能如下(流程图如下所示):

1.调用sys_unshare,主要是不共享ns以及fs信息;

2.调用sys_mount挂载devtmpfs文件系统,且挂载点为根目录

3.将当前进程的当前目录设置为'/';

4.将当前进程的根目录设置为当前进程的当前目录

5.调用complete唤醒devtmpfs驱动的初始化线程,让初始化接口退出

6.处理节点的创建与删除节点的请求(req)

 

该线程在处理事务时涉及handle接口、handle_create、handle_remove、dev_rmdir、create_path、dev_mkdir、kern_path_create、do_path_lookup、path_lookupat接口的调用,本次会对这几个接口进行简要介绍说明

 

static int devtmpfsd(void *p) { char options[] = "mode=0755"; int *err = p; *err = sys_unshare(CLONE_NEWNS); if (*err) goto out; *err = sys_mount("devtmpfs", "/", "devtmpfs", MS_SILENT, options); if (*err) goto out;     /*设置进程的当前目录为根目录*/ sys_chdir("/.."); /* will traverse into overmounted root */     /*设置进程当前的目录为进程的根目录*/ sys_chroot(".");     /*唤醒setup_done完成量上所有等待的线程*/ complete(&setup_done);     /*该代码片段的作用为     1.处理requests上所有的req,并对每一个req,则调用handle进行处理;     2.处理完成后,调用complete唤醒相应等待的线程;     3.当所有的req均处理完成后,则让出本线程的调度权。     */ while (1) { spin_lock(&req_lock); while (requests) { struct req *req = requests; requests = NULL; spin_unlock(&req_lock); while (req) { struct req *next = req->next; req->err = handle(req->name, req->mode,   req->uid, req->gid, req->dev); complete(&req->done); req = next; } spin_lock(&req_lock); } __set_current_state(TASK_INTERRUPTIBLE); spin_unlock(&req_lock); schedule(); } return 0; out: complete(&setup_done); return *err; }

 

 

设备文件及目录的创建接口

devtmpfsd线程调用handle_create进行设备文件及目录的创建。

针对handle_create而言,其处理流程如下,其功能如下:

1.调用kern_path_create用于创建设备文件的dentry接口;

2.若文件目录,则调用create_path(通过调用dev_mkdir),实现目录的dentry与inode节点的创建。

 

 

设备目录的创建

设备目录通过调用dev_mkdir接口实现目录的dentry与inode的创建,该接口实现的功能如下:

1.首先调用kern_path_create进行dentry的创建;

2.若dentry创建成功,则vfs_mkdir创建目录的inode,该接口最终调用dir->i_op->mkdir进行目录inode进行的创建(shmem_mkdir->shmem_mknod进行inode创建的操作,传递的创建模式包含为S_IFDIR,即创建目录,

    则设置inode->i_op=shmem_dir_inode_operations;inode->i_fop = &simple_dir_operations)

static int dev_mkdir(const char *name, umode_t mode) { struct dentry *dentry; struct path path; int err;     /*调用kern_path_create对*/ dentry = kern_path_create(AT_FDCWD, name, &path, LOOKUP_DIRECTORY); if (IS_ERR(dentry)) return PTR_ERR(dentry); err = vfs_mkdir(path.dentry->d_inode, dentry, mode); if (!err) /* mark as kernel-created inode */ dentry->d_inode->i_private = &thread; done_path_create(&path, dentry); return err; }

 

 

设备文件及目录的删除接口

devtmpfsd线程调用handle_remove进行设备文件及目录的创建。

该接口通过调用vfs_unlink,实现节点的删除操作;

通过调用dput,实现dentry的删除操作。

*/

static int handle_remove(const char *nodename, struct device *dev) { struct path parent; struct dentry *dentry; int deleted = 1; int err;     /*获取要删除路径对应dentry*/ dentry = kern_path_locked(nodename, &parent); if (IS_ERR(dentry)) return PTR_ERR(dentry);     /*若该dentry存在inode节点,则调用vfs_unlink进行删除操作(即调用shmem_rmdir进行删除操作)。*/ if (dentry->d_inode) { struct kstat stat; struct path p = {.mnt = parent.mnt, .dentry = dentry}; err = vfs_getattr(&p, &stat); if (!err && dev_mynode(dev, dentry->d_inode, &stat)) { struct iattr newattrs; /*  * before unlinking this node, reset permissions  * of possible references like hardlinks  */ newattrs.ia_uid = GLOBAL_ROOT_UID; newattrs.ia_gid = GLOBAL_ROOT_GID; newattrs.ia_mode = stat.mode & ~0777; newattrs.ia_valid = ATTR_UID|ATTR_GID|ATTR_MODE; mutex_lock(&dentry->d_inode->i_mutex); notify_change(dentry, &newattrs); mutex_unlock(&dentry->d_inode->i_mutex); err = vfs_unlink(parent.dentry->d_inode, dentry); if (!err || err == -ENOENT) deleted = 1; } } else { err = -ENOENT; } dput(dentry); mutex_unlock(&parent.dentry->d_inode->i_mutex); path_put(&parent); if (deleted && strchr(nodename, '/')) delete_path(nodename); return err; }

设备目录的删除

该接口通过调用vfs_rmdir接口进行目录的删除,最终调用shmem_rmdir接口实现目录的删除操作

 

*/

static int dev_rmdir(const char *name) { struct path parent; struct dentry *dentry; int err; dentry = kern_path_locked(name, &parent); if (IS_ERR(dentry)) return PTR_ERR(dentry); if (dentry->d_inode) { if (dentry->d_inode->i_private == &thread) err = vfs_rmdir(parent.dentry->d_inode, dentry); else err = -EPERM; } else { err = -ENOENT; } dput(dentry); mutex_unlock(&parent.dentry->d_inode->i_mutex); path_put(&parent); return err; }

 

 

kern_path_create接口分析

该接口用于设备文件及目录dentry的查找或创建:

1.调用do_path_lookup完成路径查找,其中LOOKUP_PARENT表示仅查找到最后一级路径的父路径的dentry

2.若最后一级路径的类型不是LAST_NORM,说明没有完成路径的查找操作,返回失败

3.调用lookup_hash进行最后一级路径的查找,若路径不存在,则创建相应的dentry;若存在则返回dentr。

4.若本次是文件查找,且最后一级路径是目录,则返回失败。

该接口主要调用path_lookupat进行路径的查找,本次对该接口进行分析下。

struct dentry *kern_path_create(int dfd, const char *pathname, struct path *path, unsigned int lookup_flags) { struct dentry *dentry = ERR_PTR(-EEXIST); struct nameidata nd; int err2; int error; bool is_dir = (lookup_flags & LOOKUP_DIRECTORY); /*  * Note that only LOOKUP_REVAL and LOOKUP_DIRECTORY matter here. Any  * other flags passed in are ignored!  */ lookup_flags &= LOOKUP_REVAL;     /*调用do_path_lookup完成路径查找,其中LOOKUP_PARENT表示仅查找到最后一级路径的父路径的dentry*/ error = do_path_lookup(dfd, pathname, LOOKUP_PARENT|lookup_flags, &nd); if (error) return ERR_PTR(error); /*  * Yucky last component or no last component at all?  * (foo/., foo/.., /)  */ /*若最后一级路径的类型不是LAST_NORM,说明没有完成路径的查找操作,返回失败*/ if (nd.last_type != LAST_NORM) goto out; nd.flags &= ~LOOKUP_PARENT; nd.flags |= LOOKUP_CREATE | LOOKUP_EXCL; /* don't fail immediately if it's r/o, at least try to report other errors */ err2 = mnt_want_write(nd.path.mnt); /*  * Do the final lookup.  */ mutex_lock_nested(&nd.path.dentry->d_inode->i_mutex, I_MUTEX_PARENT);     /*调用lookup_hash,首先在父目录的dcache中查找子dentry,若查找到则返回;     若没有查找到,则基于最后一级路径的名称,创建一个dentry,并返回该dentry*/ dentry = lookup_hash(&nd); if (IS_ERR(dentry)) goto unlock; error = -EEXIST; if (dentry->d_inode) goto fail; /*  * Special case - lookup gave negative, but... we had foo/bar/  * From the vfs_mknod() POV we just have a negative dentry -  * all is fine. Let's be bastards - you had / on the end, you've  * been asking for (non-existent) directory. -ENOENT for you.  */ if (unlikely(!is_dir && nd.last.name[nd.last.len])) { error = -ENOENT; goto fail; } if (unlikely(err2)) { error = err2; goto fail; } *path = nd.path; return dentry; fail: dput(dentry); dentry = ERR_PTR(error); unlock: mutex_unlock(&nd.path.dentry->d_inode->i_mutex); if (!err2) mnt_drop_write(nd.path.mnt); out: path_put(&nd.path); return dentry; }

path_lookupat接口分析

该接口通过调用link_path_walk、follow_link、walk_component接口实现路径查找操作,而这些接口,在之前的文章中已经分析过,此处不再分析。

 

 

 

六、设备驱动与VFS之间的关联以及处理等

针对设备节点创建时,则会根据创建文件的类型(块设备、字符设备),设置inode节点的额i_fops调用接口,这样在进行系统调用open时(针对open系统调用的分析,已经在前几篇文章中分析过,需要了解的可查看前几张的介绍),实现对块设备或者字符设备的打开操作。

在上面几个章节的介绍中,我们知道针对文件的inode节点创建时,通过调用接口shmem_mknod接口(实际由shmem_get_inode)实现的,在shmem_get_inode的定义如下:

该接口通过调用init_special_inode,实现对块设备或字符设备的i_fops接口的赋值。

static struct inode *shmem_get_inode(struct super_block *sb, const struct inode *dir,      umode_t mode, dev_t dev, unsigned long flags) { struct inode *inode; struct shmem_inode_info *info; struct shmem_sb_info *sbinfo = SHMEM_SB(sb); if (shmem_reserve_inode(sb)) return NULL; inode = new_inode(sb); if (inode) { inode->i_ino = get_next_ino(); inode_init_owner(inode, dir, mode); inode->i_blocks = 0; inode->i_mapping->backing_dev_info = &shmem_backing_dev_info; inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME; inode->i_generation = get_seconds(); info = SHMEM_I(inode); memset(info, 0, (char *)inode - (char *)info); spin_lock_init(&info->lock); info->flags = flags & VM_NORESERVE; INIT_LIST_HEAD(&info->swaplist); simple_xattrs_init(&info->xattrs); cache_no_acl(inode); switch (mode & S_IFMT) { default: inode->i_op = &shmem_special_inode_operations; init_special_inode(inode, mode, dev); break; case S_IFREG: inode->i_mapping->a_ops = &shmem_aops; inode->i_op = &shmem_inode_operations; inode->i_fop = &shmem_file_operations; mpol_shared_policy_init(&info->policy,  shmem_get_sbmpol(sbinfo)); break; case S_IFDIR: inc_nlink(inode); /* Some things misbehave if size == 0 on a directory */ inode->i_size = 2 * BOGO_DIRENT_SIZE; inode->i_op = &shmem_dir_inode_operations; inode->i_fop = &simple_dir_operations; break; case S_IFLNK: /*  * Must not load anything in the rbtree,  * mpol_free_shared_policy will not be called.  */ mpol_shared_policy_init(&info->policy, NULL); break; } } else shmem_free_inode(sb); return inode; }

 

init_special_inode接口分析

该接口的定义如下:

1.针对字符设备,其i_fops为def_chr_fops;

2.针对块设备,其i_fops为def_blk_fops。

3.设置inode->i_rdev,与设备变量进行关联,以便获取设备驱动的ops接口

 

void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev) { inode->i_mode = mode; if (S_ISCHR(mode)) { inode->i_fop = &def_chr_fops; inode->i_rdev = rdev; } else if (S_ISBLK(mode)) { inode->i_fop = &def_blk_fops; inode->i_rdev = rdev; } else if (S_ISFIFO(mode)) inode->i_fop = &pipefifo_fops; else if (S_ISSOCK(mode)) inode->i_fop = &bad_sock_fops; else printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"   " inode %s:%lu\n", mode, inode->i_sb->s_id,   inode->i_ino); }

在系统调用open的分析中,我们知道open接口最终调用do_dentry_open接口,获取inode->i_fops指针,并与struct file变量的f_op关联,接着调用inode->i_fops->open,完成open操作。下面我们看下块设备和字符设备的默认open接口。

字符设备的open接口

该接口主要用与将设备文件注册的文件操作接口赋值给struct file变量的f_op指针,其具体操作如下:

1.通过inode->r_dev,获取设备变量cdev;

2.将cdev的文件操作函数指针ops赋值为f_op

3.调用具体文件的open函数。

static int chrdev_open(struct inode *inode, struct file *filp) { struct cdev *p; struct cdev *new = NULL; int ret = 0; spin_lock(&cdev_lock);     /*根据inode->i_cdev,获取设备文件的cdev指针*/ p = inode->i_cdev;     /*若inode->i_cdev为空,则通过inode->r_dev变量,借助kobj_lookup获取其cdev变量*/ if (!p) { struct kobject *kobj; int idx; spin_unlock(&cdev_lock); kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx); if (!kobj) return -ENXIO; new = container_of(kobj, struct cdev, kobj); spin_lock(&cdev_lock); /* Check i_cdev again in case somebody beat us to it while    we dropped the lock. */ p = inode->i_cdev; if (!p) { inode->i_cdev = p = new; list_add(&inode->i_devices, &p->list); new = NULL; } else if (!cdev_get(p)) ret = -ENXIO; } else if (!cdev_get(p)) ret = -ENXIO; spin_unlock(&cdev_lock); cdev_put(new); if (ret) return ret; ret = -ENXIO;     /*重新设置filp->f_op指针,指向具体设备的ops指针(设备的ops指针在设备驱动中执行cdev_init接口时设置)*/ filp->f_op = fops_get(p->ops); if (!filp->f_op) goto out_cdev_put;     /*然后执行相应设备文件的open操作,并返回操作结果*/ if (filp->f_op->open) { ret = filp->f_op->open(inode, filp); if (ret) goto out_cdev_put; } return 0;  out_cdev_put: cdev_put(p); return ret; }  

块设备的open接口

 

 

 

七、创建inode节点的方式

针对创建设备文件有两种方式,一种是通过系统命令mknod实现,第二种是通过在设备驱动中调用devtmpfs的devtmpfs_create_node接口实现。

1.mknod方式

其用法如下

mknod [选项]... 名称 类型 [主设备号 次设备号]

 

2.devtmpfs_create_node接口调用方式

目前通过在设备驱动中,调用device_create接口,即可实现设备文件的创建,无需在应用层执行mknod命令,针对device_create接口,其调用流程如下所示,在device_create_file中,实现设备文件的创建操作。

 

 

至此,完成了设备文件系统的分析操作

最新回复(0)