深入浅出linux之字符设备和input设备

来源:互联网 发布:无线网搜索不到网络 编辑:程序博客网 时间:2024/05/21 07:06
 

  Linux把设备分为了字符设备和块设备(网络设备除外),这几乎是个常识了。对做驱动的linux程序员来说,驱动要么是字符设备驱动,要么是块设备驱动。那么什么是字符设备和块设备?本节我们探索一下字符设备。

  一个字符设备可以非常简单,以至于很多人把字符设备当作系统控制的一种手段,通过字符设备的读写和io control函数和内核交换数据。但是实际上,linux内核系统很多情况是把字符设备当作一个框架来用。这就复杂多了。

  为了正确理解和应用,我们需要写一个简单的字符设备例子。不过这里直接从系统选择一个例子,就是input设备。这个设备不但典型,而且具有很大实用价值。

  Input是一个虚拟的设备。在linux系统中,键盘鼠标,触摸屏和游戏杆都要汇入input设备统一处理。

 

一  设备的主从设备号

  Linux系统通过设备号来区分不同的设备。设备号由两部分组成:主设备号和从设备号。

  下面摘录了系统定义的一些主设备号(来自include/linux/major.h):

#define UNNAMED_MAJOR            0

#define MEM_MAJOR        1

#define RAMDISK_MAJOR              1

#define FLOPPY_MAJOR          2

#define PTY_MASTER_MAJOR       2

#define IDE0_MAJOR         3

#define HD_MAJOR           IDE0_MAJOR

#define PTY_SLAVE_MAJOR           3

#define TTY_MAJOR         4

#define TTYAUX_MAJOR         5

#define LP_MAJOR            6

#define VCS_MAJOR          7

#define LOOP_MAJOR              7

#define SCSI_DISK0_MAJOR    8

#define SCSI_TAPE_MAJOR            9

#define MD_MAJOR           9

#define MISC_MAJOR        10

#define SCSI_CDROM_MAJOR 11

#define MUX_MAJOR        11    /* PA-RISC only */

#define XT_DISK_MAJOR        13

#define INPUT_MAJOR             13

  可以看到,本节要讨论的input设备占了第13号主设备号。

  从设备号区分归属于同一个主设备的独立设备。比如,系统中有几个硬盘,那么它们占用了不同的次设备号。

 

  看到这里,也许读者会想到,input设备包括各种各样的输入设备。不说键盘鼠标的不同,就是鼠标之间,也有各种各样的鼠标,难道这些键盘鼠标游戏杆都用的同一个驱动?

 

 

  这个问题,从内核的角度很容易理解,实际上字符设备input是个驱动架构,是设备的一个汇聚层。众多的驱动和设备被input封装了,经过这个封装层之后,键盘鼠标这些设备就各行其是,分别由不同的驱动所控制了。而且不仅仅input是一层封装,从input往下,还有好几层的封装。

 

  这是linux内核设计的一个重要思想。在内核代码的阅读过程中,我们将发现越来越多的这种分层。这也是linux内核设计向面向对象设计靠拢的一个标志,在本文的叙述中,我们也大量使用“对象”这个词来描述内核中各个层次生成的数据结构。就以一个普通的键盘来说,因为键盘属于串口设备(假定,当然也有usb键盘等),所以内核要产生一个串口设备,同时挂载相应的串口驱动。同时键盘属于input设备范畴,它同时也要产生一个input设备,并挂载相应的input驱动。

 

 

二  字符设备:特殊的文件

  回顾前面创建的最简单文件系统。通过aufs_get_inode为每个文件创建它的inode对象。可以看到,对文件和目录都有各自的文件操作指针和inode操作指针。但是缺省情况下,我们用一个init_special_inode来给对象赋值。也就是这里,文件变成了设备,字符设备和块设备开始浮出海面。

 

=================================init_special_inode========================

void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)

{

       inode->i_mode = mode;

       if (S_ISCHR(mode)) {

              inode->i_fop = &def_chr_fops;

              inode->i_rdev = rdev;

       } else if (S_ISBLK(mode)) {

              inode->i_fop = &def_blk_fops;

              inode->i_rdev = rdev;

       } else if (S_ISFIFO(mode))

              inode->i_fop = &def_fifo_fops;

       else if (S_ISSOCK(mode))

              inode->i_fop = &bad_sock_fops;

       else

              printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o)\n",

                     mode);

}

  这段代码很简单,如果是字符设备,则文件操作结构指针被赋值为def_chr_fops,如果是块设备,则被赋值为def_blk_fops。同时inode的i_rdev被赋值为rdev。这个rdev其实就是设备的设备号。这个设备号是由主设备号和从设备号生成的。

 

  这个def_chr_fops是在char_dev.c文件定义的。

const struct file_operations def_chr_fops = {

       .open = chrdev_open,

};

 

=============================chrdev_open===============================

/*

 * Called every time a character special file is opened

 */

int chrdev_open(struct inode * inode, struct file * filp)

{

       struct cdev *p;

       struct cdev *new = NULL;

       int ret = 0;

 

       spin_lock(&cdev_lock);

       p = inode->i_cdev;

   

    /*如果字符设备不存在*/

       if (!p) {

              struct kobject *kobj;

              int idx;

              spin_unlock(&cdev_lock);

        /*通过kobj_lookup查找cdev_map结构包含的链表,试图发现字符设备*/

              kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);

              if (!kobj)

                     return -ENXIO;

        /*调用container方法,获得cdev对象*/

              new = container_of(kobj, struct cdev, kobj);

              spin_lock(&cdev_lock);

        /*再次检查p*/

              p = inode->i_cdev;

              if (!p) {

            /*赋值。Inode的字符设备指针等于发现的字符设备*/

                     inode->i_cdev = p = new;

                     inode->i_cindex = idx;

                     list_add(&inode->i_devices, &p->list);

                     new = NULL;

              } else if (!cdev_get(p))

                     ret = -ENXIO;

       } else if (!cdev_get(p))

              ret = -ENXIO;

       spin_unlock(&cdev_lock);

       cdev_put(new);

       if (ret)

              return ret;

    /*获得注册设备的函数指针,对input设备来说,就是input_fops*/

       filp->f_op = fops_get(p->ops);

       if (!filp->f_op) {

              cdev_put(p);

              return -ENXIO;

       }

    /*这个open就是input设备的input_open_file*/

       if (filp->f_op->open) {

        /*大内核锁*/

              lock_kernel();

              ret = filp->f_op->open(inode,filp);

              unlock_kernel();

       }

       if (ret)

              cdev_put(p);

       return ret;

}

  这个函数前面的英文注释说明了,每次打开一个字符设备的时候,都调用这个函数。可以看到这个函数还是比较简单的,就是通过kobj_lookup搜索前面注册的字符设备对象,找到后加载字符设备模块。然后继续调用字符设备的open函数,找不到返回错误。

 

  为了串联知识点,我们需要回顾一下mknod程序。这个程序的作用是产生一个字符设备文件或者块设备文件。这个程序执行时候需要输入设备类型和设备的主设备号和从设备号。

从内核的角度,可以清楚看到,设备文件其实是个“桩子”文件,它代表一个设备,起作用的参数就是主从设备号和设备类型。当打开文件时,init_special_inode函数在这个设备文件的inode里面保存了设备号和不同的函数指针。然后在chrdev_open真正去调用设备本身的驱动。

  那么chrdev_open调用的设备本身的open函数是从那里来的?这就是设备注册时候注册进来的。下面分析设备的注册过程。

 

三  input设备的注册

  Input设备是个字符设备,它是如何注册进入字符设备的?我们继续看看。

 

=============================input_init===============================

static int __init input_init(void)

{

       int err;

   

    /*input同时要注册类,这部分先跳过*/ 

       err = class_register(&input_class);

       if (err) {

              printk(KERN_ERR "input: unable to register input_dev class\n");

              return err;

       }

    /*在proc目录下创建input相关的文件*/

       err = input_proc_init();

       if (err)

              goto fail1;

 

       err = register_chrdev(INPUT_MAJOR, "input", &input_fops);

       if (err) {

              printk(KERN_ERR "input: unable to register char major %d", INPUT_MAJOR);

              goto fail2;

       }

 

       return 0;

 

 fail2:     input_proc_exit();

 fail1:     class_unregister(&input_class);

       return err;

}

  这里字符设备直接相关的就是register_chrdev函数。

 

================================register_chrdev===========================

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;

 

    /*登记设备占用的区域,区域是主设备号和从设备号共同占有的一段区间,这里的从设备号从0-256。这个区间之前不能被占用*/

       cd = __register_chrdev_region(major, 0, 256, name);

       if (IS_ERR(cd))

              return PTR_ERR(cd);

      

    /*申请一个cdev对象。*/

       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 = '!';

 

       /*实质是cdev加入链表*/  

       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;

}                                                                                                                   

  register_chrdev这个函数实际是两个登记,一个是登记区间,一个是登记字符设备。

首先看看区间的登记。

  Cdev对象里面就有个kobject对象,这个kobject对象最终要通过sysfs文件系统在/sys产生目录。

=====================================================================

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

    /*主设备号为0,说明这个设备没指定设备号,需要分配一个*/

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

   

    /*主设备号计算索引,实际是主设备号除以255的余数。*/  

       i = major_to_index(major);

 

    /*找一个未占用的区间*/

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

}

  Chrdevs是个全局变量,它是个255个元素的指针数组,对应255个主设备号。通过主设备号索引到字符设备的结构cp。字符设备的结构cp通过单向链表链接。将创建的字符设备结构cd链接到单向链表,登记设备的区间。

 

=================================cdev_add==============================

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

}

 

================================kobj_map===============================

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)

{

    /*计算设备输入的range可能占用几个主设备号。对256这个range来说,只占用一个主设备号*/

       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;

              p->lock = lock;

              p->dev = dev;

              p->range = range;

              p->data = 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;

}

  kobj_map和前面的kobj_lookup是同一组函数,目的就是通过指针数组和链表管理字符设备。cdev_map是个kobj_map结构的指针,probes则是cdev_map包含的一个指针数组,数组元素255个。每个主设备号对应一个元素,而从设备号则形成一个链表。kobj_map则把字符设备加入这个链表中。这里和前文的chrdevs结构很相似。

  可以想到,实际上这里是个hash list。每个主设备号占用一个hash list链表头,则从设备号则根据范围链接到hash list的链表头。

 

四  input设备的打开

 

  经过前面的字符设备的注册和查找分析。我们发现又回到了原点,最终还是靠设备自身的open文件来打开设备。继续看input设备的open函数input_open_file:

 

============================input_open_file============================== 

static int input_open_file(struct inode *inode, struct file *file)

{

    /*根据次设备号获得input_handler*/

       struct input_handler *handler = input_table[iminor(inode) >> 5];

       const struct file_operations *old_fops, *new_fops = NULL;

       int err;

 

       /* No load-on-demand here? */

       if (!handler || !(new_fops = fops_get(handler->fops)))

              return -ENODEV;

 

       /*

        * That's _really_ odd. Usually NULL ->open means "nothing special",

        * not "no device". Oh, well...

        */

       if (!new_fops->open) {

              fops_put(new_fops);

              return -ENODEV;

       }

    /*函数指针再次被替换*/

       old_fops = file->f_op;

       file->f_op = new_fops;

     /*调用新的open函数*/

       err = new_fops->open(inode, file);

 

       if (err) {

              fops_put(file->f_op);

              file->f_op = fops_get(old_fops);

       }

       fops_put(old_fops);

       return err;

  这个函数比较简单。重要的概念是input_handler。input_table是个数组,包含8个input_handler指针。通过次设备号获得注册的input_handler,然后调用input_handler的open函数。

  这说明input设备可以封装了8种不同的handler,每种对应一个次设备号。

 

五  input框架的设备和驱动

  Input本身是个字符设备,但是经过层层分析,原来input设备里面就隐藏了众多的设备和驱动。这些设备和驱动是怎么注册进去的?内核为何专门做一个层次来汇聚这些设备?内核根目录的drivers/input/input.c本身并不复杂,我们重点看input_register_handler和input_register_device就可以了解这个问题。

 

===========================input_register_handler===========================

void input_register_handler(struct input_handler *handler)

{

       struct input_dev *dev;

       struct input_handle *handle;

       struct input_device_id *id;

 

       if (!handler)

              return;

 

       INIT_LIST_HEAD(&handler->h_list);

    

    /*这里注册到input_table,前面了解到,input_table是个8元素的input_handler指针数组*/

       if (handler->fops != NULL)

              input_table[handler->minor >> 5] = handler;

 

    /*handler挂入input_handler_list的链表*/

       list_add_tail(&handler->node, &input_handler_list);

 

       list_for_each_entry(dev, &input_dev_list, node)

              if (!handler->blacklist || !input_match_device(handler->blacklist, dev))

                     if ((id = input_match_device(handler->id_table, dev)))

                            if ((handle = handler->connect(handler, dev, id))) {

                                   input_link_handle(handle);

                                   if (handler->start)

                                          handler->start(handle);

                            }

 

       input_wakeup_procfs_readers();

}

  这段代码也很简单。注意最后一段,input_dev_list也是个链表头,通过它遍历所有注册的input设备,然后input_match_device看handler和设备是否适合,如果适合,又调用handler提供的connect函数。

  到这里终于明白了,input设备维护了两个链表,一个是设备链表,一个是驱动链表,注册一个驱动要和所有的设备一一匹配,看是否适合。这就是input框架的管理机制。

  分析一下input管理的设备和驱动是如何匹配的。

 

=========================input_match_device==============================

static struct input_device_id *input_match_device(struct input_device_id *id, struct input_dev *dev)

{

       int i;

 

       for (; id->flags || id->driver_info; id++) {

 

              if (id->flags & INPUT_DEVICE_ID_MATCH_BUS)

                     if (id->bustype != dev->id.bustype)

                            continue;

 

              if (id->flags & INPUT_DEVICE_ID_MATCH_VENDOR)

                     if (id->vendor != dev->id.vendor)

                            continue;

 

              if (id->flags & INPUT_DEVICE_ID_MATCH_PRODUCT)

                     if (id->product != dev->id.product)

                            continue;

 

              if (id->flags & INPUT_DEVICE_ID_MATCH_VERSION)

                     if (id->version != dev->id.version)

                            continue;

 

              MATCH_BIT(evbit,  EV_MAX);

              MATCH_BIT(keybit, KEY_MAX);

              MATCH_BIT(relbit, REL_MAX);

              MATCH_BIT(absbit, ABS_MAX);

              MATCH_BIT(mscbit, MSC_MAX);

              MATCH_BIT(ledbit, LED_MAX);

              MATCH_BIT(sndbit, SND_MAX);

              MATCH_BIT(ffbit,  FF_MAX);

              MATCH_BIT(swbit,  SW_MAX);

 

              return id;

       }

 

       return NULL;

}

  Input框架提供的匹配函数,就是根据设定,逐一比对驱动的总线类型,制造商,产品号和版本是否相等。可能有人问到,如果驱动里面设定都不比较,那岂不是可以给所有设备服务了?确实如此,这就是有些驱动号称万能驱动的原因。但是一般情况,驱动是需要提供必要的信息来匹配的。

 

int input_register_device(struct input_dev *dev)

{

       static atomic_t input_no = ATOMIC_INIT(0);

       struct input_handle *handle;

       struct input_handler *handler;

       struct input_device_id *id;

       const char *path;

       int error;

 

       if (!dev->dynalloc) {

              printk(KERN_WARNING "input: device %s is statically allocated, will not register\n"

                     "Please convert to input_allocate_device() or contact dtor_core@ameritech.net\n",

                     dev->name ? dev->name : "<Unknown>");

              return -EINVAL;

       }

 

       set_bit(EV_SYN, dev->evbit);

 

       /*

        * If delay and period are pre-set by the driver, then autorepeating

        * is handled by the driver itself and we don't do it in input.c.

        */

    /*初始化设备的timer*/

       init_timer(&dev->timer);

       if (!dev->rep[REP_DELAY] && !dev->rep[REP_PERIOD]) {

              dev->timer.data = (long) dev;

              dev->timer.function = input_repeat_key;

              dev->rep[REP_DELAY] = 250;

              dev->rep[REP_PERIOD] = 33;

       }

   

    /*设备加入input的设备列表*/

       INIT_LIST_HEAD(&dev->h_list);

       list_add_tail(&dev->node, &input_dev_list);

   

    /*指定设备属于input 类*/

       dev->cdev.class = &input_class;

       snprintf(dev->cdev.class_id, sizeof(dev->cdev.class_id),

               "input%ld", (unsigned long) atomic_inc_return(&input_no) - 1);

 

       error = class_device_add(&dev->cdev);

       if (error)

              return error;

    /*下面的这段代码是通过sysfs文件系统创建设备的属性文件。文件在系统根目录/sys/input/input*/..里面。 比较一下是不是这些属性?*/

       error = sysfs_create_group(&dev->cdev.kobj, &input_dev_attr_group);

       if (error)

              goto fail1;

 

       error = sysfs_create_group(&dev->cdev.kobj, &input_dev_id_attr_group);

       if (error)

              goto fail2;

 

       error = sysfs_create_group(&dev->cdev.kobj, &input_dev_caps_attr_group);

       if (error)

              goto fail3;

 

       __module_get(THIS_MODULE);

 

       path = kobject_get_path(&dev->cdev.kobj, GFP_KERNEL);

       printk(KERN_INFO "input: %s as %s\n",

              dev->name ? dev->name : "Unspecified device", path ? path : "N/A");

       kfree(path);

 

       list_for_each_entry(handler, &input_handler_list, node)

              if (!handler->blacklist || !input_match_device(handler->blacklist, dev))

                     if ((id = input_match_device(handler->id_table, dev)))

                            if ((handle = handler->connect(handler, dev, id))) {

                                   input_link_handle(handle);

                                   if (handler->start)

                                          handler->start(handle);

                            }

 

       input_wakeup_procfs_readers();

 

       return 0;

 

 fail3:     sysfs_remove_group(&dev->cdev.kobj, &input_dev_id_attr_group);

 fail2:     sysfs_remove_group(&dev->cdev.kobj, &input_dev_attr_group);

 fail1:     class_device_del(&dev->cdev);

       return error;

}

  最后的代码和input_register_handler那段很像,不过这次是搜索所有的驱动,看是否匹配。

Input框架分析到这里,引出了一个问题:内核为什么要加这么一个层次? 所有的层次设计主要就是为了复用,简化下面层次的工作量。而input汇聚的设备,不管键盘鼠标还是游戏杆触摸屏,它们的公共特征就是截获用户的输入,交给操作系统处理。所以input提供了重要的事件处理函数input_event,通过这个函数上报用户输入(实际上输入到了终端的输入buffer),可以想到,具体设备的驱动,比如键盘驱动,需要调用这个函数上报用户的按键输入。

这个到后面再分析。

 

 

六 总结

  内核驱动中,类似input这样的框架还有一些。比如根目录下面sound/sounc.core.c,它也定义了一个sound字符设备来统一管理。Drivers/video/fbmem.c,同样定义了一个字符设备来管理。这样的架构还有很多,这些相似的架构,分析掌握一种就可以触类旁通,大大减少学习内核的时间。读者可以试着分析这些驱动,从而加强对linux驱动架构的理解和掌握。

   到这里,有必要串联一下知识点,帮助我们更全面理解linux的设备和驱动架构。回顾前文,我们知道设备配置表,总线和驱动是整个内核设备架构的三大层次。设备配置表是设备本身物理特性的描述(以PCI为例子),包括了设备的控制器信息和内存信息,而总线,则是物理上存在的(也有特殊的并不真正存在),总线可以发现设备,管理设备,配置设备,插拔设备。而通过本节的分析,我们了解到设备驱动本身是分为多层次的。对一个键盘设备来说,它的驱动也分为input层,虚拟键盘层(input_handler层),真实键盘驱动层。

   以一个usb键盘为例子,它挂载在usb总线上,要被usb总线管理和配置,同时作为一个键盘设备,它也要被虚拟键盘层,input层所管理。如果说input层,字符设备层是从上往下的视图,那么总线层就是从下往上的视图。这两层结合(再加上设备的配置文件),就是设备驱动架构的整个视图。