字符设备管理机制分析(一)

来源:互联网 发布:淘宝卖家怎么回评买家 编辑:程序博客网 时间:2024/05/01 11:20

       字符设备是linux最常见的一种设备类型,也是相对比较简单的。Linux文件系统的注册是先注册文件系统的类型,比如sysfs、devtmpfs、rootfs等,然后再初始化一个文件系统实例添加进系统中。字符设备的注册其实也是采用这种方式:先向系统注册一个字符设备类型,包括主次设备号,文件操作集等,然后才可能创建一个该类型的字符设备实例添加进系统中,当然这些同类型的实例都是依靠次设备号来区分的。

 

一、          字符设备注册相关函数

1.1    高度封装的函数 @ kernel/fs/char_dev.c

static inline int register_chrdev(unsigned int major, const char *name,

                              const struct file_operations *fops)

eg:  register_chrdev(INPUT_MAJOR, "input", &input_fops);

第一个参数是该类字符设备的主设备号,例如这里的INPUT_MAJOR是13,那么如果调用该函数注册字符设备的时候传入的是0,表示需要让系统动态分配一个主设备号给该类字符设备,并将这个主设备号返回。

每类字符设备的此设备号从0~255,也就是说,每类字符设备最多可存在256个设备实例。

      

       static inline void unregister_chrdev(unsigned int major, const char *name)

这个函数是注销系统中的一类字符设备,需要传入该类字符设备的主设备号和名字。

      

1.2    分散调用的函数

其上上面的函数都是对接下来要描述的函数的封装,我们注册字符设备的时候可以单独调用下面的函数来一步一步实现。

 

1.      注册或者分配主次设备号

如果需要系统动态分配主设备号的话,推荐调用下面的函数来实现:

int alloc_chrdev_region(dev_t *dev,  unsigned baseminor,  unsigned count,

const char *name)

              第一个参数是dev_t变量的指针,接受该函数即将返回的主次设备号组合;第二个

参数baseminor和第三个参数count分别表示最小次设备号和支持的次设备号的个数;最后一个参数是该类字符设备的名字。

              eg:alloc_chrdev_region(&fm->dev_t, 0, 1, FM_NAME); 只支持一个该类型设备实例

              返回0表示成功执行,返回一个负数则是出错。

             

              如果主设备号已知,就可以调用以下两种函数来注册:

              int register_chrdev_region(dev_t from, unsigned count, const char *name)

              static struct char_device_struct *

__register_chrdev_region(unsigned int major, unsigned int baseminor,

                                                int minorct, const char *name)

实际上register_chrdev_region()函数时调用了__register_chrdev_region()函数来实现的,只是返回值更简单明了而已,推荐使用函数register_chrdev_region()。例如:

register_chrdev_region(MKDEV(INPUT_MAJOR, 0),255,"input");

 

2.      cdev分配或者初始化

struct cdev是字符设备的关键结构体,这一步就是需要分配或者初始化该结构体。如果

cedv已经有现成的了,那么就可以直接调用函数cdev_init()来初始化,如果还没有分配cdev的空间那么就需要调用函数cdev_alloc()来分配空间并初始化。

       struct cdev *cdev_alloc(void);

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

       使用cdev_alloc()函数的,记得在执行该函数之后需要对cdev->ops进行初始化,而在cdev_init()中有做这个工作。

 

       3. 向系统注册这个字符设备类型

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

       eg: cdev_add(cdev,  MKDEV(cd->major, baseminor),  count);

 

1.3  MKDE宏

       该宏的实现是: #define MKDEV(ma,mi)      ((ma)<<8 | (mi))

       可以看出,linux系统中主设备号最大用24位表示,而次设备号只有256个,也就是说每一种字符设备最多可以有256个设备实例存在。

 

二、原理实现

       以register_chrdev(INPUT_MAJOR, "input", &input_fops)这个为例。

      

       2.1 字符设备类型注册

       static inline int register_chrdev(unsigned int major, const char *name,

                                                      const struct file_operations *fops)

{

              return __register_chrdev(major, 0, 256, name, fops);

              // 256个次设备号

}

       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);

       …

       cdev = cdev_alloc();

       …

       cdev->owner = fops->owner;

       cdev->ops = fops;                                                  // eg: &input_fops

       kobject_set_name(&cdev->kobj, "%s", name);       // eg: "input"

             

       err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);

       …

       cd->cdev = cdev;

 

       return major ? 0 : cd->major;

       …

}

      

       2.1.1 主次设备号注册

       这里可以使用第一节中出现的三个函数中alloc_chrdev_region、register_chrdev_region、__register_chrdev_region的任意一个来实现这个,只是在调用时注意入口和出口参数的不同。这里以函数__register_chrdev_region()为例来介绍,在继续之前我们先来聊一下关于这个函数的关键。

       static struct char_device_struct {

              struct char_device_struct *next;

              unsigned int major;

              unsigned int baseminor;

              int minorct;

              char name[64];

              struct cdev *cdev;

} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

       @ kernel/include/linux/fs.h

       #define CHRDEV_MAJOR_HASH_SIZE 255

      

       chrdevs是一个大小为255的char_device_struct类型的指针数组,在字符设备的管理中,没有将其当做简单的指针数组来是使用,而是将其作为一个以主设备号对255的余数为键值的hash table在使用,也就是说,这个指针数组的每一个元素(下标为i)如果不为NULL的话,都可以将其作为一个链表来链接那些主设备号是i的倍数的字符设备的char_device_struct结构体(这就是next域的作用)。

       那么接下来的这个函数的作用简单的来说,就是在获得chrdevs_lock互斥锁的情况下操作这个chrdevs hash table。具体如何操作,请看下面的源码分析:

       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, i;

      

       cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);

       …

       mutex_lock(&chrdevs_lock);

       /* 如果主设备号是0,表示需要系统动态分配一个未占用的主设备号 */

       if (major == 0) {

              for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {

// 从chrdevs数组的最后开始动态分配,直到遇到一个未占用的指针数组元素。

                     if (chrdevs[i] == NULL)

                            break;

              }

              if (i == 0) {  // 全部都已被分配,没找到一个可用的。

                     ret = -EBUSY;

                     goto out;

              }

              major = i;  // 保存分配的主设备号

              ret = major;

       }

       // 为结构体char_device_struct各域赋值,保存major,次设备号范围,name。

       cd->major = major;

       cd->baseminor = baseminor;

       cd->minorct = minorct;

       strlcpy(cd->name, name, sizeof(cd->name));

      

       i = major_to_index(major); // major对 255取余数的结果得到查询hash表的键值。

       /*hash表的每一项,都是一个无头链表,链表中的每一个元素(char_device_struct结构体)是按照字符设备的major从小到大排列;major可以相同,这里依据次设备号大小排列。这里次设备号绝对不能有任何交叉和重叠区域,否则就会注册失败。*/

       // cp是一个二级指针,*cp是指向char_device_struct的指针,*cp可以是NULL。

 

       for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)

              if ((*cp)->major > major ||

                  ((*cp)->major == major &&

                   (((*cp)->baseminor >= baseminor) ||

                    ((*cp)->baseminor + (*cp)->minorct > baseminor))))

                     break;

/* 这里查找有些复杂,大致可以分成以下几种情况:

1.      (*cp)->major > major,表示新注册的major可以插入到链表中,那么将掉过下面的if条件直接将major对应的char_device_struct插入链表。

2.      上面的for循环没有被break过,而是因为*cp = NULL才退出循环的,这个时候表名新加入的major是当前链表中最大的,这样也会直接跳过接下来的if语句,而将这个major对应的char_device_struct插入链表尾。

3.      (*cp)->major == major,表示主设备号相等,所以接下来就是要判断次设备号是否会有冲突的可能:

(*cp)->major == major &&(((*cp)->baseminor >= baseminor) ||

                    cp)->baseminor + (*cp)->minorct > baseminor))

       用上面的语句来检查次设备号的冲突情况,举个例子来说:假设当前的*cp对应的字符设备的次设备号范围是{32, 64},那么这里将会检测出新字符设备的baseminor为小于64的情况(小于32或者在32和64之间),因为这两种情况将可能会和原来的次设备号有互相覆盖,所以这两种情况都会进入下面的if条件来进一步检测。

       不过这里如果新字符设备的baseminor是大于64的话,那么就不可能发生覆盖现象,所以这种情况是不会break掉上面的for循环,直到遇到链表尾或者baseminor处于{32, 64}的另两个区间内才会退出for循环。

New             Old

         [68,90]                  {32,64}    --        {         }    [  ]

*/

 

       /* 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                        Old

       1. [16,24]                  {32,64}              --    [  ] {          }

       2. [28,36]                  {32,64}              --    [    {  ]       }

       3. [40,48]                  {32,64}              --         {   [  ]   }

       4. [54,70]                  {32,64}              --         {       [  }   ]

*/

              /* New driver overlaps from the left.  */

              if (new_max >= old_min && new_max <= old_max) {  // 检测出第2、3中情况

                     ret = -EBUSY;

                     goto out;

              }

              /* New driver overlaps from the right.  */

              if (new_min <= old_max && new_min >= old_min) {  // 检测出第4中情况

                     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);

}

 

2.1.2 cedv结构体初始化

一个特定类型的字符设备用结构体cdev来描述,结构体定义如下:

       struct cdev {

              struct kobject kobj;

              struct module *owner;

              const struct file_operations *ops;

              struct list_head list;

              dev_t dev;

              unsigned int count;

};

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;

}

cdev->owner = fops->owner;

cdev->ops = fops;           // eg: &input_fops

kobject_set_name(&cdev->kobj, "%s", name); // eg: "input"

      

       2.1.3 向系统添加一个cdev描述的字符设备类型

       cdev_add(cdev, MKDEV(cd->major, baseminor), count);

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

{

       p->dev = dev;               // 设备号的base值

       p->count = count;        // 次设备号的个数

       return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);

}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

kobj_map()是另外一个重点函数,关于这一个知识点,有必要从其init来分析一下。

void __init chrdev_init(void)

{

       cdev_map = kobj_map_init(base_probe, &chrdevs_lock);

       bdi_init(&directly_mappable_cdev_bdi);

}

static struct kobject *base_probe(dev_t dev, int *part, void *data)

{

       if (request_module("char-major-%d-%d", MAJOR(dev), MINOR(dev)) > 0)

              /* Make old-style 2.4 aliases work */

              request_module("char-major-%d", MAJOR(dev));

       return NULL;

}

static struct kobj_map *cdev_map;

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;

       } *probes[255];   // 大小为255的指针数组

       struct mutex *lock;

};

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;

       p->lock = lock;

       return p;

}

实际上cdev_map->probes这个指针数组也是会作为一个hash表来使用。这个函数kobj_map_init就是对这个hash表的每一项都初始化成执行同一个probe结构体(base指针所指的probe结构体)。从后面的kobj_map()的代码中可以看出,这个probe结构体是处于每一个链表的末尾。

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

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;  // n = 1

       unsigned index = MAJOR(dev);                                           // 主设备号

       unsigned i;

       struct probe *p;

       // 该函数常见的用法是对cdev一个一个地进行map

       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;            // eg: NULL

              p->get = probe;                   // eg: exact_match

              p->lock = lock;                    // eg: exact_lock

              p->dev = dev;                      // 主设备号

              p->range = range;                // 次设备号个数

              p->data = data;                    // 主设备号对应的cdev结构体

       }

       mutex_lock(domain->lock);

       // 而这一个循环也只是循环一次,就是为了将上面初始化好的probe函数添加到hash表对应的链表中去。

       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;

}

       到这里,一个字符设备类型注册就这样结束了,接下来我们看一下,如何添加一个真实的字符设备实例。

...