介绍
本章涵盖ioctl、poll、select、llseek以及异步通知等操作。这篇笔记记得比较粗略,但是这没有什么关系。学习编程技术类的知识,最重要的是知道有某种方法可以实现某个功能,而不是细致到把每个遇到的功能都记在脑子里。
IOCTL
ioctl(io control),是struct file_operations中用于文件操作的接口之一(新版内核有改变,CentOS8 4.18内核中没有ioctl了,取而代之的是unlocked_ioctl和compat_ioctl),从这个角度来看虚拟文件系统中的文件都可以有ioctl操作。ioctl操作提供了命令扩展的机制,让驱动更灵活。前面的章节讲到,可以在ioctl中封装查询式调试命令。实时上ioctl还有其他的妙用,比如某个设备可以是字符流作为命令,也可以是ioctl作为控制命令。一般而言ioctl如果无法匹配到命令,返回-EINVAL是合理的,但是posix标准说应该返回-ENOTTY。
ioctl提供给用户态的接口,第三个参数是可变参数。但实际上ioctl只接收三个参数,不能将其理解为可接收任意参数。用于ioctl的部分命令有部分是系统内定的,可以查阅内核文档查看。内核将用于ioctl的命令分解为4个具有不同意义的字段看待,分别是type、number、direction以及size。其含义如下表
字段名称 | 位宽 | 含义 |
---|---|---|
type | 8位 | 一个企图用来区分不同驱动模块的值 |
number | 8位 | 命令的序号,多个命令时,一般让其自然数排列 |
direction | 1位 | 表征该命令执行时数据传输方向 |
size | 通常是13或者14位,通过_IOC_SIZEBITS查看 | 表征该命令执行时数据传输大小 |
为了避免不明确的、冲突的命令值定义,内核提供了一套宏来辅助用户定义用于ioctl的宏。注意,这里的写入还是读取是对于驱动模块的使用者而言的。
1 | _IO(type, number) // 生成一个无参数命令 |
同时内核还提供了一套宏,来验证用户传入的命令是否和驱动定义的驱动匹配,命令是否有效
1 | _IOC_TYPE(type) // 从命令中提取type |
阻塞I/ O
阻塞I/O是指当驱动/设备不能满足用户进程的需求是,用户进程可能陷入等待的I/O请求。当驱动/设备无法满足用户请求时,可以让用户进程进入休眠状态。此处再次强调:1、不能在原子上线中休眠;2、不能在有自旋锁、seqlock或者RCU时休眠;3、禁用了中断时不能休眠。持有信号量时是可以睡眠的,但是要考虑后果。用户进程休眠依赖于wait_queue_head_t数据结构。内核的睡眠模型是这样的,当用户向驱动请求某项资源而不可得的时候,驱动将用户进程睡眠到wait_queue_head_t数据结构上。当其他的内核代码让请求的资源变得可用时(释放或创建),其他内核代码(可能是同一份代码的重入)负责通过同一个wait_queue_head_t的指针唤醒休眠的用户进程。用户进程醒来时需要自己判断醒来是再次休眠 还是往下走。
休眠API
常用的休眠 API如下所示,注意下方的wait_queue_t在最新的内核中已经改成了wait_queue_entry
1 | DECLARE_WAIT_QUEUE_HEAD(queue); // 一步到位地创建病初始化一个等待队列头,等待队里头用队列和自旋锁实现 |
wait_eventxxx还可以手动的实现,经典代码如下所示
1 | while(!condition) { |
某些进程设置了struct file(一次打开的上下文)指针flip->f_flags中的O_NONBLOCK标志。该标志可以是打开时指定,也可以是fcntl设定。如果驱动发现用户设置了该标志,则执行open、read、write时不应该将用户进程休眠。
唤醒
有时候驱动代码需要实现更加细粒度的休眠和唤醒,例如当资源只有一个的时候,就不需要将休眠在同一个quque上的进程一次性都唤醒。驱动可以将进程置于独占式休眠状态,这样唤醒时就可以仅唤醒一个独占休眠进程让其独占资源。独占式休眠进程的休眠item会被放到休眠queue的尾部,而非独占式休眠的item会被放到queue的头部。
1 | wake_up(wait_queue_head_t *queue); // 唤醒wait_queue_head_t上的所有非独占进程 |
poll和select
unix派生了不同分支,再加上linux的出现。内核中经常有这样的情况,那就是不同的发行版做了相同的功能。poll和select就是如此,它们想解决的核心问题就是如何让进程阻塞在多个文件描述符上。下面是我收集到的不同的poll和select版本,它们功能大同小异,具体用法可以再查询man手册。
1 | int epoll_create(int size); |
用户态的这些接口需要内核态的支持。所有的poll和select在驱动层面都是由下面这个接口作为基础的,它是file_operations的成员之一。
1 | unsigned int (*poll)(struct file *filp, poll_table *wait); |
这个接口主要干两件事情
- 当驱动/设备无法满足进程需求的时候,将进程休眠到文件描述符对应的休眠队列上
- 当驱动/设备可以满足进程需求的时候,返回对应的标志位,告知用户进程
异步通知
异步通知和poll思路刚好反过来。poll有点像轮询思路,是主动询问,而异步通知是一种让驱动主动给进程发信号的设计。在用户态,用户需要设定接受信号的进程。在内核态,驱动需要在资源到达时主动向进程发消息。实现这个功能的经典用户态代码如下
1 | signal(SIGIO, &input_handler); /* 注册信号回调函数,其实最好用sigaction实现 */ |
在驱动层面,驱动主要做下面这些事情
1 | // 当F_SETOWN调用时,设置filp->f_owner,这一步虚拟文件系统负责 |
llseek
llseek就是拿来调整文件读写位置的,按照参数格式调整filp->f_ops