ioctl
在用户空间, ioctl 系统调用有下面的原型:
int ioctl(int fd, unsigned long cmd, ...);
ioctl 驱动方法有和用户空间版本不同的原型:
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数. cmd 参数从用户那里不改变地传下来, 并且可选的参数 arg 参数以一个 unsigned long 的形式传递, 不管它是否由用户给定为一个整数或一个指针. 如果调用程序不传递第 3 个参数, 被驱动操作收到的 arg 值是无定义的. 因为类型检查在这个额外参数上被关闭, 编译器不能警告你如果一个无效的参数被传递给 ioctl, 并且任何关联的错误将难以查找.
ioctl 命令数字应当在这个系统是唯一的, 为了阻止向错误的设备发出正确的命令而引起的错误. 为帮助程序员创建唯一的 ioctl 命令代码, 这些编码已被划分为几个位段.
定义 ioctl 命令号的正确方法使用 了4 个位段, 它们有下列的含义.
type
幻数(魔数). 占了8位,可以用数字或者字符来标记类型。
number
序(顺序)号. 它是 8位宽,代表是该类型命令下的第几个命令.
direction
数据传送的方向,占2位。如果这个特殊的命令涉及数据传送. 可能的值是 _IOC_NONE(没有数据传输), _IOC_READ, _IOC_WRITE, 和 _IOC_READ|_IOC_WRITE (数据在2个方向被传送). 数据传送是从应用程序的观点来看待的; _IOC_READ 意思是从设备读, 因此设备必须写到用户空间. 注意这个成员是一个位掩码, 因此 _IOC_READ 和 _IOC_WRITE 可使用一个逻辑 AND 操作来抽取.
size
涉及到的用户数据的大小. 这个成员的宽度是依赖体系的, 但是常常是 13 或者 14 位. 你可为你的特定体系在宏 _IOC_SIZEBITS 中找到它的值. 你使用这个 size 成员不是强制的 - 内核不检查它 – 但是它是一个好主意. 正确使用这个成员可帮助检测用户空间程序的错误并使你实现向后兼容, 如果你曾需要改变相关数据项的大小. 如果你需要更大的数据结构, 但是, 你可忽略这个 size 成员. 我们很快见到如何使用这个成员.
定义宏来帮助建立命令号, 如下:
_IO(type,nr)(给没有参数的命令),
_IOR(type, nre, datatype)(给从驱动中读数据的),
_IOW(type,nr,datatype)(给写数据),
_IOWR(type,nr,datatype)(给双向传送).
type 和 number 成员作为参数被传递, 并且 size 成员通过应用 sizeof 到 datatype 参数而得到.
驱动中来解码这个号: _IOC_DIR(cmd), _IOC_TYPE(cmd), _IOC_NR(cmd), 和 _IOC_SIZE(cmd).
使用 ioctl 参数
我们需要涉及的另一点是如何使用这个额外的参数. 如果它是一个整数, 就容易,它可以直接使用. 但是如果它是一个指针, 必须小心些.
当用一个指针引用用户空间, 我们必须确保用户地址是有效的. 试图存取一个没验证过的用户提供的指针可能导致不正确的行为, 一个内核 oops, 系统崩溃, 或者安全问题. 它是驱动的责任来对每个它使用的用户空间地址进行正确的检查, 并且返回一个错误如果它是无效的.
copy_from_user 和 copy_to_user 函数, 它们可用来安全地移动数据到和从用户空间. 这些函数也可用在 ioctl 方法中, 但是 ioctl 调用常常包含小数据项, 可通过其他方法更有效地操作. 开始, 地址校验(不传送数据)由函数 access_ok 实现, 它定义在 :
int access_ok(int type, const void *addr, unsigned long size);
在较新一点得内核中是
int access_ok(const void *addr, unsigned long size);
第一个参数应当是 VERIFY_READ 或者 VERIFY_WRITE, 依据这个要进行的动作是否是读用户空间内存区或者写它. addr 参数持有一个用户空间地址, size 是一个字节量. 例如, 如果 ioctl 需要从用户空间读一个整数, size 是 sizeof(int). 如果你需要读和写给定地址, 使用 VERIFY_WRITE, 因为它是 VERIRY_READ 的超集.
不象大部分的内核函数, access_ok 返回一个布尔值: 1 是成功(存取没问题)和 0 是失败(存取有问题). 如果它返回假, 驱动应当返回 -EFAULT 给调用者.
示例代码:
mioctl.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/ioctl.h>
#include <linux/uaccess.h>
#include "cmd.h"
#define DEV_NAME "ioctl_test"
#define DEV_NUM (1)
static int kernel_num = 10;
static struct my_st
{
int major;
int minor;
dev_t dev_num;
struct cdev cdev;
struct class *class;
struct device *device;
}*my;
static int ioctl_open(struct inode *inode,struct file *flip)
{
printk("open()\r\n");
return 0;
}
static int ioctl_release(struct inode *inode,struct file *flip)
{
printk("close()\r\n");
return 0;
}
static long ioctl_ioctl(struct file *flip,unsigned int cmd,unsigned long arg)
{
int err;
void __user *argp = (void __user *)arg;
if(_IOC_DIR(cmd) & _IOC_READ)
err = !access_ok(argp,_IOC_SIZE(cmd));
else if(_IOC_DIR(cmd) & _IOC_WRITE)
err = !access_ok(argp,_IOC_SIZE(cmd));
if(err)
return -EFAULT;
switch(cmd)
{
case DEV_CMD:
printk("cmd\r\n");
break;
case DEV_GET_CMD:
copy_to_user(argp,&kernel_num,_IOC_SIZE(cmd));
printk("get cmd\r\n");
break;
case DEV_SET_CMD:
copy_from_user(&kernel_num,argp,_IOC_SIZE(cmd));
printk("data = %d\r\n",kernel_num);
printk("set cmd\r\n");
break;
default:
printk("error");
return -EINVAL;
}
return 0;
}
static ssize_t ioctl_write(struct file *flip,const char __user *buf,size_t count,loff_t *offset)
{
return 0;
}
static ssize_t ioctl_read(struct file *flip,char __user *buf,size_t count,loff_t *offset)
{
return 0;
}
static const struct file_operations fops ={
.owner = THIS_MODULE,
.open = ioctl_open,
.read = ioctl_read,
.write = ioctl_write,
.unlocked_ioctl = ioctl_ioctl,
.release = ioctl_release,
};
static int __init mioctl_init(void)
{
my = kzalloc(sizeof(struct my_st),GFP_KERNEL);
if(my == NULL)
return -ENOMEM;
int ret = alloc_chrdev_region(&my->dev_num,DEV_NUM,0,DEV_NAME);
if(ret < 0)
goto alloc_chrdev_failed;
my->major = MAJOR(my->dev_num);
my->minor = MINOR(my->dev_num);
cdev_init(&my->cdev,&fops);
my->cdev.owner = THIS_MODULE;
ret = cdev_add(&my->cdev,my->dev_num,DEV_NUM);
if(ret < 0)
goto cdev_add_failed;
my->class = class_create(THIS_MODULE,DEV_NAME);
if(IS_ERR(my->class))
{
ret = PTR_ERR(my->class);
goto class_create_failed;
}
my->device = device_create(my->class,NULL,my->dev_num,NULL,DEV_NAME);
if(IS_ERR(my->device))
{
ret = PTR_ERR(my->device);
goto device_create_failed;
}
printk("ioctl device is ok !\r\n");
printk("主设备号:%d\r\n",my->major);
return 0;
device_create_failed:
class_destroy(my->class);
class_create_failed:
cdev_del(&my->cdev);
cdev_add_failed:
unregister_chrdev_region(my->dev_num,DEV_NUM);
alloc_chrdev_failed:
kfree(my);
return ret;
}
static void __exit mioctl_exit(void)
{
device_destroy(my->class,my->dev_num);
class_destroy(my->class);
cdev_del(&my->cdev);
unregister_chrdev_region(my->dev_num,DEV_NUM);
kfree(my);
printk("ioctl device was remove\r\n");
}
module_init(mioctl_init);
module_exit(mioctl_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Chen");
Makefile
ifneq ($(KERNELRELEASE),)
obj-m:=mioctl.o
else
CURRENT_DIR:=$(shell pwd)
KERNEL_DIR:=/lib/modules/$(shell uname -r)/build
ccflags-y:=-std=gnu99 -Wno-declaration-after-statement
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURRENT_DIR) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURRENT_DIR) clean
endif
cmd.h
#ifndef __CMD_H
#define __CMD_H
#define DEV_TYPE 'c'
#define DEV_CMD _IO(DEV_TYPE,0)
#define DEV_GET_CMD _IOR(DEV_TYPE,1,int)
#define DEV_SET_CMD _IOW(DEV_TYPE,2,int)
#endif
测试代码app.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "cmd.h"
int main()
{
int arg = 30;
int fd = open("/dev/ioctl_test",O_RDWR);
if(fd < 0)
{
perror("");
exit(1);
}
int ret = ioctl(fd,DEV_GET_CMD,&arg);
if(ret < 0)
{
perror("");
exit(1);
}
int ret = ioctl(fd,DEV_GET_CMD,&arg);
if(ret < 0)
{
perror("");
exit(1);
}
printf("arg = %d\r\n",arg);
arg = 100;
ret = ioctl(fd,DEV_SET_CMD,&arg);
if(ret < 0)
{
perror("");
exit(1);
}
close(fd);
exit(1);
}