您的位置:首页 > 运维架构 > Linux

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

2011-07-24 17:33 369 查看
       字符设备是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;
}
       到这里,一个字符设备类型注册就这样结束了,接下来我们看一下,如何添加一个真实的字符设备实例。
...
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息