Linux之设备控制接口(ioctl)
ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。对I/O通道进行管理,就是对设备的一些特性进行控制。虽然在文件操作结构体struct file_operations中有很多对应的设备操作函数,但是有些命令是实在找不到对应的操作函数,拓展一些file_operations给出的接口中没有的自定义功能,则需要使用到ioctl()函数。如CD-ROM的驱动,想要一个弹出光驱的操作,这种操作并
目录
一、ioctl简介
ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。对I/O通道进行管理,就是对设备的一些特性进行控制。
虽然在文件操作结构体struct file_operations中有很多对应的设备操作函数,但是有些命令是实在找不到对应的操作函数, 拓展一些file_operations给出的接口中没有的自定义功能,则需要使用到ioctl()函数。如CD-ROM的驱动,想要一个弹出光驱的操作,这种操作并不是所有的字符设备都需要的,所以文件操作结构体也不会有对应的函数操作。
出于这样的原因,一些没办法归类的函数就统一放在ioctl这个函数操作中,通过指定的命令来实现对应的操作。所以,ioctl函数里面都实现了多个的对硬件的操作,通过应用层传入的命令来调用相应的操作。
ioctl函数调用流程:
二、用户空间ioctl
首先,需要规定一些命令码,这些命令码在应用程序和驱动程序中需要保持一致。应用程序只需向驱动程序下发一条指令码,用来通知它执行哪条命令。如何解读这条指令和怎么实现相关操作,就是驱动程序自己要做的事。
应用程序的接口函数为ioctl,参考官方文档,函数原型为
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
fd:文件描述符
request:命令码,应用程序通过下发命令码来控制驱动程序完成对应操作。
…:可变参数arg,一些情况下应用程序需要向驱动程序传参,参数就通过arg来传递。
返回值:如果ioctl执行成功,它的返回值就是驱动程序中ioctl接口给的返回值,驱动程序可以通过返回值向用户程序传参。但驱动程序最好返回一个非负数,因为用户程序中的ioctl运行失败时一定会返回-1并设置全局变量errorno。
ioctl函数中的“…”只能传递一个参数,但内核不会检查这个参数的类型。那么就有两种传参方式:只传一个整数,传递一个指针。
errono不同的值代表的含义:
EBADF:fd是一个无效的文件描述符。
EFAULT:在arg是指针的前提下,argp指向一个不可访问的内存空间。
EINVAL:request或argp是无效的。
ENOTTY:fd没有关联到一个字符特殊设备,或该request不适用于文件描述符fd引用的对象类型。也就是fd没有指向一个字符设备,或fd指向的文件不支持ioctl操作
在用户空间使用 ioctl 时,可以做如下的出错判断以及处理:
#include <string.h>
#include <errno.h>
int ret;
ret = ioctl(fd, MYCMD);
if (ret == -1)
{
printf("ioctl: %s\n", strerror(errno));
}
在实际应用中,ioctl 最常见的 errorno 值为 ENOTTY(error not a typewriter),即第一个参数 fd 指向的不是一个字符设备,不支持 ioctl 操作,这时候应该检查前面的 open 函数是否出错或者设备路径是否正确。
三、驱动程序 ioctl
在驱动程序的ioctl函数体中,实现了一个switch-case结构,每一个case对应一个命令码,case内部是驱动程序实现该命令的相关操作。
ioctl的实现函数要传递给file_operations结构体中对应的函数指针,函数原型为
#include <linux/ioctl.h>
long (*unlocked_ioctl) (struct file * fp, unsigned int request, unsigned long args);
long (*compat_ioctl) (struct file * fp, unsigned int request, unsigned long args);
inode和fp用来确定被操作的设备
request就是用户程序下发的命令
args就是用户程序在必要时传递的参数
返回值:可以在函数体中随意定义返回值,这个返回值也会被直接返回到用户程序中。通常使用非负数表示正确的返回,而返回一个负数系统会判定为ioctl调用失败。
unlocked_ioctl在无大内核锁(BKL)的情况下调用。64位用户程序运行在64位的kernel,或32位的用户程序运行在32位的kernel上,都是调用unlocked_ioctl函数。
compat_ioctl是64位系统提供32位ioctl的兼容方法,也在无大内核锁的情况下调用。即如果是32位的用户程序调用64位的kernel,则会调用compat_ioctl。如果驱动程序没有实现compat_ioctl,则用户程序在执行ioctl时会返回错误Not a typewriter。
在字符设备驱动开发中,一般情况下只要实现 unlocked_ioctl 函数即可,因为在 vfs 层的代码是直接调用 unlocked_ioctl 函数
// fs/ioctl.c
static long vfs_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
int error = -ENOTTY;
if (!filp->f_op || !filp->f_op->unlocked_ioctl)
goto out;
error = filp->f_op->unlocked_ioctl(filp, cmd, arg);
if (error == -ENOIOCTLCMD) {
error = -ENOTTY;
}
out:
return error;
}
四、 ioctl 用户与驱动之间的协议
cmd参数在用户程序端由一些宏根据设备类型、序列号、传送方向、数据尺寸等生成,这个整数通过系统调用传递到内核中的驱动程序,再由驱动程序使用解码宏从这个整数中得到设备的类型、序列号、传送方向、数据尺寸等信息,然后通过switch{case}结构进行相应的操作。
ioctl函数的第二个参数 cmd 为用户与驱动的协议,理论上可以为任意 int 型数据,可以为 0、1、2、3……,但是为了确保该协议的唯一性,ioctl 命令应该使用更科学严谨的方法赋值,在linux中,提供了一种 ioctl 命令的统一格式,将 32 位 int 型数据划分为四个位段,如下图所示:
①dir(direction):ioctl 命令访问模式(数据传输方向),占据 2 bit,可以为 _IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,分别指示了四种访问模式:无数据、读数据、写数据、读写数据
②type(device type):设备类型,占据 8 bit,也称为 “幻数” 或者 “魔数”,可以为任意 char 型字符,例如‘a’、’b’、’c’ 等等,其主要作用是使 ioctl 命令有唯一的设备标识
③nr(number):命令编号/序数,占据 8 bit,可以为任意 unsigned char 型数据,取值范围 0~255,如果定义了多个 ioctl 命令,通常从 0 开始编号递增
④size:与体系结构相关,ARM下占14bit(_IOC_SIZEBITS),如果数据是int,内核给这个赋的值就是sizeof(int)。
在内核中,提供了宏接口以生成上述格式的 ioctl 命令:
#include/uapi/asm-generic/ioctl.h
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
通常,为了方便会使用宏 _IOC() 衍生的接口来直接定义 ioctl 命令:
#include/uapi/asm-generic/ioctl.h
/* used to create numbers */
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
_IO(type, nr):用来定义不带参数的ioctl命令
_IOR(type,nr,size):用来定义用户程序向驱动程序写参数的ioctl命令
_IOW(type,nr,size):用来定义用户程序从驱动程序读参数的ioctl命令
_IOWR(type,nr,size):用来定义带读写参数的驱动命令
同时,内核还提供了反向解析 ioctl 命令的宏接口:
// include/uapi/asm-generic/ioctl.h
/* used to decode ioctl numbers */
#define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
#define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)
_IOC_DIR(nr) :提取方向
_IOC_TYPE(nr) :提取幻数
_IOC_NR(nr) :提取序数
_IOC_SIZE(nr) :提取数据大小
- 通常来说,使用read write接口,来读写数据;使用ioctl接口设置一些属性
- ioctl接口既可以读,也可以写,但是读写大数据的效率不如使用read write接口高
五、案例
驱动部分源文件
#define CLOSE_CMD (_IO(0XEF,0X1)) //关闭定时器
#define OPEN_CMD (_IO(0XEF,0X2)) //打开定时器
#define SETPERIOD_CMD (_IO(0XEF,0X3)) //设置定时器周期
static const struct file_operations timer_fops = {
.open = timer_open,
.unlocked_ioctl = timer_unlocked_ioctl,
.owner = THIS_MODULE,
};
//ioctl 函数
//filp : 要打开的设备文件(文件描述符)
//cmd : 应用程序发送过来的命令
//arg : 参数
static long timer_unlocked_ioctl(struct file *filp,unsigned int cmd, unsigned long arg)
{
struct timer_dev *dev = (struct timer_dev *)filp->private_data;
int timerperiod;
unsigned long flags;
switch (cmd)
{
case CLOSE_CMD: //关闭定时器
del_timer_sync(&dev->timers);
break;
case OPEN_CMD: //打开定时器
spin_lock_irqsave(&dev->lock, flags);
timerperiod = dev->timeperiod;
spin_unlock_irqrestore(&dev->lock, flags);
mod_timer(&dev->timers, jiffies +msecs_to_jiffies(timerperiod));
break;
case SETPERIOD_CMD: // 设置定时器周期
spin_lock_irqsave(&dev->lock, flags);
timerperiod = dev->timeperiod;
spin_unlock_irqrestore(&dev->lock, flags);
mod_timer(&dev->timers, jiffies +msecs_to_jiffies(arg));
break;
default:
break;
}
return 0;
}
应用部分源程序
if(cmd == 1) /* 关闭LED灯 */
cmd = CLOSE_CMD;
else if(cmd == 2) /* 打开LED灯 */
cmd = OPEN_CMD;
else if(cmd == 3)
{
cmd = SETPERIOD_CMD; /* 设置周期值 */
printf("Input Timer Period:");
ret = scanf("%d", &arg);
if (ret != 1) /* 参数输入错误 */
{
gets(str); /* 防止卡死 */
}
}
ioctl(fd, cmd, arg); /* 控制定时器的打开和关闭 */
函数 timer_unlocked_ioctl,对应应用程序的 ioctl 函数,应用程序调用 ioctl函数向驱动发送控制信息,此函数响应并执行。此函数有三个参数: filp, cmd 和 arg,其中 filp是对应的设备文件, cmd 是应用程序发送过来的命令信息, arg 是应用程序发送过来的参数。
更多推荐
所有评论(0)