字符设备管理机制分析(二)

 

       2.2 添加字符设备实例

              添加设备实例和其他的设备添加一样,都是调用函数device_add()来实现。

              以input子系统中添加event设备为例:@ kernel/driver/input/evdev.c  --

evdev_connect()函数中。

 

static int evdev_connect(struct input_handler *handler, struct input_dev *dev,

                      const struct input_device_id *id)

{

       struct evdev *evdev;

       int minor;

       int error;

 

       for (minor = 0; minor < EVDEV_MINORS; minor++) // 寻找可用次设备号

              if (!evdev_table[minor])

                     break;

      

       // 分配一个struct evdev结构体的内存空间并初始化

       evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL);

      

       dev_set_name(&evdev->dev, "event%d", minor); // dev对应的kobject的名字

       …

       evdev->minor = minor;

evdev->dev.devt = MKDEV(INPUT_MAJOR, EVDEV_MINOR_BASE + minor);

// 主次设备号

       evdev->dev.class = &input_class;

       evdev->dev.parent = &dev->dev; // input_dev.dev作为其父设备

       evdev->dev.release = evdev_free;

       device_initialize(&evdev->dev);  // dev其余项初始化

       …

       device_add(&evdev->dev); 

// 调用这个函数向系统注册一个设备实例,并创建对应的设备节点

       …

}

device_add()

à devtmpfs_create_node()  //创建设备节点

à device_get_devnode()

à vfs_mknod()

       à dir->i_op->mknod(dir, dentry, mode, dev)

       // 最后调用上级目录对应的inode节点的操作集中的mknod函数来创建一个设备节点文件。

             

              input_class结构体定义如下:

              struct class input_class = {

                     .name            = "input",

                     .devnode = input_devnode,

};

static char *input_devnode(struct device *dev, mode_t *mode)

{

       return kasprintf(GFP_KERNEL, "input/%s", dev_name(dev));

}

// 这个函数返回去的结果应该是(例子):input/event0之类的字符串

 

int devtmpfs_create_node(struct device *dev)

{

       …

       const char *nodename;

       mode_t mode = 0;

       struct nameidata nd;

       struct dentry *dentry;

       int err;

      

       nodename = device_get_devnode(dev, &mode, &tmp);

// 获取设备节点名,input/event0

       …

       if (mode == 0)

              mode = 0600;

       if (is_blockdev(dev)) // 是否是块设备

              mode |= S_IFBLK;

       else

              mode |= S_IFCHR;

      

       err = vfs_path_lookup(dev_mnt->mnt_root, dev_mnt,

                           nodename, LOOKUP_PARENT, &nd);

       if (err == -ENOENT) { // No such file or directory

              /* create missing parent directories */

              create_path(nodename);

              err = vfs_path_lookup(dev_mnt->mnt_root, dev_mnt,

                                  nodename, LOOKUP_PARENT, &nd);

              if (err)

                     goto out;

                     }

/* 如果/dev目录下不存在input目录的话,那么就会调用函数create_path函数来创建这个input目录 */

       ++++++++ 建议先看下后面的关于devtmpfs的分析 +++++++++++

       /* 如果nodename的路径在devtmpfs中不全,那么就进入这个if分支调用函

数create_path进行创建,ok之后再次调用函数vfs_path_lookup来搜索一下, 这个时候的err应该是0才对。*/

      

       dentry = lookup_create(&nd, 0); //这里传入0值,表明布什目录,而是一个文件

       if (!IS_ERR(dentry)) {

              int umask;

              umask = sys_umask(0000);

              err = vfs_mknod(nd.path.dentry->d_inode, dentry,  mode,  dev->devt);  

// mknod 创建设备节点,如下

              sys_umask(umask);

              /* mark as kernel created inode */

              if (!err)

                     dentry->d_inode->i_private = &dev_mnt;

              dput(dentry);

       } else {

              err = PTR_ERR(dentry);

       }

       …

}

int vfs_mknod(struct inode *dir, struct dentry *dentry, int mode, dev_t dev)

{

       int error = may_create(dir, dentry); // 权限检查

       if (error)

              return error;

      

       if ((S_ISCHR(mode) || S_ISBLK(mode)) && !capable(CAP_MKNOD))

              return -EPERM;

       if (!dir->i_op->mknod)

              return -EPERM;

       …

       vfs_dq_init(dir);

       error = dir->i_op->mknod(dir, dentry, mode, dev);

       /* 在看了下面关于devtmpfs的分析之后,应该有一个认识就是,不管是创建

根目录还是其中的子目录,都是会调用到函数shmem_get_inode()来初始化目录对应的inode节点的。所以不管这里的dir所表示的根目录还是子目录,其对应的inode操作集都是一样的:shmem_dir_inode_operations ,这个结构体中定义的mknod函数是shmem_mknod()。所以这里调用到的函数就是shmem_mknod()*/

       if (!error)

              fsnotify_create(dir, dentry);

       return error;

}

@ kernel/mm/shmem.c

static int shmem_mknod(struct inode *dir, struct dentry *dentry, int mode, dev_t dev)

{

       struct inode *inode;

       int error = -ENOSPC;

       /* 这个函数是关键的函数,部分源码已经在后面的devtmpfs中列出

就是在函数shmem_get_inode() à init_special_inode()中实现了将字符设备默认的文件操作集合设备节点搭上了关系。 */

       inode = shmem_get_inode(dir->i_sb, mode, dev, VM_NORESERVE);

       if (inode) {

              …

              dir->i_size += BOGO_DIRENT_SIZE;

              dir->i_ctime = dir->i_mtime = CURRENT_TIME;

              d_instantiate(dentry, inode);

              …

       }

       …

}

好了,到现在添加一个字符设备实例中创建字符设备节点的过程就这样完了,接下来我们继续聊一聊open一个设备节点的时候都是如何进行的。

++++++++++++++++++++++++    devtmpfs    ++++++++++++++++

devtmp文件系统的初始化在函数driver_init() à devtmpfs_init()中进行的,在该函数中注册文件系统类型,并在内存中建立devtmpfs的雏形。当然不会是在这里进行mount到用户空间的,而是在init用户进程中做的。

建立devtmpfs的雏形的函数是kern_mount_data(),这个函数所做的工作和sysfs或者rootfs类型的初始化时候类似,都是构造超级块和构造根目录对应的dentry结构体等一些重要内容。

devtmpfs的超级块构造函数实际上调用的函数是shmem_fill_super,于是我们可以想到和devtmpfs相关的操作几乎都是使用的和shmem相关的操作。具体源码文件位置:kernel/mm/shmem.c。

@ kernel/drivers/basedevtmpfs.c

static struct file_system_type dev_fs_type = {

       .name = "devtmpfs",

       .get_sb = dev_get_sb,

       .kill_sb = kill_litter_super,

};

获取超级块函数dev_get_sb()会调用超级块填充函数shmem_fill_super(),这个函数最后会shmem_get_inode()和d_alloc_root()来初始化devtmpsfs的根目录的inode和dentry结构体。如下:

inode = shmem_get_inode(sb, S_IFDIR | sbinfo->mode, 0, VM_NORESERVE);

root = d_alloc_root(inode);

@ kernel/mm/shmem.c

static struct inode *shmem_get_inode(struct super_block *sb, int mode,

                                   dev_t dev, unsigned long flags)

{

       …

       inode = new_inode(sb);

       if (inode) {

              …

              switch (mode & S_IFMT) {

              default:  // 除了file、dir、link类型的文件都会在这条分支处理

                     inode->i_op = &shmem_special_inode_operations;

                     init_special_inode(inode, mode, dev);  // 特殊文件的inode初始化

                     break;

              case S_IFREG:  // file

                     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);  // 增加一下inode的引用计数

                     inode->i_size = 2 * BOGO_DIRENT_SIZE;

                     inode->i_op = &shmem_dir_inode_operations;

                     inode->i_fop = &simple_dir_operations;

                     break;

              case S_IFLNK:

                     mpol_shared_policy_init(&info->policy, NULL);

                     break;

              }

       } else

       …

}

下面来看看针对特殊文件的inode初始化的函数:@ kernel/fs/inode.c

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;

 // 很眼熟吧,文件char_dev.c中定义。下面的分别是块设备,fifo

// 文件,socket文件的inode初始化。

              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 = &def_fifo_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);

}

@ kernel/fs/char_dev.c

const struct file_operations def_chr_fops = {

       .open = chrdev_open,

};

 

在devtmpfs中创建目录,这里还是可以用上面的例子来分析,为了创建input/event0这个设备节点,假如在这里之前/dev/input目录还不存在。

上面加到了函数devtmpfs_create_node()中调用vfs_path_lookup()来查找路径input/event0,这里值得提到的是:flag为LOOKUP_PARENT,表示不会搜索最后的结点,也就是event0。一个例子:

err = vfs_path_lookup(dev_mnt->mnt_root, dev_mnt,

                           nodename, LOOKUP_PARENT, &nd);

第一个参数就是devtmpfs的根目录dentry结构体指针;第二个参数是devtmpfs的挂载点结构体指针;第三个参数是一个字符串,表示的是/dev目录下的相对路径;第四个参数LOOKUP_PARENT正如上面所说,表示不会搜索nodename字符串中通过“/”分隔开的各个结点(也可以叫做dentry目录项);第四个参数是专门用在路径搜索中的辅助结构体nameidata结构体,用来表示搜索过程中最后一个有效存在的目录项的数据(包括路径,类型,权限等信息,可以是目录,也可以是文件)。

下面是nodename的三个例子,都是/dev目录下的。

1.      alarm

2.      input/event0

3.      graphics/lcm/fb0 (实际中没有lcm这个目录,这里为了分析方便)

对于第一种情况,vfs_path_lookup()已经遇到了最后一个目录项,所以会直接返回,同时会将根目录这个目录项填充到nameidata结构体中返回;第二种情况,如果是第一个input设备节点创建,那么此时/dev下还没有input目录,所以,这个时候err会返回-ENOENT,同时也是将根目录这个目录项填充到nameidata结构体中返回;第三种情况,假如graphics已经存在了,lcm不存在,那么也是会返回err(-ENOENT),这个时候nameidata对于的目录项则是graphics。

下面是devtmpfs中创建目录的函数:

static int create_path(const char *nodepath)

{

       char *path;

       struct nameidata nd;

       int err = 0;

      

       path = kstrdup(nodepath, GFP_KERNEL);

       …

       err = vfs_path_lookup(dev_mnt->mnt_root, dev_mnt,

                           path, LOOKUP_PARENT, &nd);

       if (err == 0) { // path表示一个目录,而且其所有父目录都存在了

              struct dentry *dentry;

              /* create directory right away */

              dentry = lookup_create(&nd, 1); // 得到一个dentry结构体,对nd表示的父

// 目录需要进行权限判定。

              if (!IS_ERR(dentry)) {

err = vfs_mkdir(nd.path.dentry->d_inode, dentry, 0755); // 创建一个新

// 目录,第一个参数父目录对于的inode节点,第二个是新得到的

// dentry结构体,第三个是新建目录的权限。

                     dput(dentry);

              }

              mutex_unlock(&nd.path.dentry->d_inode->i_mutex);

              path_put(&nd.path);

       } else if (err == -ENOENT) {// path表示一个目录或者文件,而且其所有父目录

// 至少有一个不存在,需要被创建。

              char *s;

              /* parent directories do not exist, create them */

              s = path;

              while (1) { // 在循环中创建所有不存在的父目录,LOOKUP_PARENT

                     s = strchr(s, '/');  // 找目录项分隔符号

                     if (!s)

                            break;    // 如果到了最后一个目录项,则跳出循环

                     s[0] = '\0';

                     err = dev_mkdir(path, 0755);

                     if (err && err != -EEXIST)

                            break; // 出错退出

                     s[0] = '/';

                     s++;

              /*

              假如此时graphics/lcm/fb0在/dev下面都不存在,现在要来一级一级得穿

件前面两个目录,graphics/lcm/。在第一次调用dev_mkdir的时候传递的

path= graphics,第二次传递的path则是graphics/lcm。当然这个循环到第三次的时候因为没有搜索到/字符而退出。

*/

}

       }

      

       kfree(path);

       return err;

}

// 下面这个函数和上面函数的if (err == 0)分支几乎一样。

static int dev_mkdir(const char *name, mode_t mode)

{

       struct nameidata nd;

       struct dentry *dentry;

       int err;

       // 再次搜索待创建目录的父目录以上是否存在

       err = vfs_path_lookup(dev_mnt->mnt_root, dev_mnt,

                           name, LOOKUP_PARENT, &nd);

       if (err) // 这里应该是需要返回0才对,LOOKUP_PARENT表示不会搜索最后

// 一个目录项

              return err;

      

       dentry = lookup_create(&nd, 1); // 1表示创建的目录项是用来表示一个目录

       if (!IS_ERR(dentry)) {

              err = vfs_mkdir(nd.path.dentry->d_inode, dentry, mode); // 同上

              dput(dentry);

       } else {

              err = PTR_ERR(dentry);

       }

       mutex_unlock(&nd.path.dentry->d_inode->i_mutex);

 

       path_put(&nd.path);

       return err;

}

int vfs_mkdir(struct inode *dir, struct dentry *dentry, int mode)

{ // dir表示的是父目录的inode,对于devtmpfs根目录的inode是在初始化devtmpfs

// 的时候已经构建好了,所以这里可以直接通过这个inode来创建目录。

       int error = may_create(dir, dentry);  // 权限检查

 

       if (error)

              return error;

 

       if (!dir->i_op->mkdir) // 父目录对于的inode的inode操作集的mkdir是否存在。

              return -EPERM;

 

       mode &= (S_IRWXUGO|S_ISVTX);

       error = security_inode_mkdir(dir, dentry, mode);

       if (error)

              return error;

 

       vfs_dq_init(dir);

       error = dir->i_op->mkdir(dir, dentry, mode);

       /*

       我们可以从超级块填充函数中找到devtmpfs的根目录的inode初始化时候这个inode操作集是如何初始化的。参考前面的shmem_get_inode()函数可以看出,目录类型的inode初始化如下:

case S_IFDIR:

                     inc_nlink(inode);  // 增加一下inode的引用计数

                     inode->i_size = 2 * BOGO_DIRENT_SIZE;

                     inode->i_op = &shmem_dir_inode_operations;

                     inode->i_fop = &simple_dir_operations;

                     break;

       static const struct inode_operations shmem_dir_inode_operations = {

              .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,

}

所以看得出来mkdir的最终地调用的函数是shmem_mkdir()

*/

       if (!error)

              fsnotify_mkdir(dir, dentry);

       return error;

}

static int shmem_mkdir(struct inode *dir, struct dentry *dentry, int mode)

{

       int error;

 

       if ((error = shmem_mknod(dir, dentry, mode | S_IFDIR, 0)))

              return error;

       inc_nlink(dir);

       return 0;

}

可以看出最终mkdir调用的函数还是shmem_mknod,只是传递的参数稍有不同罢了。shmem_mknod()函数中会调用函数shmem_get_inode()来初始化一个新的inode节点,在这里就会针对是dir还是file还是其他特殊的设备文件来做inode的初始化。后面在讲创建设备节点的时候将会再次提到shmem_mknod这个函数。

++++++++++++++++++++++++   devtmpfs   ++++++++++++++++

 

2.3 open一个字符设备节点

我们open一个字符设备节点的时候,从系统调用open网内核深处走,依次经过了如下主要的函数调用:sys_open() à do_sys_open() à do_filp_open() à nameidata_to_filp() à __dentry_open()。

static struct file *__dentry_open(struct dentry *dentry, struct vfsmount *mnt,

                                   int flags, struct file *f,

                                   int (*open)(struct inode *, struct file *),

                                   const struct cred *cred)

{

       struct inode *inode;

       int error;

       …

       inode = dentry->d_inode;

       …

       f->f_mapping = inode->i_mapping;

       f->f_path.dentry = dentry;

       f->f_path.mnt = mnt;

       f->f_pos = 0;

       f->f_op = fops_get(inode->i_fop); // 获取def_char_fops文件操作集

       file_move(f, &inode->i_sb->s_files);

       …

       if (!open && f->f_op)

              open = f->f_op->open;

if (open) {

              error = open(inode, f);  // 调用函数def_char_fops. chrdev_open()

              if (error)

                     goto cleanup_all;

       }

       …

}

@ kernel/fs/char_dev.c

static int chrdev_open(struct inode *inode, struct file *filp)

{

       struct cdev *p;

       struct cdev *new = NULL;

       int ret = 0;

 

       spin_lock(&cdev_lock);

       p = inode->i_cdev;

       if (!p) { // p刚开始为NULL

              struct kobject *kobj;

              int idx;

              spin_unlock(&cdev_lock);

              kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);

              // 从cdev_map这个 hash table中找到设备节点对应的字符设备类型描述

// 结构体cdev中的kobject结构体。

              if (!kobj)

                     return -ENXIO;

              new = container_of(kobj, struct cdev, kobj); // 得到cdev结构体

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

                     // cdev中的list的链表用来链接所有打开的同类型设备节点的inode

                     new = NULL;

              } else if (!cdev_get(p))

                     ret = -ENXIO;

       } else if (!cdev_get(p))

              ret = -ENXIO;

       …

       /* 所有的字符设备节点文件在devtmpfs中都是具有相同的file操作集:

def_char_fops,  然后在def_char_fopd.chrdev_open()函数中通过字符设备的管

理机制找到设备节点对应的字符设备类型,最后再次通过fops_get函数得到真

正的针对这种字符设备文件所特有的file操作集,也就是下面语句实现的内容:*/

       ret = -ENXIO;

       filp->f_op = fops_get(p->ops); // 这里再次获得

       …

       if (filp->f_op->open) {

              ret = filp->f_op->open(inode,filp);

/*

调用字符设备文件对应的字符设备类型所描述的file操作集中的具有实际意义的open函数,以input字符设备来说,那就是input_open_file()函数。如下:

static const struct file_operations input_fops = {

       .owner = THIS_MODULE,

       .open = input_open_file,

};

这个input_fops是在input_init() –>       register_chrdev(INPUT_MAJOR, "input", &input_fops); 注册字符设备类型时带进去的file操作集。

*/

              if (ret)

                     goto out_cdev_put;

       }

       …

}

 

 

参考资料:

1.      linux字符cdev和inode的联系

http://linux.chinaitlab.com/administer/820298.html

2.      open系统调用在内核中的流程分析

http://linux.chinaunix.net/techdoc/develop/2008/12/18/1053790.shtml

3.      linux文件系统之路径查找与文件系统的挂载

http://edu.codepub.com/2010/0829/25476.php

4.      source code

字符设备、devtmps、shmem等相关文件,具体见文中。

你可能感兴趣的:(struct,File,Class,input,Path,symlink)