现在的位置: 首页 > 综合 > 正文

linux 字符设备驱动框架

2018年02月07日 ⁄ 综合 ⁄ 共 7332字 ⁄ 字号 评论关闭

一、字符设备结构及原理
1.内核内部使用struct cdev结构来表示字符设备。在内核调用设备的操作之前,必须分配并注册一个或多个struct cdev。

struct cdev {
   struct kobject kobj;                           //每个 cdev 都是一个 kobject
   struct module *owner;                          //指向实现驱动的模块
   const struct file_operations *ops;             //操纵这个字符设备文件的方法
   struct list_head list;                         //与 cdev对应的字符设备文件的 inode->i_devices 的链表头
   dev_t dev;                                     //起始设备编号
   unsigned int count;                            //设备范围号大小
};

2.内核中所有已分配的字符设备编号都记录在一个名为 chrdevs 散列表里。该散列表中的每一个元素是一个 char_device_struct 结构,它的定义如下:

static struct char_device_struct {
   struct char_device_struct *next;         // 指向散列冲突链表中的下一个元素的指针
   unsigned int major;                      // 主设备号
   unsigned int baseminor;                  // 起始次设备号
   int minorct;                             // 设备编号的范围大小
   char name[64];                           // 处理该设备编号范围内的设备驱动的名称
   struct file_operations *fops;            // 没有使用
   struct cdev *cdev;                       // 指向字符设备驱动程序描述符的指针
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

注意,内核并不是为每一个字符设备编号定义一个 char_device_struct 结构,而是为一组对应同一个字符设备驱动的设备编号范围定义一个 char_device_struct 结构。chrdevs 散列表的大小是255,散列算法是把每组字符设备编号范围的主设备号以 255 取模插入相应的散列桶中。同一个散列桶中的字符设备编号范围是按起始次设备号递增排序的。

3. kobj_map结构体是用来管理设备号及其对应的设备的。 内核中所有都字符设备都会记录在一个 kobj_map 结构的 cdev_map 变量中。这个结构的变量中包含一个散列表用来快速存取所有的对象。kobj_map() 函数就是用来把字符设备编号和 cdev 结构变量一起保存到 cdev_map 这个散列表里。

当后续要打开一个字符设备文件时,通过调用 kobj_lookup() 函数,根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段。

kobj_map()函数就是将指定的设备号加入到该数组,kobj_lookup()则查找该结构体,然后返回对应设备号的kobject对象,利用该kobject对象,我们可以得到包含它的对象如cdev。

struct probe *probes[255];。
struct kobj_map {
    struct probe {
     struct probe *next;   //这样形成了链表结构
     dev_t dev;            //设备号
     unsigned long range;  //设备号的范围 
     struct module *owner;
     kobj_probe_t *get;
     int (*lock) (dev_t, void *);
     void *data;           //指向struct cdev对象
    } *probes[255];
    struct mutex *lock;
}

 int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
    int error;
    
    p->dev = dev;
    p->count = count;
    
    //将cdev结构添加到cdev_map的数组中
    error = kobj_map(cdev_map, dev, count, NULL,exact_match, exact_lock, p);
    if (error)
        return error;
    //父kobject结构计数加1
    kobject_get(p->kobj.parent);    
    return 0;
}

//内核中所有都字符设备都会记录在一个 kobj_map 结构的 cdev_map 变量中。这个结构的变量中包含一个散列表用来快速存取所有的对象。
//kobj_map() 函数就是用来把字符设备编号和 cdev 结构变量一起保存到 cdev_map 这个散列表里。当后续要打开一个字符设备文件时,通过调用 kobj_lookup() 函数,
//根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段。

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)
{
        //dev_t的前12位为主设备号,后20位为次设备号。
        //n = MAJOR(dev + range - 1) - MAJOR(dev) + 1 表示设备号范围(dev, dev+range)中不同的主设备号的个数。通常n的值为1。
    unsigned n = MAJOR(dev+range-1) - MAJOR(dev) + 1;
    unsigned index = MAJOR(dev);//主设备号
    unsigned i;
    struct probe *p;


    if (n > 255)//若n > 255,则超出了kobj_map中probes数组的大小 
        n = 255;
    p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);//分配n个struct probe
    if(p == NULL)
        return -ENOMEM;
       
    for(i = 0; i < n; i++, p++) {//用函数的参数初始化probe
        p->owner = module;
        p->get = probe;
        p->lock = lock;
        p->dev = dev;
        p->range = range;
        p->data = data;//保存的是cdev结构
    }
    mutex_lock(domain->lock);
    
    //从for循环可以看出kobj_map中的probes数组中每个元素为一个struct probe链表的头指针。 
    for(i = 0, p-=n; i < n; i++, p++, index++) {
        struct probe **s = &domain->probes[index % 255];//从数组中找到主设备号为index的probe结构链表,在此链表中每个probe结构都是相同的主设备号index
        //链表中的元素是按照range值从小到大排列的。while循环即是找出该将p插入的位置。
        while(*s && (*s)->range < range)
            s = &(*s)->next;
        p->next = *s;
        *s = p;//插入链表
    }
    mutex_unlock(domain->lock);
    return 0;
}

二、字符设备的注册
1.一个 cdev 一般它有两种定义初始化方式:静态的和动态的。
   (1)静态内存定义初始化:
         struct cdev my_cdev;
         cdev_init(&my_cdev, &fops);
         my_cdev.owner = THIS_MODULE;
   (2)动态内存定义初始化
         struct cdev *my_cdev = cdev_alloc();
         my_cdev->ops = &fops;
         my_cdev->owner = THIS_MODULE;
   两种使用方式的功能是一样的,只是使用的内存区不一样,一般视实际的数据结构需求而定。

2.注册一个独立的cdev设备的基本过程如下:

   (1)、为struct cdev 分配空间(如果已经将struct cdev 嵌入到自己的设备的特定结构体中,并分配了空间,这步略过!)

        struct cdev *my_cdev = cdev_alloc();

        my_cdev->ops=&my_ops;

   (2)、初始化struct cdev

        void cdev_init(struct cdev *cdev, const struct file_operations *fops)

   (3)、初始化cdev.owner
        cdev.owner = THIS_MODULE;

   (4)、添加cdev,通知内核struct cdev的信息(在执行这步之前必须确定你对struct cdev的以上设置已经完成!)

        int cdev_add(struct cdev *p, dev_t dev, unsigned count)

       //p是cdev结构, dev是这个设备响应的第一个设备号, count 是应当关联到设备的设备号的数目. 常常 count 是 1, 
       //在使用 cdev_add 是有几个重要事情要记住. 第一个是这个调用可能失败. 如果它返回一个负的错误码, 你的设备没有增加到系统中. 它几乎会一直成功, 但是, 并          且带起了其他的点: cdev_add 一返回, 你的设备就是"活的"并且内核可以调用它的操作. 除非你的驱动完全准备好处理设备上的操作, 你不应当调用 cdev_add.

三、分配设备号

内核提供了三个函数来注册一组字符设备编号,这三个函数分别是 register_chrdev_region()、alloc_chrdev_region() 和 register_chrdev()。这三个函数都会调用一个共用的__register_chrdev_region() 函数来注册一组设备编号范围(即一个 char_device_struct 结构)。

register_chrdev_region( )  //分配指定的设备号范围
alloc_chrdev_region( )       //动态分配设备范围
register_chrdev( )               //申请指定的设备号,并且将其注册到字符设备驱动模型中.是一个老式分配设备编号范围的函数

//内核中所有已分配的字符设备编号都记录在一个名为 chrdevs 散列表里。该散列表中的每一个元素是一个 char_device_struct 结构
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);//分配一个新的 char_device_struct 结构
   if (cd == NULL)
       return ERR_PTR(-ENOMEM);
   mutex_lock(&chrdevs_lock);
   
   //如果申请的设备编号范围的主设备号为 0,那么表示设备驱动程序请求动态分配一个主设备号。
   //动态分配主设备号的原则是从散列表的最后一个桶向前寻找,那个桶是空的,主设备号就是相应散列桶的序号。
   //所以动态分配的主设备号总是小于 256,如果每个桶都有字符设备编号了,那动态分配就会失败。
   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;
   }
   //根据参数设置 char_device_struct 结构中的初始设备号,范围大小及设备驱动名称。
   cd->major = major;
   cd->baseminor = baseminor;
   cd->minorct = minorct;
   strncpy(cd->name,name, 64);
   i = major_to_index(major);
   
   //计算出主设备号所对应的散列桶,为新的 char_device_struct 结构寻找正确的位置。同时,如果设备编号范围有重复的话,则出错返回。
   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;
       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;
       }
   }
   //将新的 char_device_struct 结构插入散列表中,并返回 char_device_struct 结构的地址。
   cd->next = *cp;
   *cp = cd;
   mutex_unlock(&chrdevs_lock);
   return cd;
out:
   mutex_unlock(&chrdevs_lock);
   kfree(cd);
   return ERR_PTR(ret);
}

四.注册cdev设备老方法
    1.如果你深入浏览 2.6 内核的大量驱动代码, 你可能注意到有许多字符驱动不使用我们刚刚描述过的 cdev 接口. 你见到的是还没有更新到 2.6 内核接口的老代码. 因为那个代码实际上能用, 这个更新可能很长时间不会发生. 为完整, 我们描述老的字符设备注册接口, 但是新代码不应当使用它; 这个机制在将来内核中可能会消失.
注册一个字符设备的经典方法是使用:
     int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
这里, major 是感兴趣的主设备号, name 是驱动的名子(出现在 /proc/devices), fops 是缺省的 file_operations 结构. 一个对 register_chrdev 的调用为给定的主编号注册 0 - 255 的次编号, 并且为每一个建立一个缺省的 cdev 结构. 使用这个接口的驱动必须准备好处理对所有 256 个次编号的 open 调用( 不管它们是否对应真实设备 ), 它们不能使用大于 255 的主或次编号.register_chrdev函数的major参数如果等于0,则表示采用系统动态分配的主设备号。
它所做的事情为:
(1). 注册设备号, 通过调用 __register_chrdev_region() 来实现
(2). 分配一个cdev, 通过调用 cdev_alloc() 来实现
(3). 将cdev添加到驱动模型中, 这一步将设备号和驱动关联了起来. 通过调用 cdev_add() 来实现
(4). 将第一步中创建的 struct char_device_struct 对象的 cdev 指向第二步中分配的cdev. 由于register_chrdev()是老的接口,这一步在新的接口中并不需要.

2.如果你使用 register_chrdev, 从系统中去除你的设备的正确的函数是:
int unregister_chrdev(unsigned int major, const char *name);//major 和 name 必须和传递给 register_chrdev 的相同, 否则调用会失败.

五、字符设备驱动模板
(1)设置驱动文件操作结构体
static struct file_operations XXX_fops =
{
   .owner = THIS_MODULE,
   .read = xxx_read,
   .write = xxx_write,
   .ioctl = xxx_ioctl,
    ...
};
(2)编写字符设备驱动模块加载与卸载函数

    static int _ _init xxx_init(void){                            //模块加载--〉申请设备号,添加设备
        ...
        cdev_init(&xxx_dev.cdev, &xxx_fops);               //初始化cdev
        xxx_dev.cdev.owner = THIS_MODULE;
    
        if (xxx_major){//获取字符设备号
                register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
        }
        else{
                alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
        } 
        ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); //注册设备
        ...//可能申请中断号request_irq
}

static void _ _exit xxx_exit(void){/*设备驱动模块卸载函数*/
    unregister_chrdev_region(xxx_dev_no, 1); //释放占用的设备号
    cdev_del(&xxx_dev.cdev); //注销设备
    ...//释放中断号free_irq
}

抱歉!评论已关闭.