在之前几篇文章中,我们介绍了文件系统的注册、超级块的创建、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中,实现设备文件的创建操作。

 

 

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

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐