In continuation of the previous text 第6章:字符设备驱动的高级操作2:Choosing the ioctl Commands, let's GO ahead.
Here is how some ioctlcommands are defined in scull. In particular, these com mands set and get the driver's configurable parameters.
下面展示 scull 驱动中部分 ioctl 命令的定义方式,这些命令主要用于设置和获取驱动的可配置参数。
cpp
/* 选择 'k' 作为幻数 */
#define SCULL_IOC_MAGIC 'k'
/* 建议你在自己的代码中使用不同的 8 位数值 */
#define SCULL_IOCRESET _IO(SCULL_IOC_MAGIC, 0)
/*
* S 表示 "Set"(设置):通过指针传递参数
* T 表示 "Tell"(告知):直接通过参数值传递(无需指针)
* G 表示 "Get"(获取):通过指针返回结果给用户态
* Q 表示 "Query"(查询):通过函数返回值直接返回结果
* X 表示 "eXchange"(交换):原子性地组合 "Get" 和 "Set" 操作
* H 表示 "sHift"(切换):原子性地组合 "Tell" 和 "Query" 操作
*/
#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
The actual source file defines a few extra commands that have not been shown here.
We chose to implement both ways of passing integer arguments: by pointer and by explicit value (although, by an established convention, ioctlshould exchange values by pointer). Similarly, both ways are used to return an integer number: by pointer or by setting the return value. This works as long as the return value is a positive inte ger; as you know by now, on return from any system call, a positive value is pre served (as we saw for read and write), while a negative value is considered an error and is used to set errno in user space.
实际的源文件中还定义了一些额外的命令,此处未展示。
我们选择同时实现了两种整数参数传递方式:指针传递 和显式值传递 (尽管按照既定约定,ioctl 应通过指针交换数据)。同样,返回整数值也支持两种方式:通过指针返回,或通过函数返回值直接返回。需要注意的是,这种方式仅在返回值为正整数 时有效 ------ 正如你目前所知,任何系统调用返回时,正数值会被保留(我们在 read 和 write 系统调用中已经见过这种行为),而负数值会被视为错误码,并用于设置用户态的 errno 变量。
The "exchange" and "shift" operations are not particularly useful for scull.We implemented "exchange" to show how the driver can combine separate operations into a single atomic one, and "shift" to pair "tell" and "query." There are times when atomic test-and-set operations like these are needed, in particular, when applica tions need to set or release locks.
"交换(exchange)" 和 "切换(shift)" 操作对于 scull 驱动而言并非特别有用。我们实现 "交换" 操作是为了展示驱动如何将多个独立操作组合成一个原子操作,而实现 "切换" 操作是为了将 "告知(tell)" 和 "查询(query)" 操作配对。在某些场景下,这类原子性的 "测试 - 设置" 操作是必需的,尤其是当应用程序需要设置或释放锁的时候。
The explicit ordinal number of the command has no specific meaning. It is used only to tell the commands apart. Actually, you could even use the same ordinal number for a read command and a write command, since the actual ioctl number is different in the "direction" bits, but there is no reason why you would want to do so. We chose not to use the ordinal number of the command anywhere but in the declara tion, so we didn't assign a symbolic value to it. That's why explicit numbers appear in the definition given previously. The example shows one way to use the command numbers, but you are free to do it differently.
命令的显式序号本身没有特定含义,仅用于区分不同的命令。实际上,你甚至可以为读命令和写命令使用相同的序号 ------ 因为实际的 ioctl 命令号在 "方向" 位上存在差异,二者仍会被区分开,但没有任何理由要这么做。我们选择仅在声明中使用命令的序号,而不为其分配符号值,这也是前面的定义中直接出现显式数字的原因。这个示例展示了命令号的一种使用方式,你也可以根据自己的需求采用不同的方式。
With the exception of a small number of predefined commands (to be discussed shortly), the value of the ioctl cmd argument is not currently used by the kernel, and it's quite unlikely it will be in the future. Therefore, you could, if you were feeling lazy, avoid the complex declarations shown earlier and explicitly declare a set of sca lar numbers. On the other hand, if you did, you wouldn't benefit from using the bit fields, and you would encounter difficulties if you ever submitted your code for inclusion in the mainline kernel. The header<linux/kd.h> is an example of this old fashioned approach, using 16-bit scalar values to define the ioctl commands. That source file relied on scalar numbers because it used the conventions obeyed at that time, not out of laziness. Changing it now would cause gratuitous incompatibility.
除了少数预定义命令(我们稍后会讨论),内核目前并不使用 ioctl 的 cmd 参数值,而且在未来也极不可能使用。因此,如果你比较偷懒,可以避开前面展示的复杂声明,直接显式声明一组标量数字作为命令号。但另一方面,这样做你将无法受益于位域带来的优势,而且如果将来你想将代码提交到主线内核,会遇到诸多困难。<linux/kd.h> 头文件就是这种老式方法的示例 ------ 它使用 16 位标量值来定义 ioctl 命令。该源文件使用标量数字并非出于偷懒,而是遵循了当时的约定;现在修改它会造成不必要的兼容性问题。
补充说明:
-
命令前缀(S/T/G/Q/X/H)的核心含义与适用场景
前缀 含义 数据传递方式 典型用途 S(Set) 设置 用户态 → 内核态(指针传递) 向驱动设置复杂配置(如结构体参数) T(Tell) 告知 用户态 → 内核态(直接传值) 向驱动传递简单整数值(如开关标志) G(Get) 获取 内核态 → 用户态(指针返回) 从驱动读取复杂数据(如设备状态结构体) Q(Query) 查询 内核态 → 用户态(返回值传递) 从驱动读取简单正整数值(如设备计数) X(eXchange) 交换 双向传递(原子操作) 原子性地 "读取旧值 + 设置新值"(如锁的状态切换) H(sHift) 切换 双向传递(原子操作) 原子性地 "告知新值 + 查询旧值"(如参数的原子更新) -
内核推荐 "指针传递" 的原因
尽管 scull 驱动同时实现了两种传递方式,但内核更推荐使用指针传递,原因如下:
-
错误处理:指针传递可以通过
copy_from_user()/copy_to_user()检测用户态指针的合法性,而值传递无法提前发现无效参数。 -
可扩展性:后续需要扩大参数规模(如从
int改为结构体)时,无需修改命令号的定义,仅需调整指针指向的数据类型,保持向后兼容; -
统一性:无论参数是简单整数还是复杂结构体,都可以用相同的方式处理,无需区分值传递和指针传递;
-
-
原子操作(X/H)的实际价值
这两种操作的核心价值是 "原子性",避免多线程 / 多进程并发操作时出现竞态。例如,
SCULL_IOCXQUANTUM可以原子性地获取当前quantum值并设置新值,无需额外加锁,适用于 "读 - 改 - 写" 的场景(如计数器的原子更新)。 -
SCULL_IOC_MAXNR的作用该宏用于定义当前驱动支持的最大命令序号,主要用于驱动中做参数合法性检查,避免处理超出驱动支持范围的命令号,示例如下:
1.cpp// 检查命令序号是否超出范围 if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) { return -ENOTTY; // 设备不支持该命令 } -
老式标量命令号的局限性
<linux/kd.h>采用的 16 位标量命令号,缺乏方向位和参数大小位的信息,无法检测用户态的参数错误,也难以避免命令号冲突。现代驱动必须使用新版位域命令号,仅在维护遗留驱动时才兼容老式标量定义。 -
幻数与序号的最佳实践
-
幻数优先选择未被占用的 ASCII 字符,参考
Documentation/ioctl-number.txt; -
序号建议按功能分组连续分配(如设置操作从 1 开始,获取操作从 5 开始),提高代码可读性;
-
避免为不同功能的命令使用相同序号,即使方向位不同,也会增加代码调试的难度。
-