-
10:32-11:40 20p
第6章 高级字符驱动操作
ioctl 接口
在用户空间, ioctl 系统调用有下面的原型:
int ioctl(int fd, unsigned long cmd, ...);
原型中的点并不表示可变参数, 而是一个单个可选的参数, 传统上标识为 char *argp. 这些点在那里只是为了阻止在编译时的类型检查.
第三个参数的类型,取决于第二个参数-命令。有的命令,不需要额外参数,有的需要整形,有的命令需要指针作为参数等等。
【编程技巧】
在内核空间,ioctl 驱动方法的原型:
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
用户空间的可选参数无论是整形还是指针,以一个unsigned long的形式传递。
【编程技巧】
选择ioctl命令
ioctl 命令数字应当在这个系统是唯一的, 这是为了阻止向错误的设备发出正确的命令而引起的错误.
【如果ioctl 命令数字不是唯一的,假设打印机 打印的命令字是0, 炸弹启动的命令字也是0. 如果你想打印,结果不小心就炸了。所以每个设备的命令字数字应该是系统内唯一的。】
定义 ioctl 命令号的正确方法使用 4 个位段, 它们有下列的含义. 这个列表中介绍的新符号定义在 <linux/ioctl.h>.
返回值
按惯例返回 -ENOTTY, 因为这个错误码被 C 库解释为"设备的不适当的 ioctl", 这常常正是程序员需要听到的。但是 -ENIVAL 也常见。
预定义的命令
预定义命令分为 3 类:
- 可对任何文件发出的(常规, 设备, FIFO, 或者 socket) 的那些.
- 只对常规文件发出的那些.
- 对文件系统类型特殊的那些.
设备驱动编写者只对第一类命令感兴趣, 它们的魔数是 "T".
使用ioctl参数
如果它是一个整数, 这很容易: 它可以直接使用.
如果它是一个指针, 必须小心些.
当用一个指针引用用户空间, 我们必须确保用户地址是有效的. 试图存取一个没验证过的用户提供的指针可能导致不正确的行为, 一个内核 oops, 系统崩溃, 或者安全问题. 这是驱动的责任来对每个它使用的用户空间地址进行正确的检查, 并且返回一个错误如果它是无效的.
copy_from_user 和 copy_to_user 函数, 它们可用来安全地移动数据到和从用户空间. 这些函数也可用在 ioctl 方法中。
ioctl 调用常常包含小数据项, 更有效地操作有:
地址校验(不传送数据)
由函数 access_ok 实现, 它定义在 <asm/uaccess.h>:
int access_ok(int type, const void *addr, unsigned long size);
传输数据
put_user() , get_user()
兼容性和受限制操作
在进行一个特权操作之前, 一个设备驱动应当检查调用进程有合适的能力; 不这样做可能导致用户进程进行非法的操作, 对系统的稳定和安全有坏的后果. 能力检查是通过 capable 函数来进行的(定义在 <linux/sched.h>):
int capable(int capability);
ioctl 命令的实现
不用 ioctl 的设备控制
有时控制设备最好是通过写控制序列到设备自身来实现. 例如, 这个技术用在控制台驱动中, 这里所谓的 escape 序列被用来移动光标, 改变缺省的颜色, 或者进行其他的配置任务. 这样实现设备控制的好处是用户可仅仅通过写数据控制设备, 不必使用(或者有时候写)只为配置设备而建立的程序. 当设备可这样来控制, 发出命令的程序甚至常常不需要运行在和它要控制的设备所在的同一个系统上.
【使用ioctl来控制设备, 需要用户自己编写一个程序来调用 ioctl 接口。而写控制序列则不用】
直接设备控制的好处是你可以使用 cat 来移动摄像机, 而不必写和编译特殊的代码来发出 ioctl 调用.
阻塞I/O
一个重要的问题:一个驱动当它无法立刻满足请求应当如何响应?
在这样的情形中, 你的驱动应当(缺省地)阻塞进程, 使它进入睡眠直到请求可继续.
休眠的介绍
对于一个进程"休眠"意味着什么? 当一个进程被置为休眠, 它被标识为处于一个特殊的状态并且从调度器的运行队列中去除. 直到发生某些事情改变了那个状态, 这个进程将不被在任何 CPU 上调度, 并且, 因此, 将不会运行. 一个睡着的进程已被搁置到系统的一边, 等待以后发生事件.
对于一个 Linux 驱动使一个进程休眠是一个容易做的事情. 但是, 有几个规则必须记住以安全的方式编码休眠.
- 当你运行在原子上下文时不能睡眠
- 持有一个自旋锁, seqlock, 或者 RCU 锁时不能睡眠
- 已关闭中断你也不能睡眠
拥有信号量时休眠,是合法的。 如果代码在持有一个信号量时休眠, 任何其他的等待这个信号量的线程也将休眠. 因此发生在持有信号量时的任何休眠应当短暂, 并且你应当说服自己, 由于持有这个信号量, 你不能阻塞那个将最终唤醒你的进程.
另一件要记住的事情是, 当你醒来, 你从不知道你的进程离开 CPU 多长时间或者同时已经发生了什么改变。结果是你不能关于你醒后的系统状态做任何的假设, 并且你必须检查来确保你在等待的条件是, 确实, 真的.
简单休眠
Linux 内核中最简单的休眠方式是, wait_event。
首选的选择是 wait_event_interruptible, 它可能被信号中断
唤醒,用 wake_up
阻塞和非阻塞操作
有时还有调用进程通知你他不想阻塞, 不管它的 I/O 是否继续. 明确的非阻塞 I/O 由 filp->f_flags 中的 O_NONBLOCK 标志来指示.
增加一个输出缓冲可允许驱动在每个写调用中接收大的数据块, 性能上有相应的提高.
如你可能期望的, 非阻塞操作立刻返回, 允许这个应用程序轮询数据. 应用程序当使用 stdio 函数处理非阻塞文件中, 必须小心, 因为它们容易搞错一个的非阻塞返回为 EOF. 它们始终必须检查 errno.
一个阻塞 I/O 的例子
【理解】
高级休眠
一个进程如何休眠
手动睡眠
互斥等待
进行互斥等待的进程被一次唤醒一个, 以顺序的方式, 并且没有引起惊群问题. 但内核仍然每次唤醒所有的非互斥等待者.
唤醒细节
以前的历史
不要再使用:
void sleep_on(wait_queue_head_t *queue);
void interruptible_sleep_on(wait_queue_head_t *queue);
测试 scullpipe 驱动