目录

一、ioctl简介

 二、用户空间ioctl

三、驱动程序 ioctl

四、 ioctl 用户与驱动之间的协议

五、案例 


一、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 是应用程序发送过来的参数
 

Logo

更多推荐