一、Ioctl的作用:大部分驱动除了需要具备读写设备的能力外,还需要具备对硬件控制的
能力。例如,要求设备报告错误信息,改变波特率,这些操作常常通过 ioctl 方法来实现。
二、Ioctl有用户空间的Ioctl和驱动空间的Ioctl
1.用户空间的Ioctl:使用ioctl 系统调用来控制设备,原型如下:
int ioctl(int fd,unsignedlong cmd,...)
原型中的点表示这是一个可选的参数,存在与否依赖于控制命令(第 2 个参数 )是否涉及
到与设备的数据交互。
2.驱动的Ioctl:函数原型如下:
int (*ioctl)(structinode*inode,struct file *filp,unsigned int cmd,unsignedlong arg)
cmd参数从用户空间传下来,可选的参数 arg 以一个unsignedlong 的形式传递,不管它是一个整数或一个指针。如果cmd命令不涉及数据传输,则第 3 个参数arg的值无任何意义。
三、Ioctl的实现
1、定义命令:
在编写ioctl代码之前,首先需要定义命令。为了防止对错误的设备使用正确的命令,命令号应该在系统范围内是唯一的。ioctl 命令编码被划分为几个位段,include/asm/ioctl.h中定义了这些位字段:类型(幻数),序号,传送方向,参数的大小。Documentation/ioctl-number.txt文件中罗列了在内核中已经使用了的幻数。
定义 ioctl 命令的正确方法是使用 4 个位段, 这个列表中介绍的符号定义在<linux/ioctl.h>中:
①Type 幻数(类型): 表明哪个设备的命令,在参考了 ioctl-number.txt之后选出,8 位宽。
②Number 序号,表明设备命令中的第几个,8 位宽。
③Direction 数据传送的方向,可能的值是 _IOC_NONE(没有数据传输), _IOC_READ, _IOC_WRITE。 数据传送是从应用程序的观点来看待的,_IOC_READ 意思是从设备读。
④Size 用户数据的大小。(13/14位宽,视处理器而定)
老谢说要是一位一位在那定义太烦,所以就给了我们几个宏来定义。
内核提供了下列宏来帮助定义命令:
v _IO(type,nr) 没有参数的命令
v _IOR(type,nr,datatype) 从驱动中读数据
v _IOW(type,nr,datatype) 写数据到驱动
v _IOWR(type,nr,datatype) 双向传送,type 和 number 成员作为参数被传递。
定义命令的实例:
- <span style="font-size:18px;">/* 定义幻数 */
- #define MEMDEV_IOC_MAGIC 'k'
- /* 定义命令 */
- #define MEMDEV_IOCPRINT _IO(MEMDEV_IOC_MAGIC, 1)
- #define MEMDEV_IOCGETDATA _IOR(MEMDEV_IOC_MAGIC, 2, int)
- #define MEMDEV_IOCSETDATA _IOW(MEMDEV_IOC_MAGIC, 3, int)
- #define MEMDEV_IOC_MAXNR 3</span>
2.Ioctl定义结束后开始进行使用这个命令
①返回值:Ioctl函数的实现通常是根据命令执行的一个switch语句。但是,当命令号不能
匹配任何一个设备所支持的命令时,通常返回-EINVAL(“非法参数”)。
②参数:在Ioctl的参数中,如果是一个整数,可以直接使用。如果是指针,我们必须确保这个用户地址是有效的,因此使用前需进行正确的检查。
③参数的检查:
不需要检测:
v copy_from_user
v copy_to_user
v get_user
v put_user
需要检测:
v __get_user
v __put_user
参数检查的函数原型:int access_ok(int type, const void *addr, unsigned long size)
第一个参数是 VERIFY_READ 或者 VERIFY_WRITE,用来表明是读用户内存还是写用户内存。addr 参数是要操作的用户内存地址,size 是操作的长度。如果 ioctl 需要从用户空间读一个整数,那么size参数等于 sizeof(int)。access_ok 返回一个布尔值: 1 是成功(存取没问题)和 0 是失败(存取有问题),如果该函数返回失败, 则Ioctl应当返回
–EFAULT 。
参数检查的实例:
- <span style="font-size:18px;">/* 根据命令类型,检测参数空间是否可以访问 */
- if (_IOC_DIR(cmd) & _IOC_READ)
- err = !access_ok(VERIFY_WRITE, (void *)arg, _IOC_SIZE(cmd));
- //关于这里的_IOC_READ和VERIFY_WRITE是因为读用户内存,然后写到设备空间。
- else if (_IOC_DIR(cmd) & _IOC_WRITE)
- err = !access_ok(VERIFY_READ, (void *)arg, _IOC_SIZE(cmd));
- if (err)
- return -EFAULT;</span>
关于Ioctl函数的实现实例:
- <span style="font-size:18px;">/* 根据命令,执行相应的操作 */
- switch(cmd)
- {
- /* 打印当前设备信息 */
- case MEMDEV_IOCPRINT:
- printk("<--- CMD MEMDEV_IOCPRINT Done--->\n\n");
- break;
- /* 获取参数 */
- case MEMDEV_IOCGETDATA:
- ioarg = 1101;
- ret = __put_user(ioarg, (int *)arg);
- break;
- /* 设置参数 */
- case MEMDEV_IOCSETDATA:
- ret = __get_user(ioarg, (int *)arg);
- printk("<--- In Kernel MEMDEV_IOCSETDATA ioarg = %d --->\n\n",ioarg);
- break;
- default:
- return -EINVAL;
- }
- return ret;
- }</span>
下面附上整个程序的源代码(来自例程)。
头文件部分:
- <span style="font-size:18px;">#ifndef _MEMDEV_H_
- #define _MEMDEV_H_
- #include <linux/ioctl.h>
- #ifndef MEMDEV_MAJOR
- #define MEMDEV_MAJOR 0 /*预设的mem的主设备号*/
- #endif
- #ifndef MEMDEV_NR_DEVS
- #define MEMDEV_NR_DEVS 2 /*设备数*/
- #endif
- #ifndef MEMDEV_SIZE
- #define MEMDEV_SIZE 4096
- #endif
- /*mem设备描述结构体*/
- struct mem_dev
- {
- char *data;
- unsigned long size;
- };
- /* 定义幻数 */
- #define MEMDEV_IOC_MAGIC 'k'
- /* 定义命令 */
- #define MEMDEV_IOCPRINT _IO(MEMDEV_IOC_MAGIC, 1)
- #define MEMDEV_IOCGETDATA _IOR(MEMDEV_IOC_MAGIC, 2, int)
- #define MEMDEV_IOCSETDATA _IOW(MEMDEV_IOC_MAGIC, 3, int)
- #define MEMDEV_IOC_MAXNR 3
- #endif /* _MEMDEV_H_ */
- </span>
驱动程序部分:
- <span style="font-size:18px;">/*
- 程序:字符设备驱动关于Ioctl函数的使用
- 整体思路:①初始化驱动函数
- ②对模拟字符设备的内存进行分配
- ③编写file_opetrions所对应的函数
- 日期:2013年5月22日12:41:32
- 作者:CYX
- */
- #include <linux/module.h>
- #include <linux/types.h>
- #include <linux/fs.h>
- #include <linux/errno.h>
- #include <linux/mm.h>
- #include <linux/sched.h>
- #include <linux/init.h>
- #include <linux/cdev.h>
- #include <asm/io.h>
- #include <asm/system.h>
- #include <asm/uaccess.h>
- #include "memdev.h" //本地调用的头文件
- static mem_major = MEMDEV_MAJOR; //定义了主设备号,为全局变量,整个程序都可以使用。
- module_param(mem_major, int, S_IRUGO);//定义模块参数
- struct mem_dev *mem_devp; //定义设备结构指针,在宏中有定义,主要定义了模拟的那块内存
- //在分配内存部分会进行结构体具体补充。
- struct cdev cdev; //定义cdev结构体来描述字符设备,定义成全局的,整个程序都可以使用。
- /*文件打开的函数
- 参数含义:inode 存储文件物理信息的结构(包含设备号)
- filp 文件的指针
- */
- int mem_open(struct inode *inode, struct file *filp)
- {
- struct mem_dev *dev; //设备描述结构体。
- int num = MINOR(inode->i_rdev); //取次设备号。
- if (num >= MEMDEV_NR_DEVS)
- return -ENODEV;
- dev = &mem_devp[num]; //这是一个设备号的指针数组。
- /*将设备描述结构指针赋值给文件私有数据指针*/
- filp->private_data = dev;
- }
- /*IO操作*/
- int (*ioctl)(structinode*inode,struct file *filp,unsigned int cmd,unsignedlong arg)
- {
- int err = 0;
- int ret = 0;
- int ioarg = 0;
- /* 检测命令的有效性 */
- if (_IOC_TYPE(cmd) != MEMDEV_IOC_MAGIC)
- return -EINVAL;
- if (_IOC_NR(cmd) > MEMDEV_IOC_MAXNR)
- return -EINVAL;
- /* 根据命令类型,检测参数空间是否可以访问 */
- if (_IOC_DIR(cmd) & _IOC_READ)
- err = !access_ok(VERIFY_WRITE, (void *)arg, _IOC_SIZE(cmd));
- //关于这里的_IOC_READ和VERIFY_WRITE是因为读用户内存,然后写到设备空间。
- else if (_IOC_DIR(cmd) & _IOC_WRITE)
- err = !access_ok(VERIFY_READ, (void *)arg, _IOC_SIZE(cmd));
- if (err)
- return -EFAULT;
- /* 根据命令,执行相应的操作 */
- switch(cmd)
- {
- /* 打印当前设备信息 */
- case MEMDEV_IOCPRINT:
- printk("<--- CMD MEMDEV_IOCPRINT Done--->\n\n");
- break;
- /* 获取参数 */
- case MEMDEV_IOCGETDATA:
- ioarg = 1101;
- ret = __put_user(ioarg, (int *)arg); //__put_user,所以这里要使用参数检查。
- break;
- /* 设置参数 */
- case MEMDEV_IOCSETDATA:
- ret = __get_user(ioarg, (int *)arg);
- printk("<--- In Kernel MEMDEV_IOCSETDATA ioarg = %d --->\n\n",ioarg);
- break;
- default:
- return -EINVAL;
- }
- return ret;
- }
- /*文件释放函数*/
- int mem_release(struct inode *inode, struct file *filp)
- //函数参数:inode存储文件物理信息的结构(包含设备号) file结构
- {
- return 0;
- }
- /*读函数
- 函数参数: struct file *filp(open的时候系统产生的结构,根据inode来的)
- char __user *buf 存储读取数据的空间
- size_t size 读取的大小 size_t应该就是typedef unsigned int的类型
- loff_t *ppos 当前文件指针的位置
- */
- static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
- {
- unsigned long p = *ppos;//保存下文件指针的当前位置
- unsigned int count = size;
- int ret = 0;
- struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
- //这个在文件的打开中已经定义了。private_data是一个空指针用来接收各种数据的传递
- /*判断读位置是否有效*/
- if (p >= MEMDEV_SIZE)
- //MEMDEV_SIZE是设备的大小,所以读取的大小绝对不可以大于设备的大小
- return 0;
- if (count > MEMDEV_SIZE - p)
- //假如读到的内容比剩下的还多,那么只能让你读到剩下的大小。count = MEMDEV_SIZE - p;
- count = MEMDEV_SIZE - p;
- /*读数据到用户空间*/
- if (copy_to_user(buf, (void*)(dev->data + p), count))
- {
- ret = - EFAULT;
- }
- else
- {
- *ppos += count; //ppos,读写指针的位置
- ret = count; //返回实际读到的数据量
- printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
- }
- return ret;
- }
- //写函数
- static ssize_t mem_write (struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
- {
- unsigned long p = *ppos;
- unsigned int count = size;
- int ret = 0;
- struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
- //和读函数时一个道理,读函数和写函数都不能够自己活得设备号,所以都要借助于打开函数。
- /*分析和获取有效的写长度*/
- if (p >= MEMDEV_SIZE)
- return 0;
- if (count > MEMDEV_SIZE - p)
- count = MEMDEV_SIZE - p;
- *从用户空间写入数据*/
- if (copy_from_user(dev->data + p, buf, count))
- ret = - EFAULT;
- else
- {
- *ppos += count;
- ret = count;
- printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
- }
- return ret;
- }
- /* seek文件定位函数 */
- //定位函数最主要的作用就是当写入这块内存的时候文件指针到达2的位置
- //而当读取的时候就开始从2的位置开始读,所以如果那样就永远也读不到自己想要的东东了。
- static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
- //whence初始位置,offset文件偏移。
- {
- loff_t newpos; //老谢说loff_t其实就是一个整型,我没有去追。
- switch(whence)
- {
- case 0: /* SEEK_SET */
- //由于SEEK_SET实际上是一个整形的,所以0就是头了。
- newpos = offset;
- break;
- case 1: /* SEEK_CUR */
- newpos = filp->f_pos + offset;//filp->f_pos从这个地方可以取当前位置。
- break;
- case 2: /* SEEK_END */
- newpos = MEMDEV_SIZE -1 + offset;
- break;
- default: /* can't happen */
- return -EINVAL;
- }
- //需要进行一下整体的判断。
- if ((newpos<0) || (newpos>MEMDEV_SIZE))
- return -EINVAL;
- filp->f_pos = newpos;
- return newpos;
- }
- /*文件操作结构体*/
- //这个是很重点的地方
- static const struct file_operations mem_fops =
- {
- .owner = THIS_MODULE,//函数名都可以自己定义 都是函数指针
- .llseek = mem_llseek,
- .read = mem_read,
- .write = mem_write,
- .open = mem_open,
- .release = mem_release,
- };
- /*模块加载函数*/
- static int memdev_init (void)
- {
- int result;
- int i;
- dev_t devno = MKDEV(mem_major, 0);
- /*静态申请设备号*/
- if (mem_major) //判断mem_major是否大于零,但是在宏中已经定义了它就是大于0的。
- {
- result = int register_chrdev_region(devno,2, "memdev")
- /*三个参数的含义:devno 希望申请使用的设备号
- 2 希望申请适用的设备数目
- "memdev" 设备名(体现在/proc/devices)*/
- }
- else
- {
- result = int alloc_chrdev_region(&devno, 0, 2,"memdev")
- /*四个参数的含义:&devno 分配到的设备号
- 0 起始的次设备号
- 2 需要分配的设备数目
- "memdev" 设备名(体现在/proc/devices)*/
- mem_major = MAJOR(devno); //宏定义的函数,提取主设备号。
- /*由于这个时候是动态申请的设备号,所以要对主设备号进行提取。*/
- }
- //申请失败。
- if (result < 0)
- return result;
- //由于在最开始处已经分配了cdev空间,所以此处不必再分配cdev空间了。
- cdev_init(&cdev, &mem_fops); //初始化cdev成员。
- cdev.owner = THIS_MODULE;
- cdev.ops = &mem_fops;
- //执行成功以后,驱动程序就在内核中注册好了。
- cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);//添加设备
- /*为所定义的内存分配空间*/
- mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
- if (!mem_devp) //头文件中定义有设备描述结构体
- {
- result = - ENOMEM;
- goto fail_malloc;
- }
- //分配空间结束后为所分配出来的描述结构进行清零。
- memset(mem_devp, 0, sizeof(struct mem_dev)); //清零的操作
- /*为设备分配内存*/
- for (i=0; i < MEMDEV_NR_DEVS; i++)
- {
- mem_devp[i].size = MEMDEV_SIZE; //这是4K的大小,宏中有定义。使用4K的内存来模拟一个设备。
- mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);//分配出4K的内存来给data指针。
- memset(mem_devp[i].data, 0, MEMDEV_SIZE);
- }
- return 0;
- //goto到达的位置
- fail_malloc:
- unregister_chrdev_region(devno, 1);
- return result; //鬼知道这玩意整哪去了。。
- }
- /*模块卸载函数*/
- static void memdev_exit(void)
- {
- cdev_del(&cdev); /*注销设备*/
- kfree(mem_devp); /*释放设备结构体内存*/
- unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*释放设备号*/
- }
- MODULE_AUTHOR("CYX");
- MODULE_LICENSE("GPL");
- module_init(memdev_init); //这里指定的就是整个程序的入口
- module_exit(memdev_exit);</span>
应用程序部分:
- <span style="font-size:18px;">#include <stdio.h>
- #include<sys/types.h>
- #include<sys/stat.h>
- #include<fcntl.h>
- #include "memdev.h" /* 包含命令定义 */
- int main()
- {
- int fd = 0;
- int cmd;
- int arg = 0;
- char Buf[4096];
- /*打开设备文件*/
- fd = open("/dev/memdev0",O_RDWR);
- if (fd < 0)
- {
- printf("Open Dev Mem0 Error!\n");
- return -1;
- }
- /* 调用命令MEMDEV_IOCPRINT (打印当前设备信息)*/
- printf("<--- Call MEMDEV_IOCPRINT --->\n");
- cmd = MEMDEV_IOCPRINT;
- if (ioctl(fd, cmd, &arg) < 0) //向内核传递
- {
- printf("Call cmd MEMDEV_IOCPRINT fail\n");
- return -1;
- }
- /* 调用命令MEMDEV_IOCSETDATA */
- printf("<--- Call MEMDEV_IOCSETDATA --->\n");
- cmd = MEMDEV_IOCSETDATA;
- arg = 2007;
- if (ioctl(fd, cmd, &arg) < 0)
- {
- printf("Call cmd MEMDEV_IOCSETDATA fail\n");
- return -1;
- }
- /* 调用命令MEMDEV_IOCGETDATA */
- printf("<--- Call MEMDEV_IOCGETDATA --->\n");
- cmd = MEMDEV_IOCGETDATA;
- if (ioctl(fd, cmd, &arg) < 0)
- {
- printf("Call cmd MEMDEV_IOCGETDATA fail\n");
- return -1;
- }
- printf("<--- In User Space MEMDEV_IOCGETDATA Get Data is %d --->\n\n",arg);
- close(fd);
- return 0;
- }
- </span>
Makefile:
- <span style="font-size:18px;">ifneq ($(KERNELRELEASE),)
- obj-m := memdev.o
- else
- KDIR := /home/guoqian/4-3-1/linux-2.6.29
- all:
- make -C $(KDIR) M=$(PWD) modules ARCH=arm CROSS_COMPILE=arm-linux-
- clean:
- rm -f *.ko *.o *.mod.o *.mod.c *.symvers modul*
- endif</span>