支持异步IO的Linux字符设备驱动程序
Linux 2.6内核新引入的AIO(异步IO)机制可以让应用程序发起多个IO请求,而不用等待IO完成。一般来说,块设备和网络设备驱动程序已经是异步的了,无需为支持AIO而做特别的改动;但是字符设备驱动程序却需要实现新的接口才可以支持AIO。字符设备为支持AIO而需要实现的接口定义在file_operations结构体中:
这是较新版本内核代码(笔者参考的是2.6.28.6版本的内核代码)中的定义。《Linux设备驱动开发详解(第2版)》以及网上搜索到的很多文章给出的接口定义不是这样的:没有使用iovec结构体,而是直接使用缓冲区指针char __user* buf,这是较老版本内核代码的定义。
aio_read和aio_write分别用于执行异步读写操作;aio_fsync与fsync类似,用于确保所有数据都写入到磁盘了,一般不需要实现这个接口。
要注意区分两种异步机制的差别:
1.异步IO就绪通知机制:适当的时候驱动程序调用kill_fasync()函数,使用信号SIGIO通知应用程序,设备准备就绪,可以执行IO操作了。
2.异步IO机制:应用程序发起多个IO请求,排队等待处理。异步IO与非阻塞IO的相同点是,应用程序不会阻塞。但是非阻塞IO中,如果设备资源不可用,则驱动程序返回EAGAIN,应用程序需要在适当的时候进行重试,直到资源可用,IO请求成功执行。而使用异步IO时,如果设备资源不可用,IO请求会排队等待处理,驱动程序返回EIOCBQUEUED,应用程序不需要进行重试。
本文论述如何让Linux字符设备驱动程序支持第2种异步机制。
1发起IO操作
aio_read和aio_write用于发起异步IO操作。不论设备资源是否可用,这两个接口都不应该阻塞。如果资源可用,接口应该执行请求的操作,返回操作结果;如果不可用,则应该记录IO请求,以便随后设备资源可用时再执行IO操作,然后返回EIOCBQUEUED表示IO操作请求正在排队等待处理。
《Linux设备驱动开发详解(第2版)》以及网上搜索到的很多文章给出的例子实际上是不正确的:延迟一段时间之后再返回“同步IO”的结果,这并不是异步IO。实现异步IO能力的要点在于:
l 不能立即执行请求的操作时,记录IO请求
l 资源可用时再执行记录下的IO请求,返回操作结果
笔者为《Linux设备驱动开发详解(第2版)》给出的globalfifo字符设备驱动程序增加了异步IO能力,其中记录IO请求的代码如下:
l 第一部分:如果是同步IO,或者可以立即执行请求的IO操作,则进行IO操作。内核代码定义的宏is_sync_kiocb()表示是否是“同步IO”请求。“同步IO”请求使得必要时可以同步地使用AIO子系统。
l 第二部分:如果第一步进行的操作没有完成所有的传输,则记录已经处理完成几个iovec结构体,以及对于余下的第一个待处理的iovec结构体已经传输了多少字节。
l 第三部分:分配一个io_struct结构体,用于记录待处理的IO请求。
l 第四部分:分配一个struct page结构体指针数组,保存输入参数iovec数组每一项的iov_base字段所对应的物理地址。这一步很重要。iovec结构体的iov_base字段给出的是用户进程地址,aio_read/aio_write接口在用户进程上下文中执行,可以通过copy_to_user/copy_from_user与这个地址所指的存储空间交换数据。但是不能保存这个地址供后续使用,因为后续真正执行IO操作的上下文很可能不是发起IO操作的用户进程上下文。例如:用户进程A请求读取数据,但是当前设备上没有数据可供读取,所以这个读取请求r_req被排队;一段时间后,用户进程B写入了一些数据,驱动程序发现有数据可供读取了,执行被排队的读取请求r_req。然而,驱动程序不能使用发起请求r_req的用户进程A的上下文来执行读取操作了,因为进程A并没有等待请求r_req执行完成。这样,驱动程序只能在进程B或者其他进程上下文中执行r_req请求的操作,从而不能访问进程A的进程地址空间中的地址,也就是r_req对应的iovec结构体中的iov_base字段所指的地址。解决这个问题的方法是,将请求r_req所提供的缓冲区地址(iovec结构体的iov_base字段)转换成物理地址保存起来供后续使用,因为物理地址跟进程上下文无关。第四部分的代码就是做这个工作的。
l 第五部分:将不能立即执行的异步IO请求放入到待处理IO请求链表中。
l 第六部分:如果读/写操作传输了一些字节,则可以进行写/读操作了,执行排队待处理的IO请求。不过,在这里执行队列中待处理的IO请求是不太合适的:队列中待处理的IO请求可能不是由当前进程发起的,浪费当前进程的处理器时间,执行其他进程的IO请求,这对进程是不公平的。正确的做法应该是:驱动程序创建一个线程(进程?),在自己的线程中执行异步IO请求;或者使用其他内核线程。笔者刚开始学习Linux设备驱动编程,对此不熟悉,所以这部分代码有待以后改进。