本学期一直在学习linux下到设备驱动开发,字符设备驱动是设备驱动开发中最基本和重要的一部分。前几天的考试让我意识到对这部分的内容理解的还不是很清楚,因此,很有必要再次理解学习字符设备驱动。
本文以全局内存字符设备globalmem为例,说明字符设备驱动的结构以及编写方法。
1.字符设备的数据结构
在linux内核中使用struct cdev来表示一个字符设备,如下:
//在linux/include/linux/cdev.h中 12struct cdev { 13 struct kobject kobj; 14 struct module *owner; 15 const struct file_operations *ops; 16 struct list_head list; 17 dev_t dev; 18 unsigned int count; 19};
下面对该数据结构的字段作简单解释:
owner:该设备的驱动程序所属的内核模块,一般设置为THIS_MODULE;
ops:文件操作结构体指针,file_operations结构体中包含一系列对设备进行操作的函数接口;
dev:设备号。dev_t封装了unsigned int,该类型前12位为主设备号,后20位为次设备号;
cdev结构是内核对字符设备驱动的标准描述。在实际的设备驱动开发中,通常使用自定义的结构体来描述一个特定的字符设备。这个自定义的结构体中必然会包含cdev结构,另外还要包含一些描述这个具体设备某些特性到字段。比如:
struct globalmem_dev { struct cdev cdev; /*cdev struct which the kernel has defined*/ unsigned char mem[GLOBALMEM_SIZE]; /*globalmem memory*/ };
该结构体用来描述一个具有全局内存的字符设备。
2.分配和释放设备号
在linux中,对于每一个设备,必须有一个惟一的设备号与之相对应。通常会有多个设备共用一个主设备号,而具体每个设备都唯一拥有一个次设备号。总的来看,每个设备都唯一的拥有一个设备号。前面已经提到,内核使用dev_t类型来表示一个设备号,对于设备号有以下几个常用的宏:
//在linux/include/linux/kdev_t.h中 7#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) 8#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) 9#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
上述三个宏的功能分别为:通过设备号获取主设备号,通过设备号获取次设备号,通过主次设备好获取设备号。
在设备驱动程序中,一般会首先向系统申请设备号。linux中设备号的申请都是一段连续的设备号,这些连续的设备号都有共同的主设备号。设备号的申请有两种方法,若提前设定了主设备号则再接着申请若干个连续的次设备即可;若未指定主设备号则直接向系统动态申请未被占用到设备号。由此可以看出,如果使用第一种方法,则可能会出现设备号已被系统中的其他设备占用的情况。
上出两种申请设备号的方法分别对应以下两个申请函数:
//在linux/fs/char_dev.c中 196int register_chrdev_region(dev_t from, unsigned count, const char *name) 232int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, 233 const char *name)
上述两个函数都可以申请一段连续的设备号。前者适用已知起始设备号的情况(通过MADEV(major,0)可以获得主设备号为major的起始设备号);后者使用于动态申请设备号的情况。如果想申请一个设备号,则将函数中的参数count设为1即可。关于这两个函数的详细源码分析,可参考这里。
3.Linux字符设备驱动的组成
实现一个基本的字符设备驱动需要完成以下几部分:字符设备驱动模块的加载卸载函数和实现file_operations结构中的成员函数。
3.1.file_operations结构体
file_operations结构体中包含许多函数指针,这些函数指针是字符设备驱动和内核的接口。,实现该结构中的这些函数也是整个字符设备驱动程序的核心工作。file_operations结构中的每个函数都对应一个具体的功能,也就是对设备的不同操作。不过,这些函数是在内核模块中实现的,最终会被加载到内核中和内核一起运行。因此,用户态下的程序是不能直接使用这些函数对相应设备进行操作的。
学过系统调用后,你就会知道,比如当应用程序通过系统调用read对设备文件进行读操作时,最终的功能落实者还是设备驱动中实现的globalmem_read函数。而将系统调用read和globalmem_read函数扯上关系的则是struct file_operations。具体的操作是:
static const struct file_operations globalmem_fops = { .owner = THIS_MODULE, .read = globalmem_read, .write = globalmem_write, .open = globalmem_open, .release = globalmem_release, };
3.2.实现加载和卸载函数
由于字符设备驱动程序是以内核模块的形式加载到内核的,因此该程序中必须有内核模块的加载和卸载函数。通常,字符设备驱动程序的加载函数完成的工作有设备号的申请、cdev的注册。具体的过程可参考下图:
globalmem_init流程图(点击看大图)
从上述的图中可以看到,在内核模块加载函数中主要完成了字符设备号的申请。将字符设备注册到系统中是通过加载函数中的globalmem_setup_cdev函数来完成的。该函数具体完成的工作可以参考下图:
globalmem_setup_cdev流程图
结合上图,接下来参看globalmem_setup_cdev函数的具体代码。由cdev_init中,除了初始化cdev结构中的字段,最重要的是将globalmem_fops传递给cdev中的ops。
static void globalmem_setup_cdev(struct globalmem_dev *dev, int index) { int ret; int devno = MKDEV(globalmem_major, index); cdev_init(&dev->cdev, &globalmem_fops); dev->cdev.owner = THIS_MODULE; dev->cdev.ops = &globalmem_fops; ret = cdev_add(&dev->cdev, devno, 1); if(ret){ printk("adding globalmem error"); } }
通过上述的几步,就可以完成字符设备驱动加载函数。对于字符设备卸载函数而言,所作的工作就是加载函数功能的逆向:将cdev从系统中注销;释放设备结构体所占用的内存空间;释放设备号。具体可参看代码:
static void __exit globalmem_exit(void) { /*free struct cdev*/ cdev_del(&dev->cdev); /*free the memory of struct globalmem_dev*/ kfree(dev); /*free the devno*/ unregister_chrdev_region(MKDEV(globalmem_major,0), 1); }
3.3.对file_operaions成员函数的实现
最基本的成员函数包括open、release、read和write等函数。对这些函数的具体实现还要根据具体的设备要求来完成。在本文所述的全局内存字符设备驱动中,我们要实现的是功能是在用户程序中对这字符设备中的这块全局内存进行读写操作。读写函数的具体功能可参考下图:
对于open和release可以不做具体实现,当用户态程序打开或释放设备文件时,会自动调用内核中通用的打开和释放函数。
这样,一个基本的字符设备驱动程序就完成了。本文所述实例是一个有代表性的通用模型,可以在理解本程序的基础上继续增加其他功能。