从北京回来了,挺累的,诺基亚的笔试挺恶心,还是自己没学到位,菜鸟还是菜鸟。把老谢讲的Ioctl写下来。
一、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 成员作为参数被传递。
定义命令的实例:
/* 定义幻数 */
#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
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 。
参数检查的实例:
/* 根据命令类型,检测参数空间是否可以访问 */
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;
关于Ioctl函数的实现实例:
/* 根据命令,执行相应的操作 */
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;
}
下面附上整个程序的源代码(来自例程)。
头文件部分:
#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_ */
驱动程序部分:
/*
程序:字符设备驱动关于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);
应用程序部分:
#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;
}
Makefile:
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