文章目录
- 概念
- 用户空间 ioctl
- 驱动程序 ioctl
- ioctl 在用户与驱动之间的协议——命令码
- 实例分析
- ioctl-test.h
- ioctl-test-driver.c
- ioctl-test.c
概念
- ioctl 是设备驱动程序中设备控制接口函数,一个字符设备驱动通常会实现设备打开、关闭、读、写等功能,
- 在一些需要细分的情境下,如果需要扩展新的功能,通常以增设 ioctl() 命令的方式实现。
- 用户程序所作的只是通过命令码告诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要做的事情,所以分为用户空间ioctl和驱动程序ioctl。
用户空间 ioctl
#include <sys/ioctl.h>
int ioctl(int fd, int cmd, ...) ;
参数描述:
- fd 文件描述符
- cmd 交互协议,设备驱动将根据 cmd 执行对应操作
- … 可变参数 arg,依赖 cmd 指定长度以及类型,根据 request 命令,设备驱动程序返回输出的数据。
返回值:
- 成功时返回 0,
- 失败则返回 -1 并设置全局变量 errorno 值,因此,在用户空间使用 ioctl 时,可以使用strerror函数得到出错判断以及处理:
int ret;
ret = ioctl(fd, MYCMD);
if (ret == -1) {
printf("ioctl: %s\n", strerror(errno));
}
ioctl 的作用非常强大、灵活。不同的驱动程序内部会实现不同的 ioctl
,APP 可以使用各种 ioctl 跟驱动程序交互:可以传数据给驱动程序,也可以从驱动程序中读出数据。
驱动程序 ioctl
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
在字符设备驱动开发中,一般情况下只要实现 unlocked_ioctl
函数即可,因为在 vfs 层的代码是直接调用unlocked_ioctl 函数
ioctl 在用户与驱动之间的协议——命令码
在驱动程序中实现的ioctl函数体内,实际上是有一个
switch {case}结构
,每一个case对应一个命令码,做出一些相应的操作。怎么实现这些操作,这是每一个程序员自己的事情,因为设备都是特定的。关键在于怎么样组织命令码,因为在ioctl中命令码是唯一联系用户程序命令和驱动程序支持的途径
。
ioctl 方法第二个参数 cmd 为用户与驱动的 “协议”,理论上可以为任意 int 型数据,可以为 0、1、2、3……,但是为了确保该 “协议” 的唯一性,ioctl 命令应该使用更科学严谨的方法赋值,在linux中,提供了一种 ioctl 命令的统一格式,将 32 位 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))
参数:
- 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,涉及到 ioctl 函数 第三个参数 arg ,占据 13bit 或者 14bit(体系相关,arm 架构一般为 14 位),指定了 arg 的数据类型及长度,如果在驱动的 ioctl 实现中不检查,通常可以忽略该参数;
通常而言,为了方便会使用宏 _IOC() 衍生的接口
来直接定义 ioctl 命令:
// include/uapi/asm-generic/ioctl.h
/* used to create numbers */
//定义不带参数的 ioctl 命令,一般用于初始化设备
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
// 定义带读参数的ioctl命令(copy_to_user)
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
// 定义带写参数的 ioctl 命令(copy_from_user)
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
//定义带读写参数的 ioctl 命令
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(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)
检测函数access_ok():
static inline int access_ok(int type, const void *addr, unsigned long size)
/*
type :是VERIFY_READ 或者VERIFY_WRITE用来表明是读用户内存还是写用户内存;
addr:是要操作的用户内存地址;
size:是操作的长度。如果ioctl需要从用户空间读一个整数,那么size参数就等于sizeof(int);
返回值:Access_ok返回一个布尔值:1,是成功(存取没问题);0,是失败,ioctl返回-EFAULT;
*/
实例分析
功能:
- 假设一个带寄存器的设备,设计了一个 ioctl 接口实现:设备初始化、读写寄存器等功能。在本例中,为了携带更多的数据,ioctl 的第三个可变参数为指针类型,指向自定义的结构体
struct msg
。
ioctl-test.h
用户空间和内核空间共用的头文件,包含 ioctl 命令及相关宏定义,可以理解为一份 “协议” 文件,代码如下:
// ioctl-test.h
#ifndef __IOCTL_TEST_H__
#define __IOCTL_TEST_H__
#include <linux/ioctl.h> // 内核空间
// #include <sys/ioctl.h> // 用户空间
/* 定义设备类型 */
#define IOC_MAGIC 'c'
/* 初始化设备 */
#define IOCINIT _IO(IOC_MAGIC, 0)
/* 读寄存器 */
#define IOCGREG _IOW(IOC_MAGIC, 1, int)
/* 写寄存器 */
#define IOCWREG _IOR(IOC_MAGIC, 2, int)
#define IOC_MAXNR 3
//携带的数据
struct msg {
int addr;
unsigned int data;
};
#endif
ioctl-test-driver.c
// ioctl-test-driver.c
......
static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = test_open,
.release = test_close,
.read = test_read,
.write = etst_write,
.unlocked_ioctl = test_ioctl,
};
......
static long test_ioctl(struct file *file, unsigned int cmd, \
unsigned long arg)
{
//printk("[%s]\n", __func__);
int ret;
struct msg my_msg;
/* 检查设备类型 */
if (_IOC_TYPE(cmd) != IOC_MAGIC) {
pr_err("[%s] command type [%c] error!\n", \
__func__, _IOC_TYPE(cmd));
return -ENOTTY;
}
/* 检查序数 */
if (_IOC_NR(cmd) > IOC_MAXNR) {
pr_err("[%s] command numer [%d] exceeded!\n",
__func__, _IOC_NR(cmd));
return -ENOTTY;
}
/* 检查访问模式 */
if (_IOC_DIR(cmd) & _IOC_READ)
ret= !access_ok(VERIFY_WRITE, (void __user *)arg, \
_IOC_SIZE(cmd));
else if (_IOC_DIR(cmd) & _IOC_WRITE)
ret= !access_ok(VERIFY_READ, (void __user *)arg, \
_IOC_SIZE(cmd));
if (ret)
return -EFAULT;
switch(cmd) {
/* 初始化设备 */
case IOCINIT:
init();
break;
/* 读寄存器 */
case IOCGREG:
ret = copy_from_user(&msg, \
(struct msg __user *)arg, sizeof(my_msg));
if (ret)
return -EFAULT;
msg->data = read_reg(msg->addr);
ret = copy_to_user((struct msg __user *)arg, \
&msg, sizeof(my_msg));
if (ret)
return -EFAULT;
break;
/* 写寄存器 */
case IOCWREG:
ret = copy_from_user(&msg, \
(struct msg __user *)arg, sizeof(my_msg));
if (ret)
return -EFAULT;
write_reg(msg->addr, msg->data);
break;
default:
return -ENOTTY;
}
return 0;
}
ioctl-test.c
// ioctl-test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "ioctl-test.h"
int main(int argc, char **argv)
{
int fd;
int ret;
struct msg my_msg;
fd = open("/dev/ioctl-test", O_RDWR);
if (fd < 0) {
perror("open");
exit(-2);
}
/* 初始化设备 */
ret = ioctl(fd, IOCINIT);
if (ret) {
perror("ioctl init:");
exit(-3);
}
/* 往寄存器0x01写入数据0xef */
memset(&my_msg, 0, sizeof(my_msg));
my_msg.addr = 0x01;
my_msg.data = 0xef;
ret = ioctl(fd, IOCWREG, &my_msg);
if (ret) {
perror("ioctl read:");
exit(-4);
}
/* 读寄存器0x01 */
memset(&my_msg, 0, sizeof(my_msg));
my_msg.addr = 0x01;
ret = ioctl(fd, IOCGREG, &my_msg);
if (ret) {
perror("ioctl write");
exit(-5);
}
printf("read: %#x\n", my_msg.data);
return 0;
}
参考博文:
linux 内核 - ioctl 函数详解
Linux设备驱动之Ioctl控制