Linux 系统调用之open(三)

前面我们以及其简单的例子分析了linux的open系统调用过程。下面换一个例子,假如我们有个字符驱动:my_kmem.ko。使用如下脚本将其加载入内核空间:

#!/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

驱动的init函数为:

static int my_chrdev_init(void)
{
	int ret;

	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;

	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()。这里我们来分析具体的调用过程。从前面的分析过程知道,内核最终都会调用到do_dentry_open()函数,来完成文件打开的操作。而do_dentry_open()函数里面会找到inode的i_fop成员变量,该成员变量也是一个指向文件操作集的指针,其中就包括 open() 函数,而后面的操作就和具体的文件系统相关了。这里简化do_dentry_open()函数如下:

static int do_dentry_open(struct file *f,
			  struct inode *inode,
			  int (*open)(struct inode *, struct file *),
			  const struct cred *cred)
{
	...
	f->f_op = fops_get(inode->i_fop);
	...
	if (!open)
		open = f->f_op->open;
	if (open) {
		error = open(inode, f);
		if (error)
			goto cleanup_all;
	}
	...
}

看来要分析整个open()系统调用的调用过程,主要是分析 inode->i_fop->open所指向的函数。这里提前说一下,这个函数指向的是 chrdev_open()函数,后面分析 mknod 加载驱动的具体过程时会详细分析为什么是这个函数。现在来分析下这个函数吧:

/*
 * 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);
		...
			inode->i_cdev = p = new;
			...
	} 
	...
	fops = fops_get(p->ops);
        ...
	replace_fops(filp, fops);
	if (filp->f_op->open) {
		ret = filp->f_op->open(inode, filp);
		...
	}

        ...
}

可见,驱动程序里面的 open() 函数是保存在 struct cdev 结构体中的,而inode的成员变量 i_cdev 正指向这一结构体。但在通过 mknod 命令加载驱动的时候,虽然创建了 inode 结构体,但该结构体只是进行了最简单的初始化,并没有将 inode 的 i_cdev 进行赋值,因此当(加载驱动之后)第一次调用该驱动的 open()函数时,需要动态找到 struct cdev 结构体,并保存在 inode 的 i_cdev 成员变量中。而查找 struct cdev 结构体的过程,是通过一个全局变量 cdev_map 来完成的。cdev_map 的成员变量 probes 是一个数组,在驱动的 init 函数中,将驱动的文件操作函数集(和其他一些数据结构)以设备号为索引塞进这个数组里面,而在(第一次)调用chrdev_open()函数的时候,再以文件号为索引,从这个数组里面找到我们需要的文件操作集(以及其他相关的数据结构)。下面我们将分析具体塞进和查找的过程。首先具体分析下驱动的init函数,主要看下面三个函数:

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

先看第一个函数:alloc_chrdev_region(),其主要作用是创建并初始化一个struct char_device_struct 结构体,然后将该结构体地址保存在名为 chrdevs 的数组里面。这个 chrdevs 数组是内核空间的全局变量,在后面我们会重点介绍,这里我们先看 alloc_chrdev_region() 函数的定义:

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

其中主要调用 __register_chrdev_region() 函数来完成工作,其定义如下:

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->next == NULL */
	*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 将新创建的 char_device_struct 结构体的地址被保存在 chrdevs 数组里面了,且以主设备号为索引。

下面继续分析cdev_init()函数,该函数相对简单:

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

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 结构体。这里我们不再详细展开了。

塞进去的过程分析完了,现在分析在 open() 系统调用的时候如何再差找到被塞进去的这个 cdev 结构体呢 ?前面我们分析到了chrdev_open()函数,在这个函数里面调用了 kobj_lookup() 函数进行查找,该查找过程其实和 kobj_map() 函数的塞入过程有点类似,这里不再详细分析,只不过这里查找到的是 struct kobject 结构体,而这个结构体又是 cdev 结构体的成员变量,关系有点绕,但最终还是把 cdev 给找到了。

-----------------------

作为个人笔记,现在通过比较kobj_lookup() 函数和kobj_map() 函数详细分析塞入和查找过程,在这里我们只考虑最简单的情况:

kobj_map函数定义如下:

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

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

kobj_lookup函数定义如下:

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

你可能感兴趣的:(Linux 系统调用之open(三))