In continuation of the previous text 第6章:字符设备驱动的高级操作6:Capabilities and Restricted Operations let’s GO ahead.
The Implementation of the ioctl Commands
The scull implementation of ioctl only transfers the configurable parameters of the device and turns out to be as easy as the following:
scull 驱动的 ioctl 实现仅用于传输设备的可配置参数,实现起来非常简单,代码如下:
switch(cmd) { case SCULL_IOCRESET: scull_quantum = SCULL_QUANTUM; scull_qset = SCULL_QSET; break; case SCULL_IOCSQUANTUM: /* Set: arg 指向数值 */ if (! capable (CAP_SYS_ADMIN)) return -EPERM; retval = __get_user(scull_quantum, (int __user *)arg); break; case SCULL_IOCTQUANTUM: /* Tell: arg 就是数值 */ if (! capable (CAP_SYS_ADMIN)) return -EPERM; scull_quantum = arg; break; case SCULL_IOCGQUANTUM: /* Get: arg 指向存放结果的指针 */ retval = __put_user(scull_quantum, (int __user *)arg); break; case SCULL_IOCQQUANTUM: /* Query: 返回它(正数) */ return scull_quantum; case SCULL_IOCXQUANTUM: /* eXchange: 使用 arg 作为指针 */ if (! capable (CAP_SYS_ADMIN)) return -EPERM; tmp = scull_quantum; retval = __get_user(scull_quantum, (int __user *)arg); if (retval == 0) retval = __put_user(tmp, (int __user *)arg); break; case SCULL_IOCHQUANTUM: /* sHift: 类似于 Tell + Query */ if (! capable (CAP_SYS_ADMIN)) return -EPERM; tmp = scull_quantum; scull_quantum = arg; return tmp; default: /* 冗余检查,因为 cmd 已通过 MAXNR 校验 */ return -ENOTTY; } return retval;scull also includes six entries that act on scull_qset. These entries are identical to the ones for scull_quantum and are not worth showing in print. The six ways to pass and receive arguments look like the following from the caller’s point of view (i.e., from user space):
scull 驱动还包含六个操作 scull_qset 的入口。这些入口与操作 scull_quantum 的代码完全相同,因此不再赘述。
从调用者(即用户空间)的角度来看,六种传递和接收参数的方式如下所示:
int quantum; ioctl(fd, SCULL_IOCSQUANTUM, &quantum); /* 通过指针设置 */ ioctl(fd, SCULL_IOCTQUANTUM, quantum); /* 通过值设置 */ ioctl(fd, SCULL_IOCGQUANTUM, &quantum); /* 通过指针获取 */ quantum = ioctl(fd, SCULL_IOCQQUANTUM); /* 通过返回值获取 */ ioctl(fd, SCULL_IOCXQUANTUM, &quantum); /* 通过指针交换 */ quantum = ioctl(fd, SCULL_IOCHQUANTUM, quantum); /* 通过值交换 */Of course, a normal driver would not implement such a mix of calling modes. We have done so here only to demonstrate the different ways in which things could be done. Normally, however, data exchanges would be consistently performed, either through pointers or by value, and mixing of the two techniques would be avoided.
当然,一个正常的驱动程序不会实现如此混合的调用模式。我们在这里这样做只是为了演示可以采用的各种不同方式。然而在实际开发中,数据交换应保持一致,要么统一通过指针,要么统一通过值,应避免两种方式混用。
补充说明:
1. 代码核心逻辑梳理
这段代码是scull 驱动 ioctl 的核心处理函数,根据不同的命令 cmd 执行不同操作:
- 重置设备:将量子大小和量子集大小恢复为默认值。
- 设置参数:分为指针传参(S)和直接传值(T),必须检查管理员权限。
- 获取参数:分为指针回写(G)和直接返回(Q),无需权限。
- 原子交换:先保存旧值,读取新值,再写回旧值,实现原子操作。
2. 六种命令模式深度解析
驱动实现了6 种参数交互方式,对应前缀含义:
- S (Set):用户传指针,驱动从用户空间读取值。
- T (Tell):用户直接传整数,驱动直接使用 arg。
- G (Get):驱动将值写入用户提供的指针。
- Q (Query):驱动直接将结果作为返回值返回。
- X (eXchange):原子交换,用户传指针,传入新值、取回旧值。
- H (sHift):原子切换,用户传值,设置新值、返回旧值。
3. 权限控制关键点
- 所有 “设置 / 修改” 操作:必须检查
CAP_SYS_ADMIN权限。 - 所有 “读取 / 查询” 操作:不检查权限,任何用户均可查询。
- 无权限时返回-EPERM,表示 “操作不允许”。
4. 内核函数解释
- __get_user:从用户空间读取一个整数(已提前校验地址,高效版)。
- __put_user:向用户空间写入一个整数(已提前校验地址,高效版)。
- capable():检查当前进程是否拥有指定特权能力。
5. 为什么实际驱动不要混合模式?
原文明确指出:真实驱动不应混合多种传参方式。 原因:
- 容易造成用户态程序出错、传参错误。
- 降低代码可读性、可维护性。
- 容易引发安全漏洞。
标准推荐做法:
- 所有命令统一使用指针传参(最安全、最通用)。
- 或统一使用返回值获取结果。
6. 错误码总结
- -EPERM:权限不足。
- -EFAULT:用户空间地址非法(由 __get_user/__put_user 返回)。
- -ENOTTY:不支持该 ioctl 命令。
7. 原子交换(X / H 命令)的意义
这两个命令实现了原子的 “读 - 改 - 写”:
- 旧值保存、新值写入、旧值返回,全程不可被打断。
- 适用于多线程并发修改配置的场景。
- 无需额外加锁,提高效率。