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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/sh

module="my_kmem"
device="my_chr_dev"
mod="664"

# 加载驱动
insmod $module.ko || exit 1

rm -f /dev/${device}0

# 获取主设备号
major=`awk -v dev=$device '$2 == dev {print $1}' /proc/devices`
echo "major number: $major"

# 创建设备节点
mknod /dev/${device}0 c $major 0
chmod $mod /dev/${device}0 # 最后设备节点名为/dev/my_chr_dev0

字符设备驱动的init函数为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int __init my_chrdev_init(void)
{
int ret = 0;
// 自动获取设备号,设备名为 my_chr_dev
ret = alloc_chrdev_region(&dev, 0, 1, "my_chr_dev");
if (ret != 0) {
printk(KERN_ALERT "error allocating device number\n");
return ret;
}

cdev_init(&my_chrdev, &my_chr_dev_fops);
my_chrdev.owner = THIS_MODULE;
//使用cdev_add()函数进行字符设备的添加
ret = cdev_add(&my_chrdev, dev, 1);
if (ret < 0) {
printk(KERN_ALERT "adding charactor device failed\n");
unregister_chrdev_region(dev, 1);
return ret;
}
return 0;
}

其中 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int do_dentry_open(struct file *f,
struct inode *inode,
int (*open)(struct inode *, struct file *))
{
//......
f->f_op = fops_get(inode->i_fop);
if (unlikely(WARN_ON(!f->f_op))) {
error = -ENODEV;
goto cleanup_all;
}
//......
if (!open)
open = f->f_op->open;
if (open) {
error = open(inode, f);
if (error)
goto cleanup_all;
}
//......
return error;
}

主要是分析 inode→i_fop→open所指向的函数。这里提前了解一下,这个函数指向的是 chrdev_open() 函数,后面分析 mknod 加载驱动的具体过程时会详细分析为什么是这个函数。

2. chrdev_open()

现在来分析下这个 chrdev_open() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*
* Called every time a character special file is opened
*/
static int chrdev_open(struct inode *inode, struct file *filp)
{
const struct file_operations *fops;
struct cdev *p;
struct cdev *new = NULL;
int ret = 0;
//......
p = inode->i_cdev;
if (!p) {
struct kobject *kobj;
//......
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
//......
new = container_of(kobj, struct cdev, kobj);
//......
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;
//......
fops = fops_get(p->ops);
//......
replace_fops(filp, fops);
if (filp->f_op->open) {
ret = filp->f_op->open(inode, filp);
//......
}

return 0;
//......
}

驱动程序里面的 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
2
3
4
void __init chrdev_init(void)
{
cdev_map = kobj_map_init(base_probe, &chrdevs_lock);
}

它的成员变量 probes 是一个指针数组,在驱动的 init 函数中,将驱动的文件操作函数集(和其他一些数据结构)以设备号为索引塞进这个数组里面,而在(第一次)调用 chrdev_open() 函数的时候,再以文件号为索引,从这个数组里面找到我们需要的文件操作集(以及其他相关的数据结构)。

4. cdev_map->probes成员

下面我们来具体分析cdev进cdev_map成员 probes 和查找的过程。

4.1 cdev怎么存入probes指针数组

首先具体分析下驱动的init函数,主要看下面三个函数:

1
2
3
4
5
6
7
8
9
10
static int my_chrdev_init(void)
{
//......
ret = alloc_chrdev_region(&dev, 0, 1, "my_chr_dev");
//......
cdev_init(&my_chrdev, &my_chr_dev_fops);
//......
ret = cdev_add(&my_chrdev, dev, 1);
//......
}

4.1.1 alloc_chrdev_region()

先看第一个函数:alloc_chrdev_region()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}

其主要作用是创建并初始化一个 struct char_device_struct 结构体,然后将该结构体地址保存在名为 chrdevs 的指针数组里面。这个 chrdevs 指针数组是内核空间的全局变量,在后面我们会重点了解,这里我们先看 alloc_chrdev_region() 函数,其中主要调用 __register_chrdev_region() 函数来完成工作,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
{
struct char_device_struct *cd, **cp;
int ret = 0;
int i;

cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
//......
if (major == 0) {
ret = find_dynamic_major();
//......
major = ret;
}
//......
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;
strlcpy(cd->name, name, sizeof(cd->name));

i = major_to_index(major);

for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next) // 这里只考虑最简单的情况, 此时 *cp == NULL, 此为空循环
//......

/* Check for overlapping minor ranges. */
if (*cp && (*cp)->major == major) { // 只考虑最简单情况, *cp == NULL, if 判断为0
//......
}

cd->next = *cp;
*cp = cd;//这时,新创建的 char_device_struct 结构体的地址被保存在 chrdevs 数组里面了,且以主设备号为索引
//......
}

在该函数中,通过 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* cdev_init() - initialize a cdev structure
* @cdev: the structure to initialize
* @fops: the file_operations for this device
*
* Initializes @cdev, remembering @fops, making it ready to add to the
* system with cdev_add().
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}

其中 kobject_init() 是进行一些必要的初始化工作,我们不再详细展开分析。最后在这里,将驱动的文件操作函数集与cdev结构进行关联,即cdev->ops = fops

4.1.3 cdev_add()

下面来看最后一个函数 cdev_add()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
* device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately. A negative error code is returned on failure.
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;

p->dev = dev;
p->count = count;

error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;

kobject_get(p->kobj.parent);

return 0;
}

这里面主要调用了 kobj_map() 函数,将 cdev 结构体塞进 cdev_map→probes 数组里面,不过需要注意的是,被塞进去的并不是 cdev 结构体本身,而是 新创建了一个 struct probe 结构体,而 probe→data 指向的才是 cdev 结构体。这里我们不再详细展开了。

4.1.4 kobj_map()

kobj_map() 函数定义如下,在这里我们只考虑最简单的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
struct module *module, kobj_probe_t *probe,
int (*lock)(dev_t, void *), void *data)
{
/* 传入参数如下:
* domain = cdev_map;
* data = &my_chr_dev; 在这里面保存了 文件操作函数集 。
*/
unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
unsigned index = MAJOR(dev);
unsigned i;
struct probe *p;

if (n > 255)
n = 255;

p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL);
if (p == NULL)
return -ENOMEM;

for (i = 0; i < n; i++, p++) {//这里循环只执行一次
p->owner = module;
p->get = probe;
p->lock = lock;
p->dev = dev;
p->range = range;
p->data = data;
}
mutex_lock(domain->lock);
//这里循环只执行一次,由于上面的循环有 p++,因此这里需要 p-=n
for (i = 0, p -= n; i < n; i++, p++, index++) {
struct probe **s = &domain->probes[index % 255];
while (*s && (*s)->range < range) // 这里 *s == NULL,循环为空
s = &(*s)->next;
p->next = *s;
*s = p; // 这里就把新创建的 probe 结构体地址保存在了 cdev_maps->probes[index % 255] 中了
}
mutex_unlock(domain->lock);
return 0;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index)
{
struct kobject *kobj;
struct probe *p;
unsigned long best = ~0UL;

retry:
mutex_lock(domain->lock);
// 假设最简单的情况,这里循环只执行一次
for (p = domain->probes[MAJOR(dev) % 255]; p; p = p->next) {
struct kobject *(*probe)(dev_t, int *, void *);
struct module *owner;
void *data;
//......
owner = p->owner;
data = p->data;
probe = p->get;
/* 这是一个函数,由 kobj_map 函数所保存,在这里,这个函数是:
* static struct kobject *exact_match(dev_t dev, int *part, void *data)
* {
* struct cdev *p = data;
* return &p->kobj;
* } */
best = p->range - 1;
*index = dev - p->dev;
//......
mutex_unlock(domain->lock);
kobj = probe(dev, index, data); // 其实就相当于: kobj = &data->kobj
/* Currently ->owner protects _only_ ->probe() itself. */
module_put(owner);
if (kobj)
return kobj;
goto retry;
}
mutex_unlock(domain->lock);
return NULL;
}

4.3 总结

存入和查找 cdev 结构体都是通过 cdev_map→probes 数组来进行的,具体来说是这个数组的第 [MAJOR(dev) % 255] 个元素。整个过程貌似和 alloc_chrdev_region() 函数里面创建的 struct char_device_struct (地址保存在 chrdevs 数组里面) 结构体没什么关系。

参考资料

Linux 系统调用之open(三)_linux open没有调用驱动的open打印-CSDN博客