一、介绍
本文主要介绍设备文件/dev/dri/cardxx、/dev/dri/renderDxx的创建与open内核流程。分析的内核代码版本为linux-5.4.191。
二、创建DRM设备文件
pci probe到GPU设备后,会调用函数devtmpfs_create_node将创建设备文件(或称为设备结点)的请求加入链表,并唤醒内核线程kdevtmpfs,最终创建设备文件由内核线程kdevtmpfs完成,pci probe相关调用流程如下:
pci probe到GPU设备
-》drm_dev_alloc
-》drm_dev_init
-》drm_minor_alloc
-》drm_sysfs_minor_alloc
-》minor_str = "renderD%d";/minor_str = "card%d";
-》kdev->class = drm_class; drm_class->devnode被设置为函数drm_devnode,具体见函数drm_sysfs_init
-》dev_set_name(kdev, minor_str, minor->index);
-》drm_dev_register
-》drm_minor_register
-》device_add
-》devtmpfs_create_node
-》req.name = device_get_devnode 将请求创建的结点名字设置为"dri/renderDxx"或"dri/cardxx"
-》dev->class->devnode 即调用函数drm_devnode,该函数返回字符串"dri/renderDxx"或"dri/cardxx"
-》req.next = requests;
requests = &req; 将创建设备结点请求加入链表
-》wake_up_process 唤醒内核线程kdevtmpfs
内核启动初始化时,会调用函数driver_init->devtmpfs_init,函数devtmpfs_init的相关工作如下:
- 调用vfs_kern_mount mount devtmpfs,根据下面的流程可知,创建的devtmpfs基于shmem,函数shmem_fill_super创建了超级块,且超级块的成员s_root(类型为struct dentry *,指devtmpfs文件系统的根路径的目录项)的inode操作被设置为shmem directory的操作(shmem_dir_inode_operations)。
- 调用kthread_run创建内核线程kdevtmpfs。
static struct file_system_type internal_fs_type = {
.name = "devtmpfs",
#ifdef CONFIG_TMPFS
.init_fs_context = shmem_init_fs_context,
.parameters = &shmem_fs_parameters,
#else
.init_fs_context = ramfs_init_fs_context,
.parameters = &ramfs_fs_parameters,
#endif
.kill_sb = kill_litter_super,
};
static const struct fs_context_operations shmem_fs_context_ops = {
.free = shmem_free_fc,
.get_tree = shmem_get_tree,
#ifdef CONFIG_TMPFS
.parse_monolithic = shmem_parse_options,
.parse_param = shmem_parse_one,
.reconfigure = shmem_reconfigure,
#endif
};
driver_init
-》devtmpfs_init
-》vfs_kern_mount(&internal_fs_type, 0, "devtmpfs", opts);
-》fs_context_for_mount
-》alloc_fs_context
-》fc->fs_type = get_filesystem(fs_type);
-》init_fs_context = fc->fs_type->init_fs_context; fc->fs_type->init_fs_context即 shmem_init_fs_context
-》ret = init_fs_context(fc); 即调用函数 shmem_init_fs_context
-》fc->ops = &shmem_fs_context_ops;
-》fc_mount(fc);
-》vfs_get_tree
-》fc->ops->get_tree(fc); 调用shmem_fs_context_ops.get_tree即函数 shmem_get_tree
-》get_tree_nodev(fc, shmem_fill_super);
-》vfs_get_super
-》err = fill_super(sb, fc); fill_super函数指针指向shmem_fill_super,即调用 shmem_fill_super
-》inode = shmem_get_inode(sb, NULL, S_IFDIR | sbinfo->mode, 0, VM_NORESERVE);
-》 inode->i_op = &shmem_dir_inode_operations;
inode->i_fop = &simple_dir_operations;
-》sb->s_root = d_make_root(inode);
-》kthread_run(devtmpfsd, &err, "kdevtmpfs"); 创建内核线程kdevtmpfs
接着分析内核线程kdevtmpfs被唤醒后的处理流程,首先从请求链表中取出创建设备结点的请求,最终调用函数vfs_mknod创建设备结点,其中dir->i_op(其中dir的类型为struct inode*)就是上面fill_super中创建的inode的成员i_op即shmem_dir_inode_operations,最终创建的设备结点文件对应的inode的成员i_fop被设置为&def_chr_fops、成员i_rdev被设置为device id,调用流程如下:
devtmpfsd 内核线程kdevtmpfs入口
-》被唤醒后从请求链表取出请求并调用函数 handle
-》handle_create
-》kern_path_create 创建目录/dev/dri
-》vfs_mknod 创建设备结点
-》dir->i_op->mknod 即shmem_dir_inode_operations->mknod 即函数 shmem_mknod
-》shmem_get_inode 创建inode,并初始化inode的成员
-》inode->i_op = &shmem_special_inode_operations;
-》init_special_inode(inode, mode, dev);
-》inode->i_fop = &def_chr_fops; 设置inode成员i_fop为&def_chr_fops
-》inode->i_rdev = rdev; 用major和minor初始化成员
-》security_inode_init_security
三、open DRM设备文件
drm模块加载的时候,模块初始化函数drm_core_init调用函数__register_chrdev主要完成如下工作:
- 调用__register_chrdev_region注册major id为226、minior id为0到255的字符设备区域,以标记该major id、minor id范围已经被占用。
- 调用函数cdev_alloc分配字符设备并设置字符设备的成员ops(类型为struct file_operations *)为&drm_stub_fops。
- 将包含major id为226、minor id为0到255、字符设备地址等信息的probe加入cdev_map,后面就可以通过inode->i_rdev(由major id、minor id组成)去cdev_map中查找字符设备(cdev)。
具体流程如下:
drm_core_init
-》register_chrdev(DRM_MAJOR, "drm", &drm_stub_fops); DRM_MAJOR等于226
-》__register_chrdev(major, 0, 256, name, fops);
-》__register_chrdev_region
-》cdev = cdev_alloc();
-》cdev->ops = fops; fops等于&drm_stub_fops
-》cdev_add
-》kobj_map(cdev_map,...) 将包含major id为226、minor id为0到255、字符设备地址等信息的probe加入
cdev_map,后面就可以通过inode->i_rdev去cdev_map中查找字符设备(cdev)
module_init(drm_core_init);
用户态程序调用open后对应的内核系统调用如下:
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
do_sys_open函数的主要工作如下:
- 调用函数link_path_walk将路径(/dev/dri/cardxx、/dev/dri/renderDxx)转换为最终的目录项dentry,通过目录项就可以得到inode。
- 语句f->f_op = fops_get(inode->i_fop)将inode的i_fop赋值给打开文件的f_op,即将内核线程kdevtmpfs创建的设备结点的inode的i_fop即&def_chr_fops赋值给打开文件的f_op。
- 调用函数指针f->f_op->open指向的函数,即调用(&def_chr_fops)->open即调用函数chrdev_open。
- 如果inode->i_cdev已经设置指向字符设备,则调用函数cdev_get增加字符设备对应的模块的引用计数(try_module_get)、增加字符设备的kobj的引用计数即可;否则,调用函数kobj_lookup根据inode的i_rdev(设备号,在内核线程kdevtmpfs)从cdev_map中查找kobj,然后根据kobj找到字符设备,并设置inode->i_cdev为找到的字符设备,避免下次重复查找。
- 调用fops_get获取字符设备的ops,即函数drm_core_init->register_chrdev设置的&drm_stub_fops,并调用replace_fops将打开文件的f_op设置为字符设备的ops即设置为&drm_stub_fops。
- 调用filp->f_op->open,即调用(&drm_stub_fops)->open即函数drm_stub_open。
- 函数drm_stub_open调用fops_get获取drm driver的fops,并调用replace_fops将drm driver的fops赋值给打开文件的f_op。
- 最终调用filp->f_op->open,即drm driver的fops的成员open。
do_sys_open
-》do_filp_open
-》path_openat
-》link_path_walk 将路径(/dev/dri/cardxx、/dev/dri/renderDxx)转换为最终的目录项dentry
-》do_last
-》vfs_open
-》do_dentry_open
-》f->f_op = fops_get(inode->i_fop); inode->i_fop即def_chr_fops的地址
-》f->f_op->open -- chrdev_open 等于def_chr_fops.open即函数chrdev_open
-》kobj_lookup(cdev_map, inode->i_rdev, &idx); 或 cdev_get
-》fops = fops_get(p->ops) p->ops即字符设备的ops,即函数register_chrdev设置的&drm_stub_fops
-》replace_fops(filp, fops); 将打开文件的f_op设置为字符设备的ops即设置为&drm_stub_fops
-》filp->f_op->open -- drm_stub_open 等于drm_stub_fops.open即函数drm_stub_open
-》new_fops = fops_get(minor->dev->driver->fops); 获取drm driver的fops
-》replace_fops(filp, new_fops); 将filp->f_op替换为new_fops即drm driver的fops
-》filp->f_op->open -- drm driver fops open 调用drm driver的成员fops->open
四、总结
根据上面流程分析可知,打开设备文件file的成员f_op(类型为struct file_operations *)最终被设置为drm driver(类型为struct drm_driver)的fops,后面应用程序对该打开文件进行mmap、close、flush、ioctl、poll、read等操作时,实际就是调用drm driver的fops定义的函数。