📄 ch06.html
字号:
* 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</pre><p>真正的源文件定义几个额外的这里没有出现的命令.</p><p>我们选择实现 2 种方法传递整数参数: 通过指针和通过明确的值(尽管, 由于一个已存在的惯例, ioclt 应当通过指针交换值). 类似地, 2 种方法被用来返回一个整数值:通过指针和通过设置返回值. 这个有效只要返回值是一个正的整数; 如同你现在所知道的, 在从任何系统调用返回时, 一个正值被保留(如同我们在 read 和 write 中见到的), 而一个负值被看作一个错误并且被用来在用户空间设置 errno.<sup>[<a name="id433917" href="#ftn.id433917">21</a>]</sup></p><p>"exchange"和"shift"操作对于 scull 没有特别的用处. 我们实现"exchange"来显示驱动如何结合独立的操作到单个的原子的操作, 并且"shift"来连接"tell"和"query". 有时需要象这样的原子的测试-和-设置操作, 特别地, 当应用程序需要设置和释放锁.</p><p>命令的明确的序号没有特别的含义. 它只用来区分命令. 实际上, 你甚至可使用相同的序号给一个读命令和一个写命令, 因为实际的 ioctl 号在"方向"位是不同的, 但是你没有理由这样做. 我们选择在任何地方不使用命令的序号除了声明中, 因此我们不分配一个返回值给它. 这就是为什么明确的号出现在之前给定的定义中. 这个例子展示了一个使用命令号的方法, 但是你有自由不这样做.</p><p>除了少数几个预定义的命令(马上就讨论), ioctl 的 cmd 参数的值当前不被内核使用, 并且在将来也很不可能. 因此, 你可以, 如果你觉得懒, 避免前面展示的复杂的声明并明确声明一组调整数字. 另一方面, 如果你做了, 你不会从使用这些位段中获益, 并且你会遇到困难如果你曾提交你的代码来包含在主线内核中. 头文件<linux/kd.h> 是这个老式方法的例子, 使用 16-位的调整值来定义 ioctl 命令. 那个源代码依靠调整数因为使用那个时候遵循的惯例, 不是由于懒惰. 现在改变它可能导致无理由的不兼容.</p></div><div class="sect2" lang="zh-cn"><div class="titlepage"><div><div><h3 class="title"><a name="TheReturnValue.sect2"></a>6.1.2. 返回值</h3></div></div></div><p>ioctl 的实现常常是一个 switch 语句, 基于命令号. 但是当命令号没有匹配一个有效的操作时缺省的选择应当是什么? 这个问题是有争议的. 几个内核函数返回 -ENIVAL("Invalid argument"), 它有意义是因为命令参数确实不是一个有效的. POSIX 标准, 但是, 说如果一个不合适的 ioctl 命令被发出, 那么 -ENOTTY 应当被返回. 这个错误码被 C 库解释为"设备的不适当的 ioctl", 这常常正是程序员需要听到的. 然而, 它仍然是相当普遍的来返回 -EINVAL, 对于响应一个无效的 ioctl 命令.</p></div><div class="sect2" lang="zh-cn"><div class="titlepage"><div><div><h3 class="title"><a name="ThePredefinedCommands.sect2"></a>6.1.3. 预定义的命令</h3></div></div></div><p>尽管 ioctl 系统调用最常用来作用于设备, 内核能识别几个命令. 注意这些命令, 当用到你的设备时, 在你自己的文件操作被调用之前被解码. 因此, 如果你选择相同的号给一个你的 ioctl命令, 你不会看到任何的给那个命令的请求, 并且应用程序获得某些不期望的东西, 因为在 ioctl 号之间的冲突.</p><p>预定义命令分为 3 类:</p><div class="itemizedlist"><ul type="disc"><li><p>可对任何文件发出的(常规, 设备, FIFO, 或者 socket) 的那些.</p></li><li><p>只对常规文件发出的那些.</p></li><li><p>对文件系统类型特殊的那些.</p></li></ul></div><p>最后一类的命令由宿主文件系统的实现来执行(这是 chattr 命令如何工作的). 设备驱动编写者只对第一类命令感兴趣, 它们的魔数是 "T". 查看其他类的工作留给读者作为练习; ext2_ioctl 是最有趣的函数(并且比预期的要容易理解), 因为它实现 append-only 标志和 immutable 标志.</p><p>下列 ioctl 命令是预定义给任何文件, 包括设备特殊的文件:</p><div class="variablelist"><dl><dt><span class="term"><span>FIOCLEX </span></span></dt><dd><p>设置 close-on-exec 标志(File IOctl Close on EXec). 设置这个标志使文件描述符被关闭, 当调用进程执行一个新程序时.</p></dd><dt><span class="term"><span>FIONCLEX </span></span></dt><dd><p>清除 close-no-exec 标志(File IOctl Not CLose on EXec). 这个命令恢复普通文件行为, 复原上面 FIOCLEX 所做的. FIOASYNC 为这个文件设置或者复位异步通知(如同在本章中"异步通知"一节中讨论的). 注意直到 Linux 2.2.4 版本的内核不正确地使用这个命令来修改 O_SYNC 标志. 因为两个动作都可通过 fcntl 来完成, 没有人真正使用 FIOASYNC 命令, 它在这里出现只是为了完整性.</p></dd><dt><span class="term"><span>FIOQSIZE </span></span></dt><dd><p>这个命令返回一个文件或者目录的大小; 当用作一个设备文件, 但是, 它返回一个 ENOTTY 错误.</p></dd><dt><span class="term"><span>FIONBIO</span></span></dt><dd><p>"File IOctl Non-Blocking I/O"(在"阻塞和非阻塞操作"一节中描述). 这个调用修改在 filp->f_flags 中的 O_NONBLOCK 标志. 给这个系统调用的第 3 个参数用作指示是否这个标志被置位或者清除. (我们将在本章看到这个标志的角色). 注意常用的改变这个标志的方法是使用 fcntl 系统调用, 使用 F_SETFL 命令.</p></dd></dl></div><p>列表中的最后一项介绍了一个新的系统调用, fcntl, 它看来象 ioctl. 事实上, fcntl 调用非常类似 ioctl, 它也是获得一个命令参数和一个额外的(可选地)参数. 它保持和 ioctl 独立主要是因为历史原因: 当 Unix 开发者面对控制 I/O 操作的问题时, 他们决定文件和设备是不同的. 那时, 有 ioctl 实现的唯一设备是 ttys, 它解释了为什么 -ENOTTY 是标准的对不正确 ioctl 命令的回答. 事情已经改变, 但是 fcntl 保留为一个独立的系统调用.</p></div><div class="sect2" lang="zh-cn"><div class="titlepage"><div><div><h3 class="title"><a name="UsingtheioctlArgument.sect2"></a>6.1.4. 使用 ioctl 参数</h3></div></div></div><p>在看 scull 驱动的 ioctl 代码之前, 我们需要涉及的另一点是如何使用这个额外的参数. 如果它是一个整数, 就容易: 它可以直接使用. 如果它是一个指针, 但是, 必须小心些.</p><p>当用一个指针引用用户空间, 我们必须确保用户地址是有效的. 试图存取一个没验证过的用户提供的指针可能导致不正确的行为, 一个内核 oops, 系统崩溃, 或者安全问题. 它是驱动的责任来对每个它使用的用户空间地址进行正确的检查, 并且返回一个错误如果它是无效的.</p><p>在第 3 章, 我们看了 copy_from_user 和 copy_to_user 函数, 它们可用来安全地移动数据到和从用户空间. 这些函数也可用在 ioctl 方法中, 但是 ioctl 调用常常包含小数据项, 可通过其他方法更有效地操作. 开始, 地址校验(不传送数据)由函数 access_ok 实现, 它定义在 <asm/uaccess.h>:</p><pre class="programlisting">int access_ok(int type, const void *addr, unsigned long size); </pre><p>第一个参数应当是 VERIFY_READ 或者 VERIFY_WRITE, 依据这个要进行的动作是否是读用户空间内存区或者写它. addr 参数持有一个用户空间地址, size 是一个字节量. 例如, 如果 ioctl 需要从用户空间读一个整数, size 是 sizeof(int). 如果你需要读和写给定地址, 使用 VERIFY_WRITE, 因为它是 VERIRY_READ 的超集.</p><p>不象大部分的内核函数, access_ok 返回一个布尔值: 1 是成功(存取没问题)和 0 是失败(存取有问题). 如果它返回假, 驱动应当返回 -EFAULT 给调用者.</p><p>关于 access_ok有多个有趣的东西要注意. 首先, 它不做校验内存存取的完整工作; 它只检查看这个内存引用是在这个进程有合理权限的内存范围中. 特别地, access_ok 确保这个地址不指向内核空间内存. 第2, 大部分驱动代码不需要真正调用 access_ok. 后面描述的内存存取函数为你负责这个. 但是, 我们来演示它的使用, 以便你可见到它如何完成.</p><p>scull 源码利用了 ioclt 号中的位段来检查参数, 在 switch 之前:</p><pre class="programlisting">int err = 0, tmp;int retval = 0;/* * extract the type and number bitfields, and don't decode * wrong cmds: return ENOTTY (inappropriate ioctl) before access_ok() */if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY;if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) return -ENOTTY;/* * the direction is a bitmask, and VERIFY_WRITE catches R/W * transfers. `Type' is user-oriented, while * access_ok is kernel-oriented, so the concept of "read" and * "write" is reversed */if (_IOC_DIR(cmd) & _IOC_READ) err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));else if (_IOC_DIR(cmd) & _IOC_WRITE) err = !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));if (err) return -EFAULT;</pre><p>在调用 access_ok 之后, 驱动可安全地进行真正的传输. 加上 copy_from_user 和 copy_to_user_ 函数, 程序员可利用一组为被最多使用的数据大小(1, 2, 4, 和 8 字节)而优化过的函数. 这些函数在下面列表中描述, 它们定义在 <asm/uaccess.h>:</p><div class="variablelist"><dl><dt><span class="term"><span>put_user(datum, ptr)</span></span></dt><dd></dd><dt><span class="term"><span>__put_user(datum, ptr)</span></span></dt><dd><p>这些宏定义写 datum 到用户空间; 它们相对快, 并且应当被调用来代替 copy_to_user 无论何时要传送单个值时. 这些宏已被编写来允许传递任何类型的指针到 put_user, 只要它是一个用户空间地址. 传送的数据大小依赖 prt 参数的类型, 并且在编译时使用 sizeof 和 typeof 等编译器内建宏确定. 结果是, 如果 prt 是一个 char 指针, 传送一个字节, 以及对于 2, 4, 和 可能的 8 字节.</p><p>put_user 检查来确保这个进程能够写入给定的内存地址. 它在成功时返回 0, 并且在错误时返回 -EFAULT. __put_user 进行更少的检查(它不调用 access_ok), 但是仍然能够失败如果被指向的内存对用户是不可写的. 因此, __put_user 应当只用在内存区已经用 access_ok 检查过的时候.</p><p>作为一个通用的规则, 当你实现一个 read 方法时, 调用 __put_user 来节省几个周期, 或者当你拷贝几个项时, 因此, 在第一次数据传送之前调用 access_ok 一次, 如同上面 ioctl 所示.</p></dd><dt><span class="term"><span>get_user(local, ptr)</span></span></dt><dd></dd><dt><span class="term"><span>__get_user(local, ptr)</span></span></dt><dd><p>这些宏定义用来从用户空间接收单个数据. 它们象 put_user 和 __put_user, 但是在相反方向传递数据. 获取的值存储于本地变量 local; 返回值指出这个操作是否成功. 再次, __get_user 应当只用在已经使用 access_ok 校验过的地址.</p></dd></dl></div><p>如果做一个尝试来使用一个列出的函数来传送一个不适合特定大小的值, 结果常常是一个来自编译器的奇怪消息, 例如"coversion to non-scalar type requested". 在这些情况中, 必须使用 copy_to_user 或者 copy_from_user.</p></div><div class="sect2" lang="zh-cn"><div class="titlepage"><div><div><h3 class="title"><a name="CapabilitiesandRestrictedOperations.sect2"></a>6.1.5. 兼容性和受限操作</h3></div></div></div><p>存取一个设备由设备文件上的许可权控制, 并且驱动正常地不涉及到许可权的检查. 但是, 有些情形, 在保证给任何用户对设备的读写许可的地方, 一些控制操作仍然应当被拒绝. 例如, 不是所有的磁带驱动器的用户都应当能够设置它的缺省块大小, 并且一个已经被给予对一个磁盘设备读写权限的用户应当仍然可能被拒绝来格式化它. 在这样的情况下, 驱动必须进行额外的检查来确保用户能够进行被请求的操作.</p><p>传统上 unix 系统对超级用户帐户限制了特权操作. 这意味着特权是一个全有-或-全无的东西 -- 超级用户可能任意做任何事情, 但是所有其他的用户被高度限制了. Linux 内核提供了一个更加灵活的系统, 称为能力. 一个基于能力的系统丢弃了全有-或全无模式, 并且打破特权操作为独立的子类. 这种方式, 一个特殊的用户(或者是程序)可被授权来进行一个特定的特权操作而不必泄漏进行其他的, 无关的操作的能力. 内核在许可权管理上排他地使用能力, 并且输出 2 个系统调用 capget 和 capset, 来允许它们被从用户空间管理.</p><p>全部能力可在 <linux/capability.h> 中找到. 这些是对系统唯一可用的能力; 对于驱动作者或者系统管理员, 不可能不修改内核源码而来定义新的. 设备驱动编写者可能感兴趣的这些能力的一个子集, 包括下面:</p><div class="variablelist"><dl><dt><span class="term">CAP_DAC_OVERRIDE <span></span></span></dt><dd><p>这个能力来推翻在文件和目录上的存取的限制(数据存取控制, 或者 DAC).</p></dd><dt><span class="term">CAP_NET_ADMIN <span></span></span></dt><dd><p>进行网络管理任务的能力, 包括那些能够影响网络接口的.</p></dd><dt><span class="term">CAP_SYS_MODULE <span></span></span></dt><dd><p>加载或去除内核模块的能力.</p></dd><dt><span class="term">CAP_SYS_RAWIO <span></span></span></dt><dd><p>进行 "raw" I/O 操作的能力. 例子包括存取设备端口或者直接和 USB 设备通讯.</p></dd><dt><span class="term">CAP_SYS_ADMIN <span></span></span></dt><dd><p>一个捕获-全部的能力, 提供对许多系统管理操作的存取.</p></dd>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -