欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

字符设备驱动学习之老接口和新街口

程序员文章站 2022-07-14 17:34:46
...

老接口是直接使用register_chrdev函数来进行字符设备号的申请和驱动注册。

字符设备驱动学习之老接口和新街口

新的接口则要先使用register_chrdev_region/alloc_chrdev_region分配设备号

再使用cdev_alloc申请内存存放字符设备信息

最后通过cdev_add把申请到的存放字符设备信息的指针放到全局存放所有字符设备信息的一个表中

字符设备驱动学习之老接口和新街口

注:使用cdev_init和使用cdev_alloc的异同可以查看我的上一篇博客。

为了弄清老接口和新街口的区别,查看具体代码实现。

首先看老接口的实现:

static inline int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)
{
	return __register_chrdev(major, 0, 256, name, fops);
}

可以发现 老接口实际是调用的内核内部的一个函数 __register_chrdev,参数分别是(主设备号,次设备号开始,次设备号个数,设备名称,文件描述符)

int __register_chrdev(unsigned int major, unsigned int baseminor,
		      unsigned int count, const char *name,
		      const struct file_operations *fops)
{
	struct char_device_struct *cd;
	struct cdev *cdev;
	int err = -ENOMEM;

    /* 得到新的字符设备次设备范围 */
	cd = __register_chrdev_region(major, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);
	
	cdev = cdev_alloc();    /* 申请一个cdev */
	if (!cdev)
		goto out2;

    /* 初始化cdev */
	cdev->owner = fops->owner;
	cdev->ops = fops;
	kobject_set_name(&cdev->kobj, "%s", name);
	/* 把cdev加入到字符设备的cdev表中 */	
	err =  cdev_add(cdev, MKDEV(cd->major, baseminor), count);
	if (err)
		goto out;

	cd->cdev = cdev;

	return major ? 0 : cd->major;
out:
	kobject_put(&cdev->kobj);
out2:
	kfree(__unregister_chrdev_region(cd->major, baseminor, count));
	return err;
}
 __register_chrdev_region(major, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);
	
	cdev = cdev_alloc();
	if (!cdev)
		goto out2;

	cdev->owner = fops->owner;
	cdev->ops = fops;
	kobject_set_name(&cdev->kobj, "%s", name);
		
	err =  cdev_add(cdev, MKDEV(cd->major, baseminor), count);
	if (err)
		goto out;

	cd->cdev = cdev;

	return major ? 0 : cd->major;
out:
	kobject_put(&cdev->kobj);
out2:
	kfree(__unregister_chrdev_region(cd->major, baseminor, count));
	return err;
}

仔细分析__register_chrdev这个函数发现,其内部还是调用了__register_chrdev_region,cdev_alloc,cdev_add这个三个函数。唯一不同的老的接口是次设备号只有一个固定的0

 


/**
 * register_chrdev_region() - register a range of device numbers
 * @from: the first in the desired range of device numbers; must include
 *        the major number.
 * @count: the number of consecutive device numbers required
 * @name: the name of the device or driver.
 *
 * Return value is zero on success, a negative error code on failure.
 */
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
	struct char_device_struct *cd;
	dev_t to = from + count;    /*计算分配设备号范围中的最大值 */
	dev_t n, next;

	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);        /* 下一个次设备号的最小值 */
		if (next > to)                      
			next = to;        /* 表示当前次设备号足够分配(理论上,但也可能已经被用) */
		cd = __register_chrdev_region(MAJOR(n), MINOR(n),
			       next - n, name);        /* 注册设备号 */
		if (IS_ERR(cd))
			goto fail;
	}
	return 0;
fail:        /*当任何一次分配失败的时候,释放所有已经申请的设备号*/
	to = n;
	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
	}
	return PTR_ERR(cd);
}

 

 

接下来再看一下,__register_chrdev_region函数的原型

#define CHRDEV_MAJOR_HASH_SIZE	255

static struct char_device_struct {
	struct char_device_struct *next;
	unsigned int major;
	unsigned int baseminor;
	int minorct;
	char name[64];
	struct cdev *cdev;		/* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];




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;

    /* 动态申请一个struct char_device_struct大小的内存空间 */
	cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
	if (cd == NULL)
		return ERR_PTR(-ENOMEM);

	mutex_lock(&chrdevs_lock);

	/* temporary */
	if (major == 0) {      /* 如果主设备号为0,则从254开始,查找最大的未用设备号 */
		for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
			if (chrdevs[i] == NULL)
				break;
		}

		if (i == 0) {        /* 没有可用的主设备号了 */
			ret = -EBUSY;
			goto out;
		}
		major = i;
		ret = major;
	}

	cd->major = major;
	cd->baseminor = baseminor;
	cd->minorct = minorct;        /* 申请次设备号的个数*/
	strlcpy(cd->name, name, sizeof(cd->name));

	i = major_to_index(major);    /*将major对256取余数,得到可以存放char_device_struct在chrdevs中的索引*/

    /*寻找新元素插入的位置*/
	for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
		if ((*cp)->major > major ||        //如果当前元素的主设备号大于要添加的主设备号(表示改主设备号没用,直接可以)
		    ((*cp)->major == major &&      //当前元素的主设备号等于要添加的主设备号(主用了,次设备号不重复也可以)
		     (((*cp)->baseminor >= baseminor) ||    //并且次设备号大于等于要添加的次设备号
		      ((*cp)->baseminor + (*cp)->minorct > baseminor))))    //如果次设备号+申请设备号的数量 > 次设备号
			break;

	/* Check for overlapping minor ranges.  */
	if (*cp && (*cp)->major == major) {    //如果当前元素存在并且主设备号和要添加的主设备号一致
		int old_min = (*cp)->baseminor;    //更新旧的最小值,值为当前元素的次设备号
		int old_max = (*cp)->baseminor + (*cp)->minorct - 1;    //更新旧的最大值,值为当前次设备号+要申请号的设备个数-1,-1是因为从0开始的次设备号
		int new_min = baseminor;            //新的最小值为要添加的次设备号
		int new_max = baseminor + minorct - 1;    //新的最大值为要添加的次设备号+要申请号的设备个数-1,-1是因为次设备号从0开始

        /* 新的区间可以在旧的区间的左侧或者右侧 */
		/* New driver overlaps from the left.  */
		if (new_max >= old_min && new_max <= old_max) {    /显然生成的新的最大值必须要大于旧的最大值或者小旧的最小值
			ret = -EBUSY;
			goto out;
		}

		/* New driver overlaps from the right.  */
		if (new_min <= old_max && new_min >= old_min) {    //显然生成的新的最小值必须要大于旧的最大值或者小旧的最小值
			ret = -EBUSY;
			goto out;
		}
	}

    /*往链表内添加新元素*/
	cd->next = *cp;          //新元素的指向当前元素
	*cp = cd;                //当前元素更新为新元素
	mutex_unlock(&chrdevs_lock);
	return cd;               //返回当前元素的结构体指针
out:
	mutex_unlock(&chrdevs_lock);
	kfree(cd);
	return ERR_PTR(ret);
}

上面函数是一个很重要的函数,它表明了字符设备的主从设备之间的关联。

字符设备驱动学习之老接口和新街口

 

	for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
		if ((*cp)->major > major ||
		    ((*cp)->major == major &&
		     (((*cp)->baseminor >= baseminor) ||
		      ((*cp)->baseminor + (*cp)->minorct > baseminor))))
			break;

上面这几句表明,如果主设备号已经存在,则新的次设备号必须小于等于原来chrdevs数组中的chrdevs[i]->baseminor【即在原来的左侧】或新的次设备号小于原来位置的次设备号+设备号个数【即在原来的右侧】

这里的i表示主设备号

再加上后面的新的次设备区间必须在满足一些条件。即可确定次设备号是否可用。

 

 

下面部分主要是 把传参放到申请的字符设备内存中。

其中我加了注释的那条表明主设备号不能超过大于等于255,否则对齐取余。

字符设备驱动学习之老接口和新街口

对这点我们深入分析一下。

假设目前主设备号2,里面有一个次设备0.如下图所示

字符设备驱动学习之老接口和新街口

此时注册一个主设备号为257,次设备号为0的设备。

字符设备驱动学习之老接口和新街口

分析上面的语句,第一句直接成立会退出for循环。cp为chedevs数组指针的第2个指针,即指向我画的图的major = 2,minor = 0。这个设备。

 

接下来注意下面这个判断。(*cp)->major 为2  ,传的参数 major为 257。所以这个判断不会成立。直接分析最后一句。

字符设备驱动学习之老接口和新街口

最后一句就是单向链表了。*cp代表指向major = 2  ,minor = 0设备的指针,即在上图的chrdevs[2]这个指针。  cd则为指向major = 257,minor = 0的设备新kzalloc的空间。

即最终注册完如下图所示。

字符设备驱动学习之老接口和新街口

 

总结注册字符设备驱动有三种方法

1、register_chrdev

2、register_chrdev_region

     cdev_alloc/cdev_init

     cdev_add

3、alloc_chrdev_region

     cdev_alloc/cdev_init

     cdev_add

三种的区别分别如下

第一种:老的字符设备驱动注册函数,不支持次设备号的注册

第二种:新的字符设备驱动注册函数,由写驱动的人定义主字符设备号

第三种:新的字符设备驱动注册函数,由系统自动分配主字符设备号