转自(39条消息) linux驱动---ioctl函数解析_那可真是太开心了的博客-CSDN博客_linux驱动ioctl
参考:ioctl,unlocked_ioctl 处理方法-阿里云开发者社区 (aliyun.com)
一个字符设备驱动会实现常规的打开、关闭、读、写等功能,但是在一些细分的情景下,如果需要扩展新功能,通常以增设ioctl()命令的方式实现,其作用类似于“拾遗补漏”。在文件I/O中,ioctl扮演着重要角色,本文将以驱动开发为侧重点,从用户空间到内核空间纵向分析ioctl函数。
#include
int ioctl(int fd, int cmd, ...) ;
在man手册中描述ioctl函数的作用是:操作特殊文件的底层设备参数。
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
在驱动程序中,ioctl()函数上传送的变量cmd是应用程序用于区别设备驱动程序请求处理内容的值,cmd除了可区别数字外,还包含有助于处理的几种相应信息。cmd的大小为32位,共分为4个域
在编写ioctl代码之前,需要选择对应不同命令的编号。为了防止对错误的设备使用正确的命令,命令号应该在系统范围内为唯一,这种错误匹配并不是不会发生,程序可能发现自己正在试图对FIFO和audio等这类非串行设备输入流修改波特率,如果每一个ioctl命令都是唯一的,应用程序进行这种操作时就会得到一个EINVAL错误,而不是无意间成功完成了意想不到的操作。
要按Linux内核的约定方法为驱动程序选择ioctl编号,应该首先看看include/asm/ioctl.h和Doucumention/ioctl-number.txt这两个文件。头文件定义了要使用的位字段:类型(幻数)、序数、传送方向以及参数大小等。ioctl-number.txt文件中罗列了内核所使用的幻数,选择自己的幻数要避免和内核冲突
在asm-generic/ioctl.h里有如下定义
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
再看__IOC()的定义:
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
可见,__IO()的最后结果由__IOC()中的4个参数移位组合而成。
#define _IOC_DIRSHIFT (_IOC_SIZESHIFT+_IOC_SIZEBITS)
#define _IOC_SIZESHIFT (_IOC_TYPESHIFT+_IOC_TYPEBITS)
#define _IOC_SIZEBITS 14
#define _IOC_TYPESHIFT (_IOC_NRSHIFT+_IOC_NRBITS)
#define _IOC_TYPEBITS 8
#define _IOC_NRSHIFT 0
#define _IOC_NRBITS 8
所以可以得到
_IOC_DIRSHIFT = _IOC_SIZESHIFT + 14 = (_IOC_TYPESHIFT+_IOC_TYPEBITS) + 14
= (_IOC_NRSHIFT+_IOC_NRBITS + 8) +14
= 0 + 8 + 8 +14
= 30
_IOC_TYPESHIFT = (_IOC_NRSHIFT+_IOC_NRBITS) = 0 + 8 = 8
_IOC_NRSHIFT = 0
_IOC_SIZESHIFT = (_IOC_TYPESHIFT+_IOC_TYPEBITS) = (_IOC_NRSHIFT+_IOC_NRBITS) + 8
= 0 + 8 + 8
= 16
所以,(dir) << _IOC_DIRSHIFT表示dir左移30位,得到bit31 ~ bit30两位上,得到方向(读写)的属性
(size) << _IOC_SIZESHIFT) 位左移 16 位得到“数据大小”区;
(type) << _IOC_TYPESHIFT) 左移 8位得到"魔数区" ;
(nr) << _IOC_NRSHIFT) 左移 0 位( bit7~bit0)
在asm-generic/ioctl.h中还有如下宏定义
//构造无参数的命令编号
#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)))
1、dir(direction),ioctl命令访问模式(数据传输方向),占据2bit,可以为_IOC_NONE、_IOC_READ、_IOC_WRITE _IOC_READ | _IOC_WRITE,分别指示了四种访问模式:无数据、读数据、写数据、读写数据
2、type(device type),设备类型,占据8bit,在一些文献中翻译为“幻数”或者“魔数”,可以为任意char型字符,例如‘a’、‘b’、‘c’等等,其主要作用是使ioctl命令有唯一的设备标识;
tips:Documentions/ioctl-number.txt记录了在内核中已经使用的“魔数”字符,为避免冲突,在自定义ioctl命令之前应该先查阅该文档
3、nr(number),命令编号/序数,占据8bit,可以为任意unsigned char型数据,取值范围0~255,如果定义了多个ioctl命令,通常从0开始编号递增;
4、size,涉及到ioctl第三个参数arg,占据13bit或者14bit(体系相关,arm架构一般为14位),指定了arg的数据类型及长度,如果在驱动的ioctl实现中不检查,通常可以忽略该参数。
内核空间
#define CLOSE_CMD (_IO(0XEF,0X1)) /*关闭定时器*/
#define OPEN_CMD (_IO(0XEF,0X2)) /*打开定时器*/
#define SETPERIOD_CMD (_IO(0XEF,0X3)) /*设置定时器周期命令*/
......
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->timer);
break;
case OPEN_CMD: /* 打开定时器 */
spin_lock_irqsave(&dev->lock, flags);
timerperiod = dev->timeperiod;
spin_unlock_irqrestore(&dev->lock, flags);
mod_timer(&dev->timer, jiffies +
msecs_to_jiffies(timerperiod));
break;
case SETPERIOD_CMD: /* 设置定时器周期 */
spin_lock_irqsave(&dev->lock, flags);
dev->timeperiod = arg;
spin_unlock_irqrestore(&dev->lock, flags);
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));
break;
default:
break;
}
return 0;
}
//设备操作函数
static struct file_operations timer_fops = {
.owner = THIS_MODULE,
.open = timer_open,
.unlocked_ioctl = timer_unlocked_ioctl,
};
用户空间
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "linux/ioctl.h"
/* 命令值 */
#define CLOSE_CMD (_IO(0XEF, 0x1)) /* 关闭定时器 */
#define OPEN_CMD (_IO(0XEF, 0x2)) /* 打开定时器 */
#define SETPERIOD_CMD (_IO(0XEF, 0x3)) /* 设置定时器周期命令 */
int main(int argc,char *argv[])
{
int fd,ret;
char *filename;
unsigned int cmd;
unsigned int arg;
unsigned char str[100];
if(argc != 2)
{
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename,O_RDWR);
if(fd < 0)
{
printf("cant opem file %s\r\n",filename);
return -1;
}
while (1)
{
printf("Input CMD:");
ret = scanf("%d", &cmd);
if (ret != 1) /* 参数输入错误 */
{
gets(str); /* 防止卡死 */
}
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); /* 控制定时器的打开和关闭 */
}
close(fd);
}