In continuation of the previous text 第6章:字符设备驱动的高级操作1:ioctl 系统调用, let's GO ahead.
Choosing the ioctl Commands
Before writing the code for ioctl , you need to choose the numbers that correspond to commands. The first instinct of many programmers is to choose a set of small numbers starting with 0 or 1 and going up from there. There are, however, good reasons for not doing things that way. The ioctl command numbers should be unique across the system in order to prevent errors caused by issuing the right command to the wrong device. Such a mismatch is not unlikely to happen, and a program might find itself trying to change the baud rate of a non-serial-port input stream, such as a FIFO or an audio device. If each ioctl number is unique, the application gets an EINVALerror rather than succeeding in doing something unintended.
在编写 ioctl 相关代码之前,你需要为各类命令分配对应的数值。很多程序员的第一反应是选择一组从 0 或 1 开始的连续小数字,但这样做存在明显的弊端。ioctl 命令号应在整个系统中保持唯一,以防止 "将正确的命令发送给错误的设备" 而引发错误。这种不匹配的情况并非不可能发生 ------ 例如,某个程序可能会试图修改一个非串口输入流(如 FIFO 或音频设备)的波特率。如果每个 ioctl 命令号都是唯一的,应用程序会收到 EINVAL 错误(无效参数),而不是成功执行非预期的操作。
To help programmers create unique ioctlcommand codes, these codes have been split up into several bitfields. The first versions of Linux used 16-bit numbers: the top eight were the "magic" numbers associated with the device, and the bottom eight were a sequential number, unique within the device. This happened because Linus was "clueless" (his own word); a better division of bitfields was conceived only later. Unfortunately, quite a few drivers still use the old convention. They have to: chang ing the command codes would break no end of binary programs, and that is not something the kernel developers are willing to do.
为了帮助程序员创建唯一的 ioctl 命令码,这些命令码被拆分为多个位域(bitfields)。早期的 Linux 版本使用 16 位命令号:高 8 位是与设备关联的 "幻数(magic number)",低 8 位是设备内唯一的连续序号。这种设计源于Linus自己所说的 "考虑不周(clueless)";更合理的位域划分是后来才被构思出来的。遗憾的是,仍有大量驱动沿用了这种旧约定 ------ 它们不得不这么做:修改命令码会大量破坏已有的二进制程序,而这是内核开发者不愿看到的结果。
To choose ioctl numbers for your driver according to the Linux kernel convention, you should first checkinclude/asm/ioctl.h and Documentation/ioctl-number.txt . The header defines the bitfields you will be using: type (magic number), ordinal number, direction of transfer, and size of argument. The ioctl-number.txt file lists the magic numbers used throughout the kernel,* so you'll be able to choose your own magic number and avoid overlaps. The text file also lists the reasons why the convention should be used.
The approved way to define ioctl command numbers uses four bitfields, which have the following meanings. New symbols introduced in this list are defined in <linux/ ioctl.h>.
若要按照 Linux 内核约定为你的驱动选择 ioctl 命令号,你应首先查看 include/asm/ioctl.h 和 Documentation/ioctl-number.txt 这两个文件:
-
头文件
include/asm/ioctl.h定义了你将使用的位域:类型(幻数)、序号、传输方向和参数大小; -
文档
Documentation/ioctl-number.txt列出了内核中所有已使用的幻数 *,你可以据此选择自己的幻数,避免冲突。该文档还阐述了遵循这一约定的原因。
按照内核规范,定义 ioctl 命令号的标准方式包含四个位域,各自含义如下(本部分引入的新符号均定义在 <linux/ioctl.h> 中):
type
The magic number. Just choose one number (after consulting ioctl-number.txt) and use it throughout the driver. This field is eight bits wide (_IOC_TYPEBITS).
number
The ordinal (sequential) number. It's eight bits (_IOC_NRBITS) wide.
direction
The direction of data transfer, if the particular command involves a data trans fer. The possible values are _IOC_NONE (no data transfer), _IOC_READ, _IOC_WRITE, and _IOC_READ|_IOC_WRITE (data is transferred both ways). Data transfer is seen from the application's point of view; _IOC_READ means reading from the device, so the driver must write to user space. Note that the field is a bit mask, so IOC READ and _IOC_WRITE can be extracted using a logical AND operation.
size
The size of user data involved. The width of this field is architecture dependent, but is usually 13 or 14 bits. You can find its value for your specific architecture in the macro _IOC_SIZEBITS. It's not mandatory that you use the size field---the kernel does not check it---but it is a good idea. Proper use of this field can help detect user-space programming errors and enable you to implement backward compatibility if you ever need to change the size of the relevant data item. If you need larger data structures, however, you can just ignore the size field. We'll see how this field is used soon.
type(类型,即幻数)
即幻数。只需选择一个数值(参考 ioctl-number.txt 后),并在整个驱动中统一使用。该位域宽度为 8 位(对应宏 _IOC_TYPEBITS)。
number(序号)
即连续序号。该位域宽度为 8 位(对应宏 _IOC_NRBITS)。
direction(传输方向)
若该命令涉及数据传输,此字段用于标识传输方向。可选值包括:
-
_IOC_NONE:无数据传输; -
_IOC_READ:从设备读取数据; -
_IOC_WRITE:向设备写入数据; -
_IOC_READ|_IOC_WRITE:双向数据传输。
数据传输方向是从应用程序的视角来看的 :_IOC_READ 表示应用程序从设备读取数据,因此驱动需要将数据写入用户态。请注意,该字段是一个位掩码(bit mask) ,可以通过逻辑与运算提取 _IOC_READ 和 _IOC_WRITE 标志。
size(参数大小)
即涉及的用户态数据大小。该位域的宽度与架构相关,通常为 13 或 14 位,你可以通过宏 _IOC_SIZEBITS 查看目标架构的具体值。此字段并非强制要求使用(内核不会对其进行校验),但使用它是一个良好的实践:合理利用该字段有助于检测用户态编程错误,且当你需要修改相关数据项的大小时,能够实现向后兼容。不过,若你需要使用更大的数据结构,也可以忽略该字段。我们很快就会看到该字段的具体用法。
The header file*<asm/ioctl.h>* , which is included by <linux/ioctl.h>, defines macros that help set up the command numbers as follows: _IO(type,nr) (for a command that has no argument), _IOR(type,nr,datatype) (for reading data from the driver), _IOW(type,nr,datatype) (for writing data), and _IOWR(type,nr,datatype) (for bidirectional transfers). The type and number fields are passed as arguments, and the size field is derived by applying sizeof to the datatype argument.
The header also defines macros that may be used in your driver to decode the num bers: _IOC_DIR(nr), _IOC_TYPE(nr), _IOC_NR(nr), and _IOC_SIZE(nr). We won't go into any more detail about these macros because the header file is clear, and sample code is shown later in this section.
头文件 <asm/ioctl.h>(被 <linux/ioctl.h> 包含)定义了一系列用于构造命令号的宏,如下所示:
-
_IO(type,nr):用于无参数的命令; -
_IOR(type,nr,datatype):用于从驱动读取数据的命令; -
_IOW(type,nr,datatype):用于向驱动写入数据的命令; -
_IOWR(type,nr,datatype):用于双向数据传输的命令。
其中,type 和 nr(序号)作为参数传入,size 字段则通过对 datatype 参数执行 sizeof 运算推导得到。
该头文件还定义了一系列可在驱动中用于解析命令号的宏:_IOC_DIR(nr)、_IOC_TYPE(nr)、_IOC_NR(nr) 和 _IOC_SIZE(nr)。我们不会对这些宏进行更深入的讲解,因为头文件中的注释已经很清晰,且本节后续会展示相关示例代码。
补充说明:
-
幻数选择的核心原则
幻数通常选择一个 ASCII 字符(取值范围
0x00-0xFF),且需在Documentation/ioctl-number.txt中未被占用,例如'S'代表 scull 驱动、'T'代表串口驱动。避免使用已经被内核核心驱动占用的幻数(如'A'用于音频、'B'用于块设备),防止命令号冲突。 -
构造宏的实际使用示例
以 scull 驱动为例,规范的命令号定义如下:
1.cpp#include <linux/ioctl.h> // 幻数选择 'S',未被占用 #define SCULL_MAGIC 'S' // 无参数命令:重置 scull 设备 #define SCULL_IOCTL_RESET _IO(SCULL_MAGIC, 0) // 从驱动读取数据:获取设备存储大小(用户态传入 int 指针接收结果) #define SCULL_IOCTL_GET_CAPACITY _IOR(SCULL_MAGIC, 1, int) // 向驱动写入数据:设置设备存储大小(用户态传入 int 数值) #define SCULL_IOCTL_SET_CAPACITY _IOW(SCULL_MAGIC, 2, int) // 双向传输:交换数据(用户态传入/传出 struct scull_data 结构体) #define SCULL_IOCTL_SWAP_DATA _IOWR(SCULL_MAGIC, 3, struct scull_data) -
size 字段的实际价值
内核虽不校验 size 字段,但驱动可以通过
_IOC_SIZE(cmd)提取参数大小,与实际传入的数据大小对比,提前发现用户态的错误。例如:
1.cpp// 检测参数大小是否匹配 if (_IOC_SIZE(cmd) != sizeof(int)) { return -EINVAL; // 参数大小不匹配,返回无效参数错误 }这在后续修改数据结构大小时,能有效区分 "旧版用户态程序" 和 "新版用户态程序",实现平滑兼容。
-
旧版 16 位命令号的局限性
早期 16 位命令号(高 8 位幻数 + 低 8 位序号)没有传输方向和参数大小字段,无法区分数据传输方向,也无法检测参数错误,容易引发兼容性问题。现代驱动必须使用新版 4 位域命令号,仅在维护遗留驱动时才兼容旧版。
-
命令号的唯一性保障
新版 4 位域命令号的总宽度通常为 32 位,通过 "幻数 + 序号 + 传输方向 + 参数大小" 的组合,大幅降低了命令号冲突的概率。即使两个驱动使用了相同的幻数和序号,只要传输方向或参数大小不同,命令号也会不同,从而避免误操作。