- 论坛徽章:
- 0
|
本帖最后由 zhiqiang0071 于 2010-07-26 09:17 编辑
本文属本人原创,欢迎转载,转载请注明出处。由于个人的见识和能力有限,不可能面面俱到,也可能存在谬误,敬请网友指出,本人的邮箱是yzq.seen@gmail.com,博客是http://zhiqiang0071.cublog.cn。
在linux系统中,很多驱动是字符型驱动,有些是直接编译集成在内核中,另一些是单独编译成“.ko”动态加载的。其实字符驱动只是个外壳,用于内核与应用程序间通信,无非是调用open,release,read,write和ioctl等例程。所以根据应用不同,字符驱动能会调用其他驱动模块,如i2c、spi和v4l2等,于是字符驱动还可分WDT驱动、RTC驱动和MTD驱动等。所以在分析其他驱动模块之前有必要好好分析下字符设备驱动模型。本篇文章要讲的就是字符设备驱动模型,也就是字符设备驱动是怎么注册和注销的,怎么生成设备节点的,怎么和应用程序关联的,例程调用具体如何实现的等等。
一、字符设备驱动的注册和注销
对于写过linux-2.6内核(本文采用linux-2.6.18内核)字符驱动的程序员来说,对下面这段程序的形式肯定不陌生。- int result;
- /*
- * Register the driver in the kernel
- * Dynmically get the major number for the driver using
- * alloc_chrdev_region function
- */
- result = alloc_chrdev_region(&dev, 0, 1, “testchar”);
- /* if it fails return error */
- if (result < 0) {
- printk("Error registering test character device\n");
- return -ENODEV;
- }
- printk(KERN_INFO " test major#: %d, minor# %d\n", MAJOR(dev), MINOR(dev));
- /* initialize cdev with file operations */
- cdev_init(&cdev, & test _fops);
- cdev.owner = THIS_MODULE;
- cdev.ops = &test_fops;
- /* add cdev to the kernel */
- result = cdev_add(&cdev, dev, 1);
- if (result) {
- unregister_chrdev_region(dev, 1);
- printk("Error adding test char device: error no:%d\n", result);
- return -EINVAL;
- }
- testchar _class = class_create(THIS_MODULE, "testchar");
- if (!testchar _class) {
- printk("Error creating test device class\n");
- unregister_chrdev_region(dev, 1);
- unregister_chrdev(MAJOR(dev), "testchar");
- cdev_del(&cdev);
- return -EINVAL;
- }
- class_device_create(testchar _class, NULL, dev, NULL, "testchar");
复制代码 通常这段程序会放在一个模块初始化加载函数里,形式是这样的,- int __init testchar_init(void)
- {
- }
- module_init(testchar_init);
复制代码 既然有注册的函数,那必然有注销的函数,这叫有进必有出,有公必有母…,总而言之,这是大自然的神奇被人类所利用。废话少说,形式是这样的,- void __exit testchar _cleanup(void)
- {
- /* remove major number allocated to this driver */
- unregister_chrdev_region(dev, 1);
- /* remove simple class device */
- class_device_destroy(testchar_class, dev);
- /* destroy simple class */
- class_destroy(testchar class);
- cdev_del(&cdev);
- /* unregistering the driver from the kernel */
- unregister_chrdev(MAJOR(dev), "testchar");
- }
- module_exit(testchar_cleanup);
复制代码 这些注册字符驱动的例程调用大都集中在文件fs/char_dev.c中。所以先来看看这个文件中都有些啥,这叫直捣黄龙。
这有个初始化函数,在模块加载过程中会被调用到(动态insmod加载或在内核中加载),如下,- void __init chrdev_init(void)
- {
- cdev_map = kobj_map_init(base_probe, &chrdevs_lock);
- }。
复制代码 kobj_map_init()函数传进去了两个参数,base_probe函数和chrdevs_lock互斥变量,返回一个struct kobj_map类型的指针。
base_probe调用了request_module()函数,用于加载与字符驱动相关的驱动程序,被加载的驱动命名方式是char-major-主设备号-次设备号。request_module()函数,你可以看看该函数上头的英文注释,它最终会调用应用层空间的modprobe命令来加载驱动程序。实际上,没有使用该项功能。
chrdevs_lock是一个全局的互斥变量,用于整个设备驱动模块的关键区域保护,后面你会看到。
返回的是struct kobj_map类型指针,保存到cdev_map中,该结构体干吗用的呢,顾名思义,用来做映射连通的,后面会慢慢说明。先来看看该结构体的定义,- struct kobj_map {
- struct probe {
- struct probe *next; /* 被255整除后相同的设备号链成一个单向链表 */
- dev_t dev; /* 字符设备驱动的设备号,包含主设备号(高12位)和次设备号(低20位) */
- unsigned long range; /* 次设备号范围 */
- struct module *owner; /* 表明模块的归属,是THIS_MODULE */
- kobj_probe_t *get; /* 这里可以保存传进来的base_probe函数指针,可回调 */
- int (*lock)(dev_t, void *); /* 保存回调函数,具体是啥,后续会说到 */
- void *data;
- } *probes[255]; /* 虽然大小只有255,但采用了链表的形式,可以支持到4096个主设 */
- struct mutex *lock; /* 保存全局互斥锁,用于关键区域的保护 */
- };
复制代码 我们再来看看kobj_map_init()函数里头做了什么,该函数是这样的,- struct kobj_map *kobj_map_init(kobj_probe_t *base_probe, struct mutex *lock)
- {
- struct kobj_map *p = kmalloc(sizeof(struct kobj_map), GFP_KERNEL);
- struct probe *base = kzalloc(sizeof(*base), GFP_KERNEL);
- int i;
- if ((p == NULL) || (base == NULL)) {
- kfree(p);
- kfree(base);
- return NULL;
- }
- base->dev = 1;
- base->range = ~0; /* 初始的范围很大 */
- base->get = base_probe; /* 保存函数指针 */
- for (i = 0; i < 255; i++)
- p->probes[i] = base; /* 所有指针都指向同一个base */
- p->lock = lock;
- return p;
- }。
复制代码 该函数只是分配了一个结构体struct kobj_map,并做了初始化,保存了函数指针base_probe和全局锁lock。
下面就按照驱动注册流程一个个解析这些例程调用吧。首先是alloc_chrdev_region()函数,解析它之前,先看看结构体(定义了255个结构体指针),- static struct char_device_struct {
- /*被255整除后相同的设备号链成一个单向链表*/
- struct char_device_struct *next;
- unsigned int major; /* 主设备号 */
- unsigned int baseminor; /* 次设备起始号 */
- int minorct; /* 次设备号范围 */
- char name[64]; /* 驱动的名字 */
- struct file_operations *fops; /* 保存文件操作指针,目前没有使用 */
- struct cdev *cdev; /* will die */ /*目前没有使用*/
- } *chrdevs[CHRDEV_MAJOR_HASH_SIZE]; /* CHRDEV_MAJOR_HASH_SIZE = 255 */
复制代码 它的作用仅仅是用于注册字符设备驱动,保存已经注册字符驱动的一些信息,如主次设备号,次设备号的数量,驱动的名字等,便于字符设备驱动注册时索引查找。alloc_chrdev_region()函数很简单,通过调用__register_chrdev_region()来实现,通过英语注释你也可以明白,这个函数有两个作用,一是,如果主设备号为0,则分配一个最近的主设备号,返回给调用者;二是,如果主设备号不为0,则占用好该主设备号对应的位置,返回给调用者。如下,- 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 (cd == NULL)
- return ERR_PTR(-ENOMEM);
- mutex_lock(&chrdevs_lock); /* 这下看到了吧,加锁,就允许你一个人进来 */
- /* temporary */
- if (major == 0) { /* 如果主设备号为零,则找一个最近空闲的号码分配 */
- 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;
- strncpy(cd->name,name, 64);
- i = major_to_index(major);
- /* 如果主设备号不为0,则占用好该主设备号对应的位置 */
- for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
- if ((*cp)->major > major ||
- ((*cp)->major == major && (*cp)->baseminor >= baseminor))
- break;
- if (*cp && (*cp)->major == major &&
- (*cp)->baseminor < baseminor + minorct) {
- 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);
- }
复制代码 接着是cdev_init()函数,先说说cdev的结构体,- struct cdev {
- struct kobject kobj; /* 不多解释了,看看鄙人前面写的文章吧 */
- struct module *owner; /* 模块锁定和加载时用得着 */
- const struct file_operations *ops; /* 保存文件操作例程结构体 */
- struct list_head list; /* open时,会将其inode加到该链表中,方便判别是否空闲 */
- dev_t dev; /* 设备号 */
- unsigned int count;
- };
复制代码 cdev结构体把字符设备驱动和文件系统相关联,后面解析字符设备驱动怎样运行的时候会详谈。
cdev_init()函数如下,- void cdev_init(struct cdev *cdev, const struct file_operations *fops)
- {
- memset(cdev, 0, sizeof *cdev);
- INIT_LIST_HEAD(&cdev->list);
- cdev->kobj.ktype = &ktype_cdev_default; /* 卸载驱动时会用到,别急,后面详讲 */
- kobject_init(&cdev->kobj);
- cdev->ops = fops; /* 用户写的字符设备驱动fops就保存在这了 */
- }。
复制代码 你也看到了,该函数就是对变量做了初始化,关于kobject的解析,建议你看看鄙人博客上写的《Linux设备模型浅析之设备篇》和《Linux设备模型浅析之驱动篇》两篇文章,这里就不详谈了。
用户的fops,在本文中是test_fops,一般形式是这样的,
- static const struct file_operations test_fops = {
- .owner = THIS_MODULE,
- .open = test_fops_open,
- .release = test_fops_release,
- .ioctl = test_fops_ioctl,
- .read = test_fops_read,
- .write = test_fops_write,
- };
复制代码
接着又调用了函数cdev_add(),这个函数又调用了kobj_map()函数,其作用就是分配一个struct probe结构体,填充该结构体中的变量并将其加入到全局的cdev_map中,说白了,就是分个一亩三分田给该字符设备驱动,并做好标记,放到主设备号对应的地方,等主人下次来找的时候能找到(使用kobj_lookup()函数,后面会讲到)。该函数是这样的,- 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)
- {
- 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(sizeof(struct probe) * n, GFP_KERNEL);
- if (p == NULL)
- return -ENOMEM;
- /* 填充些私有的东西 */
- for (i = 0; i < n; i++, p++) {
- p->owner = module;
- p->get = probe; /* 是exact_match ()函数,获取cdev结构体的kobject指针 */
- p->lock = lock; /* 是exact_lock()函数,增加引用*/
- p->dev = dev;
- p->range = range;
- p->data = data; /* cdev保存到p->data中 */
- }
- mutex_lock(domain->lock);
- /* 将这一亩三分田加到主设备号对应的位置上去 */
- for (i = 0, p -= n; i < n; i++, p++, index++) {
- struct probe **s = &domain->probes[index % 255];
- while (*s && (*s)->range < range)
- s = &(*s)->next;
- p->next = *s;
- *s = p;
- }
- mutex_unlock(domain->lock);
- return 0;
- }
复制代码 接下来有class_create()函数和class_device_create()函数,前者生成一个名字为"testchar"的class,后者作用就是在/dev目录下生成设备节点,当然,需要uevent和UDEVD的支持,具体可见鄙人博客上的文章《Linux设备模型浅析之uevent篇》。
顺带说下register_chrdev()函数,其也是注册字符设备驱动,只不过是封装好的,包含了所有前面讲的注册步骤——分配一个设备号,由一个主设备号和255个次设备号组成。如下,- int register_chrdev(unsigned int major, const char *name,
- const struct file_operations *fops)
- {
- struct char_device_struct *cd;
- struct cdev *cdev;
- char *s;
- int err = -ENOMEM;
- cd = __register_chrdev_region(major, 0, 256, 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);
- for (s = strchr(kobject_name(&cdev->kobj),'/'); s; s = strchr(s, '/'))
- *s = '!';
-
- err = cdev_add(cdev, MKDEV(cd->major, 0), 256);
- if (err)
- goto out;
- cd->cdev = cdev;
- return major ? 0 : cd->major;
- out:
- kobject_put(&cdev->kobj);
- out2:
- kfree(__unregister_chrdev_region(cd->major, 0, 256));
- return err;
- }
复制代码 |
|