卡尔说过:“在科学上没有平坦的大道,只有那些不畏劳苦而沿着陡峭的山路攀登的人,才有可能达到辉煌的顶点!”
在深刻了解“Hello World”之后,我们再向前迈进一小步,真正在linux下进行设备驱动程序开发。Linux 系统的设备分为字符设备(char device),块设备(block device)和网络设备(network device)三种。字符设备是指存取时没有缓存的设备。块设备的读写都有缓存来支持,并且块设备必须能够随机存取(random access),字符设备则没有这个要求。典型的字符设备包括鼠标,键盘,串行口等。块设备主要包括硬盘软盘设备,CD-ROM等。一个文件系统要安装进入操作系统必须在块设备上。网络设备在Linux里做专门的处理。Linux的网络系统主要是基于BSD unix的socket机制。在系统和驱动程序之间定义有专门的数据结构(sk_buff)进行数据的传递。系统里支持对发送数据和接收数据的缓存,提供流量控制机制,提供对多协议的支持。
首先我们要接触是字符设备驱动程序,此类驱动程序适合于大多数简单的硬件设备,比起块设备或网络设备驱动程序更加容易理解。那么要学习掌握它,要有以下几方面的预备知识。
一、设备编号在内核中的表达
设备编号即主设备号和次设备号,在内核中用dev_t类型(在中定义)来表达。在内核2.6版本中,dev_t是一个32位的数,其中的12位有来表示主设备号,而其余20位用来表示次设备号。
掌握以下三个宏(见),它们用来处理dev_t和设备编号之间的转换:
dev_t -> 设备编号
|
主设备号: MAJOR(dev_t dev) 次设备号: MINOR(dev_t dev)
|
设备编号-> dev_t
|
MKDEV(int major, int minor)
|
二、分配和释放设备编号
在建立一个字符设备之前,驱动程序首先要获得一个或多个设备编号,可以通过两种途径完成:
指定设备编号 |
int register_chrdev_region(dev_t frist, unsigned int count, char *name);
|
动态分配 |
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name); |
这些分配的设备编号不再使用时,应该释放掉,使用的函数为:
释放编号 |
void unregister_chrdev_region(dev_t first,unsigned int count);
|
以上三个函数中声明。
温馨提示:分配主设备号的最佳方式:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。
三、字符设备注册
注册字符设备有两种方式,一种是2.6内核以前的早期方式及2.6内核开始的新技术,当然2.6内核兼容前者。
早期方式:
它是注册一个字符设备驱动程序的经典方式。int register_chrdev(unsigned int major,const char *name, struct file_operations *fops);
|
其中,major是设备的主设备号,name是驱动程序的名称,fops为指向操作这个设备方式的file_operations结构指针。
与注册相对的操作就是将设备从系统中移除,早期调用下面函数:
int unregister_chrdev(unsigned int major,const char *name);
|
新方法: 在这里就必须引进一个新的结构体——struct cdev,它就用来表示字符设备(见)。在内核调用设备的操作之前,必须分配并注册一个或多个struct cdev。
struct cdev{ struct kobject kobj;
struct module *owner; // 所属模块 struct file_operations *ops; // 操作字符设备的方式 struct list_head list; dev_t dev; // 设备编号 unsigned int count; };
|
新技术下,分配和初始化字符设备有
两种方式:静态方式和动态方式。
静态内存定义初始化:struct
cdev my_cdev;
cdev_init(&my_cdev, &fops);
my_cdev.owner
= THIS_MODULE; |
动态内存定义初始化:struct
cdev *my_cdev = cdev_alloc();
my_cdev->ops =
&fops;
my_cdev->owner =
THIS_MODULE; |
两种使用方式的功能是一样的,只是使用的内存区不一样,一般视实际的数据结构需求而定。
提示:cdev_init和cdev_alloc两个函数完成的功能基本一致(实现过程见内核fs目录下char_dev.c文件),只是cdev_alloc没有对cdev结构中ops字段进行初始化,需要其函数体外进行。另外两个方式都需要对cdev结构中的owner字段进行单独初始化。
注意:尽信书不如无书。LDD中文版60页很容易让人产生先调用cdev_alloc再调用cdev_init的误解。
到此为止,我们的事还没完。接下来应该告诉内核该结构信息:
int cdev_add(struct cdev *dev,dev_t num,unsigned int count);
|
牢记一点:调用这个函数可能会失败。如果它返回一个负的错误码,则设备不会被添加到系统中。 从系统中移除一个字符设备:
void cdev_del(struct cdev *dev);
|
四、一些重要的数据结构
大部分基本的驱动程序操作涉及及到三个重要的内核数据结构,分别是file_operations、file和inode,它们的定义都在。
struct file_operations是一个字符设备把驱动的操作和设备号联系在一起的纽带,
是一系列指针的集合,每个被打开的文件都对应于一系列的操作,这就是file_operations,用
来执行一系列的系统调用。
struct file代表一个打开的文件,在执行file_operation中
的open操作时被创建,这里需要注意的是与用户空间inode指
针的区别,一个在内核,而file指针在用户空间,由c库
来定义。
struct inodeITPUB个
人空间MPd+L:R6[T#ZK: 被内核用来代表一个文件,注意和struct file的
区别,struct inode一个是代表文件,struct
file一个是代表打开的文件。
struct
inode包括很重要的二个成员:
dev_t i_rdev; 设备文件的设备号
struct cdev *i_cdev; 代表字符设备的数据结构 struct inode结构是用来在内核内部表示文件的。同
一个文件可以被打开好多次,所以可以对应很多struct
file,但是只对应一个struct inode。五、具体应用 对于驱动一个字符设备,掌握上面的知识点是必须的,不难。但难的是如何灵活有效地去使用它们,这才是我们所追求的。
贯穿整个LDD第三章,作者在给我们介绍一个真正的设备驱动程序:scull。现在我把它抽出来进行集中的讲解。scull是一个操作内存区域的字符设备驱动程序,这片内存区域就相当于一个设备。因此它与硬件无关,只是对内核分配的内存进行操作。
1) 首先我们有必要掌握两个数据结构(scull_dev和scull_qset)及由它们构成scull设备的结构关系,这有助于我们理解scull设备驱动程序。
scull_dev结构用来表示一个scull设备,该结构如下:
struct scull_dev{ struct scull_qset *data; /* 指向第一个量子集的指针 */ int quantum; /* 当前量子的大小 */ int qset; /* 当前数组的大小 */ unsigned long size; /* 保存在其中的数据总量 */ unsigned int access_key; /* 由sculluid和scullpriv使用 */ struct semaphore sem; /* 互斥型信号量 */ struct cdev cdev; /* 字符设备结构 */ };
|
可以看出,scull设备把cdev结构嵌入其中,我们的注意力要集中在cdev上,cdev才是内核与设备间的接口。
scull_qset结构用来对scull设备的数据进行处理:
struct scull_qset { void **data; struct scull_qset *next; }; |
两个数据结构即构成scull设备的模型,它们之间的关系如下图:
如上图,在scull中,每个设备都是一个指针链表,其中每个指针指向一个scull_qset结构。
scull_qset结构指向一个中间指针数组,数据大小由scull_dev中的qset字段表示,我们称这样一个数组为量子集。指针数组中的每一个元素指向一个内存区(称之为量子),量子的大小由scull_dev中的quantum字段表示。 2) 获取scull设备编号
scull驱动程序获取设备编号采用就是动态分配和指定设备编号相结方式,具体代码如下:
if (scull_major) { dev = MKDEV(scull_major, scull_minor); result = register_chrdev_region(dev, scull_nr_devs, "scull"); } else { result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,"scull"); scull_major = MAJOR(dev); } if (result < 0) { printk(KERN_WARNING "scull: can't get major %d\n", scull_major); return result; } |
3) 注册scull设备
static void scull_setup_cdev(struct scull_dev *dev, int index) { int err, devno = MKDEV(scull_major, scull_minor + index); cdev_init(&dev->cdev, &scull_fops); dev->cdev.owner = THIS_MODULE; // dev->cdev.ops = &scull_fops; //这句可以省略,在cdev_init中已经做过 err = cdev_add (&dev->cdev, devno, 1); /* Fail gracefully if need be 这步值得注意*/ if (err) printk(KERN_NOTICE "Error %d adding scull%d", err, index); } |
4) 实现对scull设备的操作
open open提供给驱动程序以初始化的能力,为以后的操作作准备。应完成的工作如下: (1) 检查设备特定的错误(如设备未就绪或硬件问题);
(2) 如果设备是首次打开,则对其进行初始化;
(3) 如有必要,更新f_op指针;
(4) 分配并填写置于filp->private_data里的数据结构。
而根据scull的实际情况,open函数只要完成第四步,即将初始化过的
struct scull_dev dev的指针传递到filp->private_data里,以备后用。从传入给open函数的实参inode类型数据,我们只能得到cdev结构,但我们希望得到包含cdev结构的scull_dev结构。幸好linux内核可以帮我们实现,可通过
定义在中的container_of宏得以实现
container_of(pointer, container_type, container_field )
|
其作用就是:通过指向container_field字段的pointer指针,获得container_type结构指针,container_type结构包含container_field字段。
release
release方法提供释放内存,关闭设备的功能。应完成的工作如下:
(1) 释放由open分配的、保存在file->private_data中的所有内容;
(2) 在最后一次关闭操作时关闭设备。
由于前面定义了scull是一个全局且持久的内存区,所以他的release什么都不做。
read和write
read和write方法的主要作用就是实现内核与用户空间之间的数据拷贝。因为Linux的内核空间和用户空间隔离的,所以要实现数据拷贝就必须使用在中定义的:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count); unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
|
而值得一提的是以上两个函数和
#define __copy_from_user(to,from,n) (memcpy(to, (void __force *)from, n), 0) #define __copy_to_user(to,from,n) (memcpy((void __force *)to, from, n), 0)
|
之间的关系:通过源码可知,前者调用后者,但前者在调用前对用户空间指针进行了检查。
5)模块测试 模块测试使用平台:Linux2.6.16内核、友善之臂QQ2440开发板
a、编译scull设备驱动程序:make modules。将生成的模块scull.ko通过nfs放到开发板中的/lib目录下。
b、在/lib目录下加载模块:insmod scull.ko。
c、查看动态分配得到的主设备号:cat /proc/devices
[root@FriendlyARM /lib]# insmod scull.ko [root@FriendlyARM /lib]# cat /proc/devices Character devices: 1 mem 4 /dev/vc/0 ... 232 buttons 253 scull 254 devfs Block devices: 1 ramdisk 7 loop ...
|
d、将分配到的主设备号建立设备文件:
mknod /dev/scull0 c 253 0 [root@FriendlyARM /lib]# mknod /dev/scull0 c 253 0
|
此时在/dev目录下可以看到多一个scull0文件
e、编译用户测试程序:
make。并将得到可执行文件scull_test同样通过nfs放到开发板的某个目录下。
f、可执行文件:
./scull_test。结果显示如下:
[root@FriendlyARM /mnt]# ./scull_test write ok! code=20 read ok! code=20 [0]=0 [1]=1 [2]=2 [3]=3 [4]=4 [5]=5 [6]=6 [7]=7 [8]=8 [9]=9 [10]=10 [11]=11 [12]=12 [13]=13 [14]=14 [15]=15 [16]=16 [17]=17 [18]=18 [19]=19
|
g、进入/lib目录卸载模块:rmmod scull
[root@FriendlyARM /lib]# rmmod scull |
这时可通过cat /proc/devices命令查看到,主设备号253没有了,卸载成功。
h、重新加载模块,同时修改模块中的某些参数,看一下实验结果:
[root@FriendlyARM /lib]# insmod scull.ko scull_quantum=6 scull_qset=2 [root@FriendlyARM /lib]# cd /mnt [root@FriendlyARM /mnt]# ls hello scull scull_test [root@FriendlyARM /mnt]# ./scull_test write error! code=6 write error! code=6 write error! code=6 write ok! code=2 read error! code=6 read error! code=6 read error! code=6 read ok! code=2 [0]=0 [1]=1 [2]=2 [3]=3 [4]=4 [5]=5 [6]=6 [7]=7 [8]=8 [9]=9 [10]=10 [11]=11 [12]=12 [13]=13 [14]=14 [15]=15 [16]=16 [17]=17 [18]=18 [19]=19
|
实验结束。前后两次实验验证了模块的读写能力,同时验证了对量子的有效读写。每一次读写最多只处理一个量子。
附件:scull设备驱动程序及用户测试程序:
|
文件: | 3_scull.tar.gz |
大小: | 5KB |
下载: | 下载 |
|
感谢:本文要特别素未谋面及交流的网友Tekkaman Ninja,他的博客使我受益匪浅。
阅读(1744) | 评论(0) | 转发(0) |