LV05-03-Kernel-05-03-04-open函数解析3
本文主要是Linux系统调用open()在字符设备驱动中的应用,详细了解了如何通过open()调用驱动的自定义文件操作集,并学习驱动加载、cdev结构体、chrdev_open()函数及其在内核中的映射过程。若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
PC端开发环境 | Windows | Windows11 |
Ubuntu | Ubuntu20.04.2的64位版本 | |
VMware® Workstation 17 Pro | 17.6.0 build-24238078 | |
终端软件 | MobaXterm(Professional Edition v23.0 Build 5042 (license)) | |
Win32DiskImager | Win32DiskImager v1.0 | |
Linux开发板环境 | Linux开发板 | 正点原子 i.MX6ULL Linux 阿尔法开发板 |
uboot | NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04 | |
linux内核 | linux-4.19.71(NXP官方提供) |
点击查看本文参考资料
分类 | 网址 | 说明 |
官方网站 | https://www.arm.com/ | ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档 |
https://www.nxp.com.cn/ | NXP官方网站 | |
https://www.nxpic.org.cn/ | NXP 官方社区 | |
https://u-boot.readthedocs.io/en/latest/ | u-boot官网 | |
https://www.kernel.org/ | linux内核官网 |
点击查看相关文件下载
分类 | 网址 | 说明 |
NXP | https://github.com/nxp-imx | NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库 |
nxp-imx/linux-imx/releases/tag/v4.19.71 | NXP linux内核仓库tags中的v4.19.71 | |
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 | NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0 | |
I.MX6ULL | i.MX 6ULL Applications Processors for Industrial Products | I.MX6ULL 芯片手册(datasheet,可以在线查看) |
i.MX 6ULL Applications ProcessorReference Manual | I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网) | |
Source Code | https://elixir.bootlin.com/linux/latest/source | linux kernel源码 |
https://elixir.bootlin.com/u-boot/latest/source | uboot源码 |
一、字符设备驱动实例
前面我们打开一个txt文本文件的方式了解了linux的open系统调用过程。下面我们来看设备节点这种文件,假如我们有个字符驱动:my_kmem.ko。使用如下脚本将其加载入内核空间:
1 | !/bin/sh |
字符设备驱动的init函数为:
1 | static int __init my_chrdev_init(void) |
其中 my_chr_dev_fops
是自定义的文件操作集,包括 open()、read()、write()等。我们知道,当我们通过系统调用 open() 打开驱动文件的时候,最后肯定会调用到我们自定义的文件操作集中的 open()。这里我们来分析具体的调用过程。
二、相关函数分析
1. do_dentry_open()
从前面的分析过程知道,内核最终都会调用到 do_dentry_open() 函数,来完成文件打开的操作。而do_dentry_open() 函数里面会找到inode的i_fop成员变量,该成员变量也是一个指向文件操作集的指针,其中就包括 open() 函数,而后面的操作就和具体的文件系统相关了。这里简化 do_dentry_open() 函数如下:
1 | static int do_dentry_open(struct file *f, |
主要是分析 inode→i_fop→open所指向的函数。这里提前了解一下,这个函数指向的是 chrdev_open() 函数,后面分析 mknod 加载驱动的具体过程时会详细分析为什么是这个函数。
2. chrdev_open()
现在来分析下这个 chrdev_open() 函数:
1 | /* |
驱动程序里面的 open() 函数是保存在 struct cdev 结构体中的,而inode的成员变量 i_cdev 正指向这一结构体。
3. cdev_map
在通过 mknod 命令加载驱动的时候,虽然创建了 inode 结构体,但该结构体只是进行了最简单的初始化,并没有将 inode 的 i_cdev 进行赋值,因此当(加载驱动之后)第一次调用该驱动的 open()函数时,需要动态找到 struct cdev 结构体,并保存在 inode 的 i_cdev 成员变量中。而查找 struct cdev 结构体的过程,是通过一个全局变量 cdev_map 来完成的,cdev_map 的数据类型为 struct kobj_map,它其实是一个指针,它在这里被赋值:char_dev.c « fs - kernel/git/stable/linux.git - Linux kernel stable tree
1 | void __init chrdev_init(void) |
它的成员变量 probes 是一个指针数组,在驱动的 init 函数中,将驱动的文件操作函数集(和其他一些数据结构)以设备号为索引塞进这个数组里面,而在(第一次)调用 chrdev_open() 函数的时候,再以文件号为索引,从这个数组里面找到我们需要的文件操作集(以及其他相关的数据结构)。
4. cdev_map->probes成员
下面我们来具体分析cdev进cdev_map成员 probes 和查找的过程。
4.1 cdev怎么存入probes指针数组
首先具体分析下驱动的init函数,主要看下面三个函数:
1 | static int my_chrdev_init(void) |
4.1.1 alloc_chrdev_region()
先看第一个函数:alloc_chrdev_region():
1 | /** |
其主要作用是创建并初始化一个 struct char_device_struct 结构体,然后将该结构体地址保存在名为 chrdevs 的指针数组里面。这个 chrdevs 指针数组是内核空间的全局变量,在后面我们会重点了解,这里我们先看 alloc_chrdev_region() 函数,其中主要调用 __register_chrdev_region() 函数来完成工作,其定义如下:
1 | static struct char_device_struct * |
在该函数中,通过 find_dynamic_major() 函数查找可用的主设备号,这里我们不再详细分析,但我们只假设最简单的情况,即返回的设备号 i 对应的 chrdevs[i] == NULL
。
这个chrdevs数组有点绕:chrdevs 是个指针数组,也就是说它的每个成员变量的值都是一个地址,而这些地址是指向 struct char_device_struct 结构体的,但有些地址指向NULL。通过 find_dynamic_major() 函数其实是找第一个指向 NULL 的元素索引。在最后通过 *cp = cd 将新创建的 struct char_device_struct 的地址被保存在 chrdevs 数组里面了,且以主设备号为索引。
4.1.2 cdev_init()
下面继续分析 cdev_init() 函数:
1 | /** |
其中 kobject_init() 是进行一些必要的初始化工作,我们不再详细展开分析。最后在这里,将驱动的文件操作函数集与cdev结构进行关联,即cdev->ops = fops
。
4.1.3 cdev_add()
下面来看最后一个函数 cdev_add() :
1 | /** |
这里面主要调用了 kobj_map() 函数,将 cdev 结构体塞进 cdev_map→probes 数组里面,不过需要注意的是,被塞进去的并不是 cdev 结构体本身,而是 新创建了一个 struct probe 结构体,而 probe→data 指向的才是 cdev 结构体。这里我们不再详细展开了。
4.1.4 kobj_map()
kobj_map() 函数定义如下,在这里我们只考虑最简单的情况:
1 | int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range, |
4.2 怎么从probes指针数组找到cdev
cdev存进去的过程分析完了,现在分析在 open() 系统调用的时候如何再差找到被存进去的这个 cdev 结构体呢 ?前面我们分析到了 chrdev_open() 函数,在这个函数里面调用了 kobj_lookup() 函数进行查找,该查找过程其实和 kobj_map() 函数的存入过程有点类似,这里不再详细分析,只不过这里查找到的是 struct kobject 结构体,而这个结构体又是 cdev 结构体的成员变量,关系有点绕,但最终还是把 cdev 给找到了。
4.2.1 kobj_lookup()
在这里我们只考虑最简单的情况,kobj_lookup() 函数定义如下:
1 | struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index) |
4.3 总结
存入和查找 cdev 结构体都是通过 cdev_map→probes 数组来进行的,具体来说是这个数组的第 [MAJOR(dev) % 255] 个元素。整个过程貌似和 alloc_chrdev_region()
函数里面创建的 struct char_device_struct (地址保存在 chrdevs 数组里面) 结构体没什么关系。
参考资料