linux设备驱动模型一字符设备 驱动简析

来源:互联网 发布:painter 2017 mac 编辑:程序博客网 时间:2024/04/27 23:18
字符设备不需要复杂的缓冲策略,也不涉及磁盘高速缓存,处理起来比较容易

字符设备驱动程序由一个cdev结构描述:

struct cdev {struct kobject kobj;          // 每个 cdev 都是一个 kobjectstruct module *owner;       // 指向实现驱动的模块const struct file_operations *ops;   // 操纵这个字符设备文件的方法struct list_head list;       // 与 cdev 对应的字符设备文件的 inode->i_devices 的链表头dev_t dev;                   //设备号,int 类型,高12位为主设备号,低20位为次设备号unsigned int count;       // 设备范围号大小};
list字段是双向循环链表的首部,用于收集相同字符设备驱动程序所对应的字符设备文件的索引节点。可能很多设备文件具有相同的设备号,并对应于相同的字符设备。此外,一个设备驱动程序对应的设备号可以是一个范围,而不仅仅是一个号;设备号位于同一范围内的所有设备文件由同一个字符设备驱动程序处理
一个 cdev 一般它有两种定义初始化方式:静态的和动态的。
静态内存定义初始化:
struct cdev my_cdev;
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
动态内存定义初始化:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &fops;
my_cdev->owner = THIS_MODULE;
两种使用方式的功能是一样的,只是使用的内存区不一样,一般视实际的数据结构需求而定。

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;}void cdev_init(struct cdev *cdev, const struct file_operations *fops){memset(cdev, 0, sizeof *cdev); 注1;   INIT_LIST_HEAD(&cdev->list);kobject_init(&cdev->kobj, &ktype_cdev_default);cdev->ops = fops;}
由代码可以看到,两个函数功能基本上是一致的,都是初始化内部的kobject,只是cdev_init还给ops进行了初始化。
下面以字符设备的注册注册过程为主线介绍其内部调用及相关结构。
首先看一下设备编号的内部表示
在内核中, dev_t 类型(在 <linux/types.h>中定义)用来持有设备编号 -- 主次部分都包括. 对于 2.6.0 内核, dev_t 是 32 位的量, 12 位用作主编号, 20 位用作次编号,在 <linux/kdev_t.h>中的一套宏定义. 为获得一个 dev_t 的主或者次编号, 使用:
MAJOR(dev_t dev); 
MINOR(dev_t dev);
相反, 如果你有主次编号, 需要将其转换为一个 dev_t, 使用:
MKDEV(int major, int minor); 
好了,进入正题,对照前面  linux设备驱动模型一字符设备 驱动实例中的init中,下面就是设备号的分配了,有两种方式,register_chrdev_region和alloc_chrdev_region
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接收三个参数:初始设备号(主+次)请求设备号范围大小,以及这个范围内的设备号对应的设备驱动的名称。当次设备号数目过多(count过多)的时候,次设备号可能会溢出到下一个主设备。因此我们在for语句中可以看到,首先得到下一个主设备号(其实也是一个设备号,只不过此时的次设备号为0)并存储于next中。然后判断在from的基础上再追加count个设备是否已经溢出到下一个主设备号。如果没有溢出(next小于to),那么整个for语句就只执行个一次__register_chrdev_region函数;否则当设备号溢出时,会把当前溢出的设备号范围划分为几个小范围,分别调用__register_chrdev_region函数。
如果在某个小范围调用__register_chrdev_region时出现了失败,那么会将此前分配的设备号都释放。
再来看下__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);//配内存并用零来填充(这就是用kzalloc而不是kmalloc的原因)if (cd == NULL)return ERR_PTR(-ENOMEM);mutex_lock(&chrdevs_lock);/* temporary */if (major == 0) {//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;//若找到后,那么i不仅代表这组设备的主设备号,也代表其在散列表中的关键字ret = major;}cd->major = major;//将参数中的值依次赋给cd变量的对应字段cd->baseminor = baseminor;cd->minorct = minorct;strlcpy(cd->name, name, sizeof(cd->name));i = major_to_index(major);//除模255运算for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)//寻找正确的位置if ((*cp)->major > major || //主设备号(*(cp)->major)大于我们所分配的主设备号(major)(有序)    ((*cp)->major == major &&//如果(*cp)结点和cd结点的主设备号相同     (((*cp)->baseminor >= baseminor) || //前者的次设备号起点比cd结点的大      ((*cp)->baseminor + (*cp)->minorct > baseminor))))//cd结点的次设备号起点小于(*cp)结点的次设备号的终点break;/* Check for overlapping minor ranges.  */if (*cp && (*cp)->major == major) {//主设备号相同时才需要冲突判断int old_min = (*cp)->baseminor;//计算出新老次设备号的范围,int old_max = (*cp)->baseminor + (*cp)->minorct - 1;int new_min = baseminor;int new_max = baseminor + minorct - 1;/* 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;//将char_device_struct描述符插入到中途链表中*cp = cd;mutex_unlock(&chrdevs_lock);return cd;out:mutex_unlock(&chrdevs_lock);kfree(cd);return ERR_PTR(ret);}

这里有一个结构char_device_struct,用于注册字符设备驱动,保存已经注册字符驱动的一些信息,如主次设备号,次设备号的数量,驱动的名字等,便于字符设备驱动注册时索引查找

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,它是动态的分配一个主设备号

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。
设备号分配成功了之后就是分配cdev结构了,及初始化,可以直接调用cdev_alloc进行分配和初始化,也可以先分配空间,然后调用cdev_init进行初始化
初始化完成之后 就要调用cdev_add在设备驱动程序模型中注册一个cdev描述符。
int cdev_add(struct cdev *p, dev_t dev, unsigned count){p->dev = dev;p->count = count;return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);}
它初始化dev和count字段,然后调用kobj_map依次建立设备驱动程序模型的数据结构,把设备号范围复制到设备驱动程序的描述符中。
cdev_map是在chrdev_init时候分配的
void __init chrdev_init(void){cdev_map = kobj_map_init(base_probe, &chrdevs_lock);bdi_init(&directly_mappable_cdev_bdi);}
看一下kobj_map结构
struct kobj_map {        struct probe {                struct probe *next;         /* 散列冲突表中的下一个元素 */                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()函数传进去了两个参数,base_probe函数和chrdevs_lock互斥变量,返回一个struct kobj_map类型的指针。base_probe调用了request_module()函数,用于加载与字符驱动相关的驱动程序,被加载的驱动命名方式是char-major-主设备号-次设备号。request_module()函数,你可以看看该函数上头的英文注释,它最终会调用应用层空间的modprobe命令来加载驱动程序。实际上,没有使用该项功能
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;}
回到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;//根据设备号和范围,计数出需要多少个probe结构(range没有超过255的话一个就够了)unsigned index = MAJOR(dev);unsigned i;struct probe *p;if (n > 255)n = 255;p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);//分配probe结构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;  //把cdev结构保存在了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)//保证probe中ranger大的靠后s = &(*s)->next;p->next = *s;*s = p;}mutex_unlock(domain->lock);return 0;}

到这里,该字符设备就注册进去了


字符设备驱动注册另外一个经常使用的函数 就是register_chrdev,它的参数为一个主设备号,驱动程序名字以及一个指针fops,它会分配该主设备相关的0~255的次设备号范围并进行注册,建立一个缺省的 cdev 结构. 使用这个接口的驱动必须准备好处理对所有 256 个次编号的 open 调用( 不管它们是否对应真实设备 ), 它们不能使用大于 255 的主或次编号.

static inline int register_chrdev(unsigned int major, const char *name,  const struct file_operations *fops){return __register_chrdev(major, 0, 256, name, fops);}
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();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;}
这个函数和前面讲的驱动注册基本是一致的,只是把一些流程给综合起来了。


再总结一下三个主要的结构: