【专业技术】Linux设备驱动第六篇:高级字符驱动操作之iotcl

在之前我们介绍了如何实现一个简单的字符设备驱动,并介绍了简单的open,close,read,write等驱动提供的基本功能。但是一个真正的设备驱动往往提供了比简单读写更高级的功能。这一篇我们就来介绍一些驱动动中使用的一些高级的操作的实现。

大部分驱动除了提供对设备的读写操作外,还需要提供对硬件控制的接口,比如查询一个framebuffer设备能提供多大的分辨率,读取一个RTC设备的时间,设置一个gpio的高低电平等等。而这些对硬件操作能力的实现一般都是通过ioctl方法来实现的

1.原型介绍

Ioctl在用户空间的原型为:

int ioctl(int fd, unsigned long cmd, ...);

原型中的点不表示一个变数目的参数, 而是一个单个可选的参数, 传统上标识为 char *argp. 这些点在那里只是为了阻止在编译时的类型检查. 第 3个参数的实际特点依赖所发出的特定的控制命令( 第 2 个参数 ). 一些命令不用参数, 一些用一个整数值, 以及一些使用指向其他数据的指针. 使用一个指针是传递任意数据到 ioctl 调用的方法; 设备接着可与用户空间交换任何数量的数据.

ioctl在内核空间的原型为:

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);

inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数. cmd 参数从用户那里不改变地传下来, 并且可选的参数 arg 参数以一个 unsigned long 的形式传递, 不管它是否由用户给定为一个整数或一个指针. 如果调用程序不传递第 3 个参数, 被驱动操作收到的 arg 值是无定义的. 因为类型检查在这个额外参数上被关闭, 编译器不能对此作出警告。

2.iotcl cmd的选择

在实现ioctl之前,我们应该来定义一组ioctl命令,一种简单的方法是使用一组简单的数字来标示,比如从0到9。这种情况一般也没有问题,但是最好不要这样做,ioctl的cmd应该是在系统内是唯一的,这样可以防止向错误的设备发出正确的命令。而如果ioctl命令在系统内是唯一的,那么就不会发生这种情况。

Linux中把ioctl cmd划分成几个位段来帮助创建唯一的cmd。这几个位段一般是:type(模数),序号,传输方向和参数大小。在定义的时候可以参考include/asm/ioctl.h 和 Documentation/ioctl-number.txt两个文件,头文件定义了构建cmd命令的宏,而ioctl-number.txt列举了内核中已经使用的tpye,为了唯一性,尽量不要和这里的type重叠。

以下是这几个位段的简单介绍:

type

魔数. 只是选择一个数(在参考了 ioctl-number.txt之后)并且使用它在整个驱动中. 这个成员是 8 位宽(_IOC_TYPEBITS).

number

序(顺序)号. 它是 8 位(_IOC_NRBITS)宽.

direction

数据传送的方向,如果这个特殊的命令涉及数据传送. 可能的值是 _IOC_NONE(没有数据传输), _IOC_READ, _IOC_WRITE, 和 _IOC_READ|_IOC_WRITE (数据在2个方向被传送). 数据传送是从应用程序的观点来看待的; _IOC_READ 意思是从设备读, 因此设备必须写到用户空间. 注意这个成员是一个位掩码, 因此 _IOC_READ 和 _IOC_WRITE 可使用一个逻辑 AND 操作来抽取.

size

涉及到的用户数据的大小. 这个成员的宽度是依赖体系的, 但是常常是 13 或者 14 位. 你可为你的特定体系在宏 _IOC_SIZEBITS 中找到它的值. 你使用这个 size 成员不是强制的 - 内核不检查它 -- 但是它是一个好主意. 正确使用这个成员可帮助检测用户空间程序的错误并使你实现向后兼容, 如果你曾需要改变相关数据项的大小. 如果你需要更大的数据结构, 但是, 你可忽略这个 size 成员. 我们很快见到如何使用这个成员.

下面是一个定义ioctl命令的展示:

/* Use 'k' as magic number */
#define SCULL_IOC_MAGIC 'k'
/* Please use a different 8-bit number in your code */

#define SCULL_IOCRESET _IO(SCULL_IOC_MAGIC, 0)
/*
 * S means "Set" through a ptr,
 * T means "Tell" directly with the argument value
 * G means "Get": reply by setting through a pointer
 * Q means "Query": response is on the return value
 * X means "eXchange": switch G and S atomically
 * H means "sHift": switch T and Q atomically
 */
#define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC, 1, int)
#define SCULL_IOCSQSET _IOW(SCULL_IOC_MAGIC, 2, int)
#define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC, 3)
#define SCULL_IOCTQSET _IO(SCULL_IOC_MAGIC, 4)
#define SCULL_IOCGQUANTUM _IOR(SCULL_IOC_MAGIC, 5, int)
#define SCULL_IOCGQSET _IOR(SCULL_IOC_MAGIC, 6, int)
#define SCULL_IOCQQUANTUM _IO(SCULL_IOC_MAGIC, 7)
#define SCULL_IOCQQSET _IO(SCULL_IOC_MAGIC, 8)
#define SCULL_IOCXQUANTUM _IOWR(SCULL_IOC_MAGIC, 9, int)
#define SCULL_IOCXQSET _IOWR(SCULL_IOC_MAGIC,10, int)
#define SCULL_IOCHQUANTUM _IO(SCULL_IOC_MAGIC, 11)
#define SCULL_IOCHQSET _IO(SCULL_IOC_MAGIC, 12)
#define SCULL_IOC_MAXNR 14

关于_IOWR等宏定义更多的内容可参考头文件中的定义。

3. IOCTL的返回值

IOCTL的实现往往都是一个switch case语句,返回值依赖每个case分支的实现。当遇到没有定义的cmd时改返回什么值呢,我建议使用-EINVAL,表示无用的参数。另外一点,在case分支比较多的时候,有些人大意常常会忘记写break,导致后面的case分支同样执行,导致错误发生。

4. IOCTL的arg参数

有些ioctl命令并不需要arg这个参数,而大部分ioctl需要在应用层和内核层传递数据,就需要用到这个参数。当arg参数是一个整形的时候,非常简单,我们直接拿来使用就可以了。如果是一个指针,就需要小心一些。

应用层和内核层的数据交换我们常用的是copy_from_user 和 copy_to_user 函数,它们可用来安全地用来移动数据。 这些函数也可用在 ioctl 方法中。但是ioctl中的数据项常常是很小的数据,用这两个函数有点笨重了,我们可以尝试使用其他的方式来实现数据的传递。

int access_ok(int type, const void *addr, unsigned long size);

这个函数用来检查给定的地址是否满足特定的访问需求,这个函数只检查而没有数据copy。使用access_ok之后就可以安全地传输数据。可使用下面的接口来做数据的传输:

put_user(datum, ptr)

__put_user(datum, ptr)

这些宏定义写 datum 到用户空间;它们相对快,且应当被调用来代替 copy_to_user 无论何时要传送单个值时。这些宏已被编写来允许传递任何类型的指针到 put_user, 只要它是一个用户空间地址。传送的数据大小依赖 prt 参数的类型,并且在编译时使用 sizeof 和 typeof 等编译器内建宏确定。结果是,如果 prt 是一个 char 指针,传送一个字节,以及对于 2, 4, 和 可能的 8 字节。

put_user 检查来确保这个进程能够写入给定的内存地址,它在成功时返回 0,并且在错误时返回 -EFAULT。 __put_user 进行更少的检查(它不调用 access_ok),但是仍然能够失败如果被指向的内存对用户是不可写的。因此, __put_user 应当只用在内存区已经用 access_ok 检查过的时候。

作为一个通用的规则,当你实现一个 read 方法时,调用 __put_user 来节省几个周期,或者当你拷贝几个项时,因此, 在第一次数据传送之前调用 access_ok 一次, 如同上面 ioctl 所示。

get_user(local, ptr) __get_user(local, ptr)

这些宏定义用来从用户空间接收单个数据。它们象 put_user 和 __put_user,但是在相反方向传递数据。获取的值存储于本地变量 local; 返回值指出这个操作是否成功。再次, __get_user 应当只用在已经使用 access_ok 校验过的地址。

原文发布于微信公众号 - 程序员互动联盟(coder_online)

原文发表时间:2015-07-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏JAVA技术zhai

一线互联网常见的 14 个 Java 面试题,你颤抖了吗程序员

跳槽不算频繁,但参加过不少面试(电话面试、face to face 面试),面过大 / 小公司、互联网 / 传统软件公司,面糊过(眼高手低,缺乏实战经验,挂掉)...

1123
来自专栏Java架构

一线互联网常见的 14 个 Java 面试题,你颤抖了吗程序员

跳槽不算频繁,但参加过不少面试(电话面试、face to face 面试),面过大 / 小公司、互联网 / 传统软件公司,面糊过(眼高手低,缺乏实战经验,挂掉)...

1084
来自专栏Seebug漏洞平台

从补丁到漏洞分析——记一次joomla漏洞应急

2018年1月30日,joomla更新了3.8.4版本,这次更新修复了4个安全漏洞,以及上百个bug修复。

4138
来自专栏窗户

深入设计电子计算器(一)——CPU框架及指令集设计

前几天写了一篇《如何设计一个电子计算器》,一个朋友看了之后说实在太low,好吧,依照他的意思,那我就采用文中FPGA设计的方式,然后自己从指令集设计、cpu设...

1846
来自专栏编程

PHP7 下的协程实现

前言 相信大家都听说过『协程』这个概念吧。 但是有些同学对这个概念似懂非懂,不知道怎么实现,怎么用,用在哪,甚至有些人认为yield就是协程! 我始终相信,如果...

2408
来自专栏信安之路

二进制漏洞学习笔记

这个程序非常简单,甚至不需要你写脚本,直接运行就能获得shell。 写这个程序的目的主要是为了使第一次接触漏洞的同学更好地理解栈溢出的原理。

820
来自专栏同步博客

制作类似ThinkPHP框架中的PATHINFO模式功能(二)

  距离上一次发布的《制作类似ThinkPHP框架中的PATHINFO模式功能》(文章地址:http://www.cnblogs.com/phpstudy201...

705
来自专栏蛋未明的专栏

Node.js如何处理健壮性

2065
来自专栏叁金大数据

自学Python八 爬虫大坑之网页乱码

  Bug有时候破坏的你的兴致,阻挠了保持到现在的渴望。可是,自己又非常明白,它是一种激励,是注定要被你踩在脚下的垫脚石!

781
来自专栏架构师之旅

JVM内存管理------JAVA语言的内存管理概述

引言 内存管理一直是Java语言自豪与骄傲的资本,它让JAVA程序员基本上可以彻底忽略与内存管理相关的细节,只专注于业务逻辑。不过世界上不存在十全十...

1877

扫码关注云+社区