在为 ioctl 编写代码之前, 你需要选择对应命令的数字. 许多程序员的第一个本能的反 应是选择一组小数从0或1 开始, 并且从此开始向上. 但是, 有充分的理由不这样做. ioctl 命令数字应当在这个系统是唯一的, 为了阻止向错误的设备发出正确的命令而引起 的错误. 这样的不匹配不会不可能发生, 并且一个程序可能发现它自己试图改变一个非串 口输入系统的波特率, 例如一个 FIFO 或者一个音频设备. 如果这样的 ioctl 号是唯一 的, 这个应用程序得到一个 EINVAL 错误而不是继续做不应当做的事情.
为帮助程序员创建唯一的 ioctl 命令代码, 这些编码已被划分为几个位段. Linux 的第 一个版本使用 16-位数: 高 8 位是关联这个设备的"魔"数, 低 8 位是一个顺序号, 在设 备内唯一. 这样做是因为 Linus 是"无能"的(他自己的话); 一个更好的位段划分仅在后 来被设想. 不幸的是, 许多驱动仍然使用老传统. 它们不得不: 改变命令编码会破坏大量 的二进制程序,并且这不是内核开发者愿意见到的.
根据 Linux 内核惯例来为你的驱动选择 ioctl 号, 你应当首先检查 include/asm/ioctl.h 和 Documentation/ioctl-number.txt. 这个头文件定义你将使用 的位段: type(魔数), 序号, 传输方向, 和参数大小. ioctl-number.txt 文件列举了在 内核中使用的魔数,[20]20 因此你将可选择你自己的魔数并且避免交叠. 这个文本文件也列 举了为什么应当使用惯例的原因.
定义 ioctl 命令号的正确方法使用 4 个位段, 它们有下列的含义. 这个列表中介绍的新 符号定义在 <linux/ioctl.h>.
但是, 这个文件的维护在后来有些少见了.
魔数. 只是选择一个数(在参考了 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 成员. 我们 很快见到如何使用这个成员.
头文件 <asm/ioctl.h>, 它包含在 <linux/ioctl.h> 中, 定义宏来帮助建立命令号, 如 下: _IO(type,nr)(给没有参数的命令), _IOR(type, nre, datatype)(给从驱动中读数据 的), _IOW(type,nr,datatype)(给写数据), 和 _IOWR(type,nr,datatype)(给双向传送). type 和 number 成员作为参数被传递, 并且 size 成员通过应用 sizeof 到 datatype 参数而得到.
这个头文件还定义宏, 可被用在你的驱动中来解码这个号: _IOC_DIR(nr),
_IOC_TYPE(nr), _IOC_NR(nr), 和 _IOC_SIZE(nr). 我们不进入任何这些宏的细节, 因为 头文件是清楚的, 并且在本节稍后有例子代码展示.
这里是一些 ioctl 命令如何在 scull 被定义的. 特别地, 这些命令设置和获得驱动的可 配置参数.
/* 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 真正的源文件定义几个额外的这里没有出现的命令.
我们选择实现 2 种方法传递整数参数: 通过指针和通过明确的值(尽管, 由于一个已存在 的惯例, ioclt 应当通过指针交换值). 类似地, 2 种方法被用来返回一个整数值:通过指 针和通过设置返回值. 这个有效只要返回值是一个正的整数; 如同你现在所知道的, 在从 任何系统调用返回时, 一个正值被保留(如同我们在 read 和 write 中见到的), 而一个 负值被看作一个错误并且被用来在用户空间设置 errno.[21] 21
"exchange"和"shift"操作对于 scull 没有特别的用处. 我们实现"exchange"来显示驱动 如何结合独立的操作到单个的原子的操作, 并且"shift"来连接"tell"和"query". 有时需 要象这样的原子的测试-和-设置操作, 特别地, 当应用程序需要设置和释放锁.
命令的明确的序号没有特别的含义. 它只用来区分命令. 实际上, 你甚至可使用相同的序 号给一个读命令和一个写命令, 因为实际的 ioctl 号在"方向"位是不同的, 但是你没有 理由这样做. 我们选择在任何地方不使用命令的序号除了声明中, 因此我们不分配一个返 回值给它. 这就是为什么明确的号出现在之前给定的定义中. 这个例子展示了一个使用命 令号的方法, 但是你有自由不这样做.
除了少数几个预定义的命令(马上就讨论), ioctl 的 cmd 参数的值当前不被内核使用, 并且在将来也很不可能. 因此, 你可以, 如果你觉得懒, 避免前面展示的复杂的声明并明 确声明一组调整数字. 另一方面, 如果你做了, 你不会从使用这些位段中获益, 并且你会 遇到困难如果你曾提交你的代码来包含在主线内核中. 头文件<linux/kd.h> 是这个老式 方法的例子, 使用 16-位的调整值来定义 ioctl 命令. 那个源代码依靠调整数因为使用 那个时候遵循的惯例, 不是由于懒惰. 现在改变它可能导致无理由的不兼容.