深入浅出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层,字符设备层是从上往下的视图,那么总线层就是从下往上的视图。这两层结合(再加上设备的配置文件),就是设备驱动架构的整个视图。
- 深入浅出linux之字符设备和input设备
- 深入浅出Linux设备驱动之字符设备驱动程序
- 深入浅出Linux设备驱动之字符设备驱动程序
- 深入浅出Linux设备驱动之字符设备驱动程序
- 深入浅出Linux设备驱动之字符设备驱动程序
- 深入浅出Linux设备驱动之字符设备驱动程序
- 深入浅出Linux设备驱动之字符设备驱动程序
- 深入浅出:Linux设备驱动之字符设备驱动
- 深入浅出:Linux设备驱动之字符设备驱动
- 深入浅出:Linux设备驱动之字符设备驱动
- 深入浅出:Linux设备驱动之字符设备驱动
- 深入浅出:Linux设备驱动之字符设备驱动
- 深入浅出:Linux设备驱动之字符设备驱动
- 深入浅出:Linux设备驱动之字符设备驱动
- 深入浅出:Linux设备驱动之字符设备驱动
- 深入浅出:Linux设备驱动之字符设备驱动
- 深入浅出:Linux设备驱动之字符设备驱动
- 字符设备 和 input 设备--input设备的注册
- 数据结构基本概念
- 委托理解
- 找不到atlapp.h的解決方法
- JS防止重复提交
- vs2005编译opencv 2.1[CMake 2.8.4 Python 2.6.5]
- 深入浅出linux之字符设备和input设备
- 家有千金之迎春杯少儿段级位赛
- 学习原则
- vc++ 如何得到程序运行时间
- 函数的副作用
- 在VS2010 里面包含进全局的include目录和lib目录
- 二分查找算法
- Visual Studio 2010下编译调试MongoDB源码
- C++标准库auto_ptr指针的应用