一、ioctl函数的命令定义方法:
int (*unlocked_ioctl)(struct file*filp,unsigned int cmd,unsigned long arg)
虽然其中没有指针的参数,但是通常采用arg传递指针参数。cmd是一个命令。每一个命令由一个整形数据构成(32bits),将一个命令分成四部分,每一部分实现具体的配置,设备类型(幻数)8bits,序号8bits,数据大小13/14bits,方向2bits。命令的实现实质上就是通过简单的移位操作,将各个部分组合起来而已。
一个命令的分布的大概情况如下:
|--方向位(31-30)--|---数据长度(29-16)-----|----设备类型(15-8)----|----序号(7-0)-------|
方向位主要是表示对设备的操作,比如读设备,写设备等操作以及读写设备等都具有一定的方向,2个bits只有4种方向。
数据长度表示每一次操作(读、写)数据的大小,一般而言每一个命令对应的数据大小都是一个固定的值,不会经常改变,14bits说明可以选择的数据长度最大为16k。
设备类型类似于主设备号(由于8bits,刚好组成一个字节,因此经常采用字符作为幻数,表示某一类设备的命令),用来区别不同的命令类型,也就是特定的设备类型对应特定的设备。
序号主要是这一类命令中的具体某一个,类似于次设备号(256个命令),也就是一个设备支持的命令多达256个。
在内核中用来定义命令的宏:
_IO(type,nr) 表示定义一个没有方向的命令,
_IOR(type,nr,size) 表示定义一个类型为type,序号为nr,数据大小为size的读命令
_IOW(type,nr,size) 表示定义一个类型为type,序号为nr,数据大小为size的写命令
_IOWR(type,nr,size) 表示定义一个类型为type,序号为nr,数据大小为size的写读命令
通常的type可采用某一个字母或者数字作为设备命令类型。
实际运用中通常采用如下的方法定义一个具体的命令:
//头文件
/*定义一系列的命令*/
/*幻数,主要用于表示类型*/
#define MAGIC_NUM 'k'
/*打印命令*/
#define MEMDEV_PRINTF _IO(MAGIC_NUM,1)
/*从设备读一个int数据*/
#define MEMDEV_READ _IOR(MAGIC_NUM,2,int)
/*往设备写一个int数据*/
#define MEMDEV_WRITE _IOW(MAGIC_NUM,3,int)
/*最大的序列号*/
#define MEM_MAX_CMD 3
在内核中用来解析命令的宏:
对命令进行解析的宏,用来确定具体命令的四个部分(方向,大小,类型,序号)具体如下所示:
/*确定命令的方向*/
_IOC_DIR(nr)
/*确定命令的类型*/
_IOC_TYPE(nr)
/*确定命令的序号*/
_IOC_NR(nr)
/*确定命令的大小*/
_IOC_SIZE(nr)
上面的几个宏可以用来命令,实现命令正确性的检查。
二、ioctl的实现过程主要包括如下的过程:
1)命令的检测
2)指针参数的检测
3)命令的控制switch-case语句
1、命令的检测主要包括类型的检查,数据大小,序号的检测,通过结合上面的命令解析宏可以快速的确定。
/*检查类型,幻数是否正确*/
if(_IOC_TYPE(cmd)!=MAGIC_NUM)
return -EINVAL;
/*检测命令序号是否大于允许的最大序号*/
if(_IOC_NR(cmd)> MEM_MAX_CMD)
return -EINVAL;
2、主要是指针参数的检测。指针参数主要是因为内核空间和用户空间的差异性导致的,因此需要判断来自用户空间指针的有效性。使用copy_from_user,copy_to_user,get_user,put_user之类的函数时,由于函数会实现指针参量的检测,因此可以省略,但是采用__get_user(),__put_user()之类的函数时一定要进行检测。具体的检测方法如下所示:
if(_IOC_DIR(cmd) & _IOC_READ)
err = !access_ok(VERIFY_WRITE,(void *)args,_IOC_SIZE(cmd));
else if(_IOC_DIR(cmd) & _IOC_WRITE)
err = !access_ok(VERIFY_READ,(void *)args,_IOC_SIZE(cmd));
if(err)/*返回错误*/
return -EFAULT;
当方向是读时,说明是从设备读数据,然后写到用户空间,因此要检测用户空间的指针是否可写,采用VERIFY_WRITE,而当方向是写时,说明是往设备中写数据,因此需要检测用户空间中的指针的可读性VERIFY_READ。检查通常采用access_ok()实现检测,第一个参数为读写,第二个为检测的指针,第三个为数据的大小。
3、命名的控制:
命令的控制主要是采用switch和case相结合实现的,这于window编程中的检测各种消息的实现方式是相同的。
/*根据命令执行相应的操作*/
switch(cmd)
{
case MEMDEV_PRINTF:
printk("<--------CMD MEMDEV_PRINTF Done------------>\n\n");
...
break;
case MEMDEV_READ:
ioarg = &mem_devp->data;
...
ret = __put_user(ioarg,(int *)args);
ioarg = 0;
...
break;
case MEMDEV_WRITE:
...
ret = __get_user(ioarg,(int *)args);
printk("<--------CMD MEMDEV_WRITE Done ioarg = %d--------->\n\n",ioarg);
ioarg = 0;
...
break;
default:
ret = -EINVAL;
printk("<-------INVAL CMD--------->\n\n");
break;
}
这只是基本的框架结构,实际中根据具体的情况进行修改。这样就实现了基本的命令控制。
三、文件操作支持的集合如下:
/*添加该模块的基本文件操作支持*/
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,
/*添加新的操作支持*/
.unlocked_ioctl = mem_ioctl,
};
需要注意不是ioctl,而是unlocked_ioctl。
四、access_ok
access_ok — 检查用户空间指针是否有效
注意,根据体系结构的不同,这个函数可能只是检查指针是否在用户空间范围内——在调用这个函数之后,内存访问函数可能仍然返回 -EFAULT
函数原型:
access_ok ( type, addr, size);
参数说明:
type,Type of access: VERIFY_READ or VERIFY_WRITE.请注意,VERIFY_WRITE是VERIFY_READ的超集——如果写入一个块是安全的,那么从它读取总是安全的。
addr,要检查的块的开始的用户空间指针
size,要检查的块的大小
返回值:
此函数检查用户空间中的内存块是否可用。如果可用,则返回真(非0值),否则返回假 (0) 。
用户上下文:在用户上下文。这个功能可能会休眠。
示例
static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (access_ok(VERIFY_READ, from, n))
n = __copy_from_user(to, from, n);
else /* security hole - plug it */
memset(to, 0, n);
return n;
}
static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (access_ok(VERIFY_WRITE, to, n))
n = __copy_to_user(to, from, n);
return n;
}