Linux 设备驱动之字符设备(三)

转载原文:http://blog.chinaunix.net/uid-26833883-id-4371047.html

1. 前言

通过Linux 设备驱动之字符设备(二)我们对字符驱动的完善,已经可以完成基本的功能了,但是仍然有以下不足,这一节我们继续完善它。

  1. 没有实现xxx_ioctl接口,没办法通过命令来控制dev_fifo
    改善:增加xxx_ioctl函数接口,应用层可以通过ioctl系统调用,根据不同的命令来操作dev_fifo。

  2. 只能驱动一个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宏,探究内部的实现:

  1. typeof ( ( (struct test *)0 )->b ) : 获取成员变量b的类型 ,这里获取的就是short 类型。这是GNU_C的扩展语法。
  2. typeof ( ( (struct test *)0 )->b ) *__mptr = addr_b : 用获取的变量类型,定义了一个指针变量 __mptr ,并且将成员变量 b的首地址赋值给它。
  3. (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. 测试
  1. 加载模块,并指定访问权限;
    在这里插入图片描述
  2. 测试字符设备;
    在这里插入图片描述
  3. 使用dmesg命令检查log;
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值