1、ioctl简介
ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。对I/O通道进行管理,就是对设备的一些特性进行控制。
虽然在文件操作结构体struct file_operations中有很多对应的设备操作函数,但是有些命令是实在找不到对应的操作函数, 拓展一些file_operations给出的接口中没有的自定义功能,则需要使用到ioctl()函数。如CD-ROM的驱动,想要一个弹出光驱的操作,这种操作并不是所有的字符设备都需要的,所以文件操作结构体也不会有对应的函数操作。
出于这样的原因,一些没办法归类的函数就统一放在ioctl这个函数操作中,通过指定的命令来实现对应的操作。所以,ioctl函数里面都实现了多个的对硬件的操作,通过应用层传入的命令来调用相应的操作。
ioctl函数调用流程:
1.1、用户空间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 函数是否出错或者设备路径是否正确。
1.2、驱动程序 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;
}
1.3、 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 numb