转载原文:http://blog.chinaunix.net/uid-26833883-id-4371047.html
目录
1. 前言
通过Linux 设备驱动之字符设备(二)我们对字符驱动的完善,已经可以完成基本的功能了,但是仍然有以下不足,这一节我们继续完善它。
-
没有实现xxx_ioctl接口,没办法通过命令来控制dev_fifo
改善:增加xxx_ioctl函数接口,应用层可以通过ioctl系统调用,根据不同的命令来操作dev_fifo。 -
只能驱动一个dev_fifo设备,多个dev_fifo设备无法驱动
改善 :通过给模块传递参数,驱动多个dev_fifo设备。
2. 实现xxx_ioctl接口
2.1. 为什么要实现xxx_ioctl ?
前面我们在驱动中已经实现了读写接口,通过这些接口我们可以完成对设备的读写。但是很多时候我们的应用层工程师除了要对设备进行读写数据之外,还希望还可以对设备进行控制。例如:对于串口设备,驱动层除了需要提供对串口的读写之外,还需提供对串口波特率的设置。
一句话总结一下了,实现xxx_ioctl函数接口,主要的目的是提供对设备的控制能力,增加驱动程序的灵活性。
2.2. 如何实现xxx_ioctl函数接口?
先来看看应用层的ioctl和驱动层的xxx_ioctl对应关系:
<1> 应用层ioctl参数分析:
第一个参数:打开设备文件的时候获得文件描述符
第二个参数:给驱动层传递的命令,需要注意的时候,驱动层的命令和应用层的命令一定要统一
第三个参数: "…"在C语言中,很多时候都被理解成可变参数。这里不是这个意思。
当我们通过ioctl调用驱动层xxx_ioctl的时候,有三种情况可供选择:
第一种情况:不传递数据给xxx_ioctl
第二种情况:传递数据给xxx_ioctl,希望它最终能把数据写入设备(例如:设置串口的波特率)
第三种情况:调用xxxx_ioctl希望获取设备的硬件参数(例如:获取当前串口设备的波特率)
好了,这三种情况中,有些时候需要传递数据,有些时候不需要传递数据。在C语言中,是无法实现函数重载的 。
那怎么办?用"…“来欺骗编译器了,”…"本来的意思是传递多参数。在这里的意思是带一个参数还是不带参数。
哎!说起来真费劲,重在意会,希望你能理解。
<2> 驱动层xxx_ioctl参数分析:
第一个参数和第二个参数就不说了,你懂得。
第三个参数 :用户空间传递的命令,可以根据不同的命令做不同的事情
第四个参数: 用户空间的数据,主要这个数据可能是一个地址值(用户空间传递的是一个地址),也可能是一个数值,也可能没值
<3> 如何确定cmd 的值 ?
是不是都认为随便给cmd一个值就可以了,只要这个值驱动层能认识就可以。
这是不负责人的干的事情,最后只能引起一场悲剧。这种事情生活中太多了…
想一想这种情况:
有两个个设备,你本来想操作第一个设备,但是你就是一个和"2"结了不解之缘,所以你打开了第二个设备。正好你传递的命令也是第二个设备驱动能识的,你很开心的操作它。本来你想通过这命令来打开磁盘驱动器(第一个设备)来看片的,由于"2",这个命令被电源设备(第二个设备)接收到,直接关机。疯了,熬了一晚上的代码没保存,本来想通过看片振奋一下精神的,现在全玩完…你是不是想,就算我"2"打开了错误的设备,你也不应该识别我这个命令呀,返回一个错误给我,告诉我这是无效的命令(这个命令不能再这个设备上使用),为什么不让系统中每个驱动程序识别的命令都不一样呢?呜呜。。。
哎,说多了都是泪!所以我们在设计命令的时候,一定要做到唯一性哦,这样就算很"2"的人,也可以过的很幸福。。。
好了,扯了这么多,我想你应该明白了,命令不能随便指定,要做到规范。现在我就来看看,在Linux 内核中这个cmd是如何设计的吧!
貌似很麻烦,难道我设计一个命令的时候,还需要在不同的位域写不同的数字?
不需要的,Linux 系统已经给我们封装好了宏,我们只需要直接调用宏来设计命令即可。
通过Linux 系统给我们提供的宏,我们在设计命令的时候,只需要指定设备类型、命令序号,数据类型三个字段就可以了。
在这里需要注意的时候,我们不能随便指定的哦,我们最终设计的命令应该是Linux系统中没有的命令才符号规范。那怎么知道Linux 系统中已经设计了哪些命令呢?可以通过查阅Linux 源码中的Documentation/ioctl/ioctl-number.txt文件,看哪些命令已经被使用过了。
好了,还是举个例子来看看,如何设计一个唯一的命令吧。
代码实例:
3. 驱动多个dev_fifo 设备
上附图,是最后的效果。我们在内核空间分配多个struct mycdev结构体,每个struct mycdev代表一个dev_fifo设备。由于这些设备都是属于dev_fifo类,所以我们在处理的时候,让这些设备的主设备号一样,次设备号不一样。
想一个问题:如果我打开/dev/dev_fifo0 进行操作的时候,我怎么知道dev_fifo0对应内存空间的地址呢?
以前我们只有一个dev_fifo设备好办,我们在分配好内存后用一个全局的指针变量保存它的地址。现在有多个dev_fifo设备难道是定义多个全局变量分别保存,就算这样干了,当我调到驱动层的 xxx_read和xxx_write函数,我怎么知道是对哪一个设备进行读写操作呢?
有问题是好的,我们带着问题出发,看看大牛们是怎么做的。
先分析contianer_of 接口,这简直就是一个神奇的实现呀!
举个例子,来简单分析一下container_of 内部实现机制。
例如:
struct test
{
int a;
short b;
char c;
};
struct test *p = (struct test *)malloc(sizeof(struct test));
test_function(&(p->b));
int test_function(short *addr_b)
{
//获取struct test结构体空间的首地址
struct test *addr;
addr = container_of(addr_b,struct test,b);
}
展开container_of宏,探究内部的实现:
- typeof ( ( (struct test *)0 )->b ) : 获取成员变量b的类型 ,这里获取的就是short 类型。这是GNU_C的扩展语法。
- typeof ( ( (struct test *)0 )->b ) *__mptr = addr_b : 用获取的变量类型,定义了一个指针变量 __mptr ,并且将成员变量 b的首地址赋值给它。
- (struct test *)( (char *)__mptr - offsetof(struct test,b)) : 这里的offsetof(struct test,b)是用来计算成员b在这个struct test 结构体的偏移。__mptr 是成员b的首地址, 现在减去成员b在结构体里面的偏移值,算出来的是不是这个结构体的首地址呀。
现在能看懂上面xxx_open那段代码了吗?
每次我们从应用层调用到驱动层的函数接口时,VFS层都会把struct inode 和 struct file 结构体分配的内存空间首地址传递给对应的驱动层接口。
还记得,前面我分析应用层和驱动层是如何关联的吗?最终找到驱动层的字符设备,并且把内核为字符设备分配的内存首地址保存在了struct inode 的i_cdev指针变量中。
我们通过container_of获取了内核空间为dev_fifo设备分配的内存首地址,然后把它保存在了file->private_data成员变量中。
现在能回答"如果我打开/dev/dev_fifo0 进行操作的时候,我怎么知道dev_fifo0对应的dev_ffifo0所在的内存空间的首地址呢?" 这个问题了吗?
是的,我们通过open打开/dev/dev_fifo0这个设备文件时,Linux 内核最终找到了/dev/dev_fifo0对应在内核空间的dev_fifo0这个设备。在xxxx_open函数中我们把内核为这个设备分配的内存空间首地址保存在了file->private_data这成员变量中。下一次当我们通过read 或 write调用到驱动层的xxx_read或xxx_write时,VFS层都会把struct file *file 传递下来的哦。
4. 实例
4.1. 实例源码
实例源码请自行下载:dev_fifo_v3.zip
5.3. 测试
- 加载模块,并指定访问权限;
- 测试字符设备;
- 使用
dmesg
命令检查log;