对技术执着
分类: LINUX
2015-03-14 14:15:06
原文地址:cdev字符串驱动架构 作者:cainiao413
2.3 字符设备的内核抽象(1)
顾名思义,字符设备驱动程序管理的核心对象是字符设备。从字符设备驱动程序的设计框架角度出发,内核为字符设备抽象出了一个具体的数据结构struct cdev,其定义如下:
在本章后续的内容中将陆续看到它们的实际用法,这里只把这些成员的作用简单描述如下:
- <include/linux/cdev.h>
- struct cdev {
- struct kobject kobj;
- struct module *owner;
- const struct file_operations *ops;
- struct list_head list;
- dev_t dev;
- unsigned int count;
- };
内嵌的内核对象,其用途将在"Linux设备驱动模型"一章中讨论。
- struct kobject kobj
字符设备驱动程序所在的内核模块对象指针。
- struct module *owner
字符设备驱动程序中一个极其关键的数据结构,在应用程序通过文件系统接口呼叫到设备驱动程序中实现的文件操作类函数的过程中,ops指针起着桥梁纽带的作用。
- const struct file_operations *ops
用来将系统中的字符设备形成链表。
- struct list_head list
字符设备的设备号,由主设备号和次设备号构成。
- dev_t dev
- unsigned int count
隶属于同一主设备号的次设备号的个数,用于表示由当前设备驱动程序控制的实际同类设备的数量。
设备驱动程序中可以用两种方式来产生struct cdev对象。一是静态定义的方式,比如在前面的那个示例程序中,通过下列代码静态定义了一个struct cdev对象:
另一种是在程序的执行期通过动态分配的方式产生,比如:
- static struct cdev chr_dev;
其实Linux内核源码中提供了一个函数cdev_alloc,专门用于动态分配struct cdev对象。cdev_alloc不仅会为struct cdev对象分配内存空间,还会对该对象进行必要的初始化:
- static struct cdev *p = kmalloc(sizeof(struct cdev), GFP_KERNEL);
- <fs/char_dev.c>
- struct cdev *cdev_alloc(void)
- {
- struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
- if (p) {
- INIT_LIST_HEAD(&p->list);
- kobject_init(&p->kobj, &ktype_cdev_dynamic);
- }
- return p;
- }
需要注意的是,内核引入struct cdev数据结构作为字符设备的抽象,仅仅是为了满足系统对字符设备驱动程序框架结构设计的需要,现实中一个具体的字符硬件设备的数据结构的抽象往往要复 杂得多,在这种情况下struct cdev常常作为一种内嵌的成员变量出现在实际设备的数据机构中,比如:
- struct my_keypad_dev{
- //硬件相关的成员变量
- int a;
- int b;
- int c;
- …
- //内嵌的struct cdev数据结构
- struct cdev cdev;
- };
2.3 字符设备的内核抽象(2)
在这样的情况下,如果要动态分配一个struct real_char_dev对象,cdev_alloc函数显然就无能为力了,此时只能使用下面的方法:
- static struct real_char_dev *p = kzalloc(sizeof(struct real_char_dev), GFP_KERNEL);
前面讨论了如何分配一个struct cdev对象,接下来的一个话题是如何初始化一个cdev对象,内核为此提供的函数是cdev_init:
- <fs/char_dev.c>
- 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;
- }
函数的代码非常直白,不再赘述。一个struct cdev对象在被最终加入系统前,都应该被初始化,无论是直接通过cdev_init或者是其他途径。理由很简单,这是Linux系统中字符设备驱动程序框架设计的需要。
照理在谈完cdev对象的分配和初始化之后,下面应该讨论如何将一个cdev对象加入到系统了,但是由于这个过程需要用到设备号相关的技术点,所以暂且先来探讨设备号的问题。
2.4 设备号的构成与分配
本节开始讨论设备号相关的问题,不过设备号对于设备驱动程序而言究竟意味着什么,换句话说,它在内核中起着怎样的作用,本节暂不讨论,这里只关心它在内核中是如何分配和管理的。
2.4.1 设备号的构成
Linux系统中一个设备号由主设备号和次设备号构成,Linux内核用主设备号来定位对应的设备驱动程序,而次设备号则由驱动程序使用,用来标识 它所管理的若干同类设备。因此,从这个角度而言,设备号作为一种系统资源,必须仔细加以管理,以防止因设备号与驱动程序错误的对应关系所带来的混乱。
Linux用dev_t类型变量来标识一个设备号,这是个32位的无符号整数:
图2-2显示了2.6.39版本内核中设备号的构成:
- <include/linux/types.h>
- typedef __u32 __kernel_dev_t;
- typedef __kernel_dev_t dev_t;
图2-2 Linux的设备号的构成 |
MAJOR宏用来从一个dev_t类型的设备号中提取出主设备号,MINOR宏则用来提取设备号中的次设备号。MKDEV则是将主设备号 ma和次设备号mi合成一个dev_t类型的设备号。在上述宏定义中,MINORBITS宏在2.6.39版本中定义的值是20,如果之后的内核对主次设 备号所占用的位宽重新进行调整,例如将MINORBITS改成12,只要设备驱动程序坚持使用MAJOR、MINOR和MKDEV来操作设备号,那么这部 分代码应该无须修改就可以在新内核中运行。
- <include/linux/kdev_t.h>
- #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
- #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
- #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
2.4.2 设备号的分配与管理(1)
在内核源码中,涉及设备号分配与管理的函数主要有以下两个:
register_chrdev_region函数
该函数的代码实现如下:
该函数的第一参数from表示的是一个设备号,第二参数count是连续设备编号的个数,代表当前驱动程序所管理的同类设备的个数,第三参 数name表示设备或者驱动的名称。register_chrdev_region的核心功能体现在内部调用的 __register_chrdev_region函数中,在讨论这个函数之前,先要看一个全局性的指针数组chrdevs,它是内核用于设备号分配与管 理的核心元素,其定义如下:
- <fs/char_dev.c>
- 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);
- }
- <fs/char_dev.c>
- 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 ];
这个数组中的每一项都是一个指向struct char_device_struct类型的指针。系统刚开始运行时,该数组的初始状态如图2-3所示:
现在回过头来看看register_chrdev_region函数,这个函数要完成的主要功能是将当前设备驱动程序要使用的设备号记录到 chrdevs数组中,有了这种对设备号使用情况的跟踪,系统就可以避免不同的设备驱动程序使用同一个设备号的情形出现。这意味着当设备驱动程序调用这个 函数时,事先已经明确知道它所要使用的设备号,之所以调用这个函数,是要将所使用的设备号纳入到内核的设备号管理体系中,防止别的驱动程序错误使用到。当 然如果它试图使用的设备号已经被之前某个驱动程序使用了,调用将不会成功,register_chrdev_region函数将会返回一个负的错误码告知 调用者,如果调用成功,函数返回0。
图2-3 初始状态的chrdevs数组结构 |
- <fs/char_dev.c>
- static struct char_device_struct *
- __register_chrdev_region(unsigned int major, unsigned int baseminor,
- int minorct, const char *name)
- {
- cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
- …
- cd->majormajor = major;
- cd->baseminorbaseminor = baseminor;
- cd->minorctminorct = minorct;
- strlcpy(cd->name, name, sizeof(cd->name));
这个过程完成之后,它开始搜索chrdevs数组,搜索是以哈希表的形式进行的,为此必须首先获取一个散列关键值,正如读者所预料的那样,它用主设备号来生成这个关键值:
- i = major_to_index(major);
这是个非常简单的获得散列关键值的方法,i = major % 255。此后函数将对chrdevs[i]元素管理的链表进行扫描,如果chrdevs[i]上已经有了链表节点,表明之前有别的设备驱动程序使用的主设 备号散列到了chrdevs[i]上,为此函数需要相应的逻辑确保当前正在操作的设备号不会与这些已经在使用的设备号发生冲突,如果有冲突,函数将返回错 误码,表明本次调用没有成功。如果本次调用使用的设备号与chrdevs[i]上已有的设备号没有发生冲突,先前分配的struct char_device_struct对象cd将加入到chrdevs[i]领衔的链表中成为一个新的节点。没有必要再仔细分析 __register_chrdev_region函数中的相关代码了,接下来以一个具体的例子来了解这一过程。
在chrdevs数组尚处于初始状态的情形下,假设现在有一个设备驱动程序要使用的主设备号是257,次设备号分别是0、1、2和3(意味着该驱动程序将管理四个同类型的设备)。它对register_chrdev_region函数的调用如下:
- int ret = register_chrdev_region(MKDEV(257, 0), 4, "demodev");