Linux 设备驱动之字符设备(三)
2016-12-01 10:25:18
通过前面我们对字符驱动的完善,已经可以完成基本的功能了,但是仍然有以下不足,这一节我们继续完善它。
(1)没有实现xxx_ioctl接口,没办法通过命令来控制dev_fifo
改善:增加xxx_ioctl函数接口,应用层可以通过ioctl系统调用,根据不同的命令来操作dev_fifo。
(2)只能驱动一个dev_fifo设备,多个dev_fifo设备无法驱动
改善 :通过给模块传递参数,驱动多个dev_fifo设备
一 实现xxx_ioctl接口
(1)为什么要实现xxx_ioctl ?
前面我们在驱动中已经实现了读写接口,通过这些接口我们可以完成对设备的读写。但是很多时候我们的应用层工程师除了要对设备进行读写数据之外,还希望还可以对设备进行控制。例如:对于串口设备,驱动层除了需要提供对
串口的读写之外,还需提供对串口波特率的设置。
一句话总结一下了,实现xxx_ioctl函数接口,主要的目的是提供对设备的控制能力,增加驱动程序的灵活性。
(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文件,看哪些命令已经被使用过了。
好了,还是举个例子来看看,如何设计一个唯一的命令吧。
代码实例:
二、驱动多个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 传递下来的哦。
三、完善上一次的字符设备驱动
- #include <linux/init.h>
- #include <linux/module.h>
- #include <linux/cdev.h>
- #include <linux/fs.h>
- #include <linux/device.h>
- #include <linux/slab.h>
- #include <asm/uaccess.h>
- #include "dev_fifo_head.h"
-
- //指定的主设备号
- #define MAJOR_NUM 250
-
- //自己的字符设备
- struct mycdev
- {
- int len;
- unsigned char buffer[50];
- struct cdev cdev;
- };
-
- MODULE_LICENSE("GPL");
-
- //设备号
- static dev_t dev_num = {0};
-
- //全局gcd
- struct mycdev *gcd;
-
- //设备类
- struct class *cls;
-
- //获得用户传递的数据,根据它来决定注册的设备个数
- static int ndevices = 1;
- module_param(ndevices, int, 0644);
- MODULE_PARM_DESC(ndevices, "The number of devices for register.\n");
-
-
- //打开设备
- static int dev_fifo_open(struct inode *inode, struct file *file)
- {
- struct mycdev *cd;
-
- printk("dev_fifo_open success!\n");
-
- //用struct file的文件私有数据指针保存struct mycdev结构体指针
- cd = container_of(inode->i_cdev,struct mycdev,cdev);
- file->private_data = cd;
-
- return 0;
- }
-
- //读设备
- static ssize_t dev_fifo_read(struct file *file, char __user *ubuf, size_t size, loff_t *ppos)
- {
- int n;
- int ret;
- char *kbuf;
- struct mycdev *mycd = file->private_data;
-
- printk("read *ppos : %lld\n",*ppos);
-
- if(*ppos == mycd->len)
- return 0;
-
- //请求大大小 > buffer剩余的字节数 :读取实际记得字节数
- if(size > mycd->len - *ppos)
- n = mycd->len - *ppos;
- else
- n = size;
-
- printk("n = %d\n",n);
- //从上一次文件位置指针的位置开始读取数据
- kbuf = mycd->buffer + *ppos;
-
- //拷贝数据到用户空间
- ret = copy_to_user(ubuf,kbuf, n);
- if(ret != 0)
- return -EFAULT;
-
- //更新文件位置指针的值
- *ppos += n;
-
- printk("dev_fifo_read success!\n");
-
- return n;
- }
-
- //写设备
- static ssize_t dev_fifo_write(struct file *file, const char __user *ubuf, size_t size, loff_t *ppos)
- {
- int n;
- int ret;
- char *kbuf;
- struct mycdev *mycd = file->private_data;
-
- printk("write *ppos : %lld\n",*ppos);
-
- //已经到达buffer尾部了
- if(*ppos == sizeof(mycd->buffer))
- return -1;
-
- //请求大大小 > buffer剩余的字节数(有多少空间就写多少数据)
- if(size > sizeof(mycd->buffer) - *ppos)
- n = sizeof(mycd->buffer) - *ppos;
- else
- n = size;
-
- //从上一次文件位置指针的位置开始写入数据
- kbuf = mycd->buffer + *ppos;
-
- //拷贝数据到内核空间
- ret = copy_from_user(kbuf, ubuf, n);
- if(ret != 0)
- return -EFAULT;
-
- //更新文件位置指针的值
- *ppos += n;
-
- //更新dev_fifo.len
- mycd->len += n;
-
- printk("dev_fifo_write success!\n");
- return n;
- }
-
- //linux 内核在2.6以后,已经废弃了ioctl函数指针结构,取而代之的是unlocked_ioctl
- long dev_fifo_unlocked_ioctl(struct file *file, unsigned int cmd,
- unsigned long arg)
- {
- int ret = 0;
- struct mycdev *mycd = file->private_data;
-
- switch(cmd)
- {
- case DEV_FIFO_CLEAN:
- printk("CMD:CLEAN\n");
- memset(mycd->buffer, 0, sizeof(mycd->buffer));
- break;
-
- case DEV_FIFO_SETVALUE:
- printk("CMD:SETVALUE\n");
- mycd->len = arg;
- break;
-
- case DEV_FIFO_GETVALUE:
- printk("CMD:GETVALUE\n");
- ret = put_user(mycd->len, (int *)arg);
- break;
-
- default:
- return -EFAULT;
- }
-
- return ret;
- }
-
-
- //设备操作函数接口
- static const struct file_operations fifo_operations = {
- .owner = THIS_MODULE,
- .open = dev_fifo_open,
- .read = dev_fifo_read,
- .write = dev_fifo_write,
- .unlocked_ioctl = dev_fifo_unlocked_ioctl,
- };
-
-
- //模块入口
- int __init dev_fifo_init(void)
- {
- int i = 0;
- int n = 0;
- int ret;
- struct device *device;
-
- gcd = kzalloc(ndevices * sizeof(struct mycdev), GFP_KERNEL);
- if(!gcd){
- return -ENOMEM;
- }
-
- //设备号 : 主设备号(12bit) | 次设备号(20bit)
- dev_num = MKDEV(MAJOR_NUM, 0);
-
- //静态注册设备号
- ret = register_chrdev_region(dev_num,ndevices,"dev_fifo");
- if(ret < 0){
-
- //静态注册失败,进行动态注册设备号
- ret = alloc_chrdev_region(&dev_num,0,ndevices,"dev_fifo");
- if(ret < 0){
- printk("Fail to register_chrdev_region\n");
- goto err_register_chrdev_region;
- }
- }
-
- //创建设备类
- cls = class_create(THIS_MODULE, "dev_fifo");
- if(IS_ERR(cls)){
- ret = PTR_ERR(cls);
- goto err_class_create;
- }
-
- printk("ndevices : %d\n",ndevices);
-
- for(n = 0;n < ndevices;n ++)
- {
- //初始化字符设备
- cdev_init(&gcd[n].cdev,&fifo_operations);
-
- //添加设备到操作系统
- ret = cdev_add(&gcd[n].cdev,dev_num + n,1);
- if (ret < 0)
- {
- goto err_cdev_add;
- }
- //导出设备信息到用户空间(/sys/class/类名/设备名)
- device = device_create(cls,NULL,dev_num + n,NULL,"dev_fifo%d",n);
- if(IS_ERR(device)){
- ret = PTR_ERR(device);
- printk("Fail to device_create\n");
- goto err_device_create;
- }
- }
- printk("Register dev_fito to system,ok!\n");
-
-
- return 0;
-
- err_device_create:
- //将已经导出的设备信息除去
- for(i = 0;i < n;i ++)
- {
- device_destroy(cls,dev_num + i);
- }
-
- err_cdev_add:
- //将已经添加的全部除去
- for(i = 0;i < n;i ++)
- {
- cdev_del(&gcd[i].cdev);
- }
-
- err_class_create:
- unregister_chrdev_region(dev_num, ndevices);
-
- err_register_chrdev_region:
- return ret;
-
- }
-
- void __exit dev_fifo_exit(void)
- {
- int i;
-
- //删除sysfs文件系统中的设备
- for(i = 0;i < ndevices;i ++)
- {
- device_destroy(cls,dev_num + i);
- }
-
- //删除系统中的设备类
- class_destroy(cls);
-
- //从系统中删除添加的字符设备
- for(i = 0;i < ndevices;i ++)
- {
- cdev_del(&gcd[i].cdev);
- }
-
- //释放申请的设备号
- unregister_chrdev_region(dev_num, ndevices);
-
- return;
- }
-
-
- module_init(dev_fifo_init);
- module_exit(dev_fifo_exit);
- #ifndef _DEV_FIFO_HEAD_H
- #define _DEV_FIFO_HEAD_H
-
- #define DEV_FIFO_TYPE 'k'
- #define DEV_FIFO_CLEAN _IO(DEV_FIFO_TYPE,0x10)
- #define DEV_FIFO_GETVALUE _IOR(DEV_FIFO_TYPE,0x11,int)
- #define DEV_FIFO_SETVALUE _IOW(DEV_FIFO_TYPE,0x12,int)
-
- #endif
- ifeq ($(KERNELRELEASE),)
-
- KERNEL_DIR ?=/lib/modules/$(shell uname -r)/build
- PWD :=$(shell pwd)
-
- modules:
- $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
-
- .PHONY:modules clean
- clean:
- $(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
- else
-
- obj-m := dev_fifo.o
- endif
应用程序:
- #include <stdio.h>
- #include <stdlib.h>
- #include <sys/types.h>
- #include <string.h>
- #include <sys/stat.h>
- #include <fcntl.h>
-
- int main(int argc, const char *argv[])
- {
- int fd ;
- int n;
- char buf[1024] = "hello word";
-
- fd = open("/dev/dev_fifo0",O_RDWR);
- if(fd < 0){
- perror("Fail ot open");
- return -1;
- }
-
- printf("open successful ,fd = %d\n",fd);
-
- n = write(fd,buf,strlen(buf));
- if(n < 0){
- perror("Fail to write");
- return -1;
- }
-
- printf("write %d bytes!\n",n);
-
- n = write(fd,buf,strlen(buf));
- if(n < 0){
- perror("Fail to write");
- return -1;
- }
-
- printf("write %d bytes!\n",n);
- return 0;
- }