Skip to content

LV203-open函数解析3

本文主要是 Linux 系统调用 open()在字符设备驱动中的应用,详细了解了如何通过 open()调用驱动的自定义文件操作集,并学习驱动加载、cdev 结构体、chrdev_open()函数及其在内核中的映射过程。

一、字符设备驱动实例

前面我们打开一个 txt 文本文件的方式了解了 linux 的 open 系统调用过程。下面我们来看设备节点这种文件,假如我们有个字符驱动:my_kmem.ko。使用如下脚本将其加载入内核空间:

shell
#!/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 函数为:

cpp
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() 函数如下:

cpp
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() 函数:

cpp
/*
 * 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

c
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 函数,主要看下面三个函数:

cpp
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()

c
/**
 * 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()](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/fs/char_dev.c?h = v4.19.71&id = e7d2672c66e4d3675570369bf20856296da312c4#n100) 函数来完成工作,其定义如下:

cpp
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() 函数:

cpp
/**
 * 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()

cpp
/**
 * 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() 函数定义如下,在这里我们只考虑最简单的情况:

cpp
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() 函数定义如下:

cpp
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 博客