无
分类: LINUX
2010-01-19 11:30:08
关于异步IO
记得几年前使用MFC编程的时候,曾经使用过windows的异步socket。
当在socket句柄上设置好关心的事件(如,可读、可写)后,如果事件发生,则指定的窗口会收到一个指定的消息。
int WSAAsyncSelect(SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent);
然后窗口例程取得消息,对socket进行处理(如,recv、send)。
linux也支持类似的异步IO(不局限于socket),如果事件发生,指定的进程会收到一个指定的信号,然后在信号处理函数里面可以对fd进行处理。
fcntl(fd, F_SETOWN, getpid());
使用异步socket模型可以在一个线程中处理多个socket,并且通过消息队列(或信号队列)将这些处理过程串行化。
相比于传统的select模型,异步socket模型在性能上有一定的优势(每次select操作时,对所有fd的poll操作是很影响性能的)。可能是由于代码写起来不够结构化,异步IO方式较少被人使用。
但是,实际上上面提到的异步IO并不是真正的异步IO。真正的异步IO应该是:
1、进程对fd进行读写,非阻塞;
2、内核负责完成对可读写事件的等待,以及读写过程,最后把结果通知给进程;
而不是:
1、进程设置关心的事件,非阻塞;
2、内核监听到事件,然后通知进程;
3、进程调用读写接口,对fd进程操作;
4、内核完成读写操作,返回结果;
真正的异步IO省略了上面的2~3步,省略了一次内核和用户的切进切出,具有更高的效率。
然而,一直以来,很多操作系统都没有实现真正的异步IO机制。
实现自己的异步IO
前段时间在学习写内核模块,作为练习,想做一个实现异步IO的内核模块。其基本思路是使用一个内核线程来完成对于所有相关联的fd的读写操作。用户进程进行读写时,实际上是向这个内核线程添加一个任务。
这个内核模块注册了一个字符设备(cdev),用户使用异步IO的方式如下:
1、打开这个设备,获得一个设备fd;
fd = open(“/dev/fasync”, O_RDWR);
这时,内核模块生成一个异步任务描述对象,存放在返回的fd对应的file->private_data中;
2、通过ioctl接口,将另一个实际需要读写的fd(如:socket)“绑定”到这个设备fd上;
ioctl(fd, FASYNC_IOCTL_BIND, socket);
这时,内核模块将socket信息添加到fd对应的异步任务描述对象中;
3、设置socket的f_owner,指定异步通知的对象,并注册对应的信号处理过程;
fcntl(socket, F_SETOWN, getpid());
这是由文件子系统实现的功能,owner被记录在socket对应的file结构中;
4、对这个设备fd进行一次读写操作,读写操作不阻塞。用户程序在信号处理过程中获知fd的读写结果;
read(fd, buffer, size);
这时,内核模块在fd对应的异步任务描述对象中设置任务为read,及任务相关参数buffer和size。然后将该任务添加到该模块创建的内核工作线程中。
5、内核工作线程完成对socket的监听和读写。任务完成后向socket对应的owner发送信号。
问题及解决办法
大体的想法就是这样。但是其中有一点很难实现:用户传入buffer是一个虚拟地址,它与进程的页表是对应的(如果页表换了,这个地址也就没有意义了)。这个地址仅仅在对应的进程上下文中才有效,在这个内核态的工作线程中可能是无效的,所以工作线程不能通过这个地址来进行读写。
内核空间的地址映射在系统初始化时已经生成在init_mm中,但是init_mm中的页表信息并不会直接被使用。每一个进程在创建时,它的mm结构都会在init_mm的基础上生成。也就是说,每个进程的页表实际上是继承了内核的页表。于是,运行内核代码时,并不需要切换页表,因为每一个用户进程都能提供内核所需的页表。这样的设计避免了内核和用户空间切换时的页表切换。
在上面的设计中,异步IO工作线程作为一个内核线程,并没有自己专用的页表。它也是使用之前的用户进程的页表(当从某个用户进程A切换到这个内核线程时,A的页表不被切换,继续被内核线程使用)。
当用户进程A调用read的时候,必定是从A切换到内核空间的(实际上这里还是进程A的上下文),A的页表还是生效的,所以内核可以使用用户传入的buffer。
而在工作线程因为socket可读而被唤醒时,就没法保证前一个进程就是A了,这个时候buffer是不能直接使用的。
一个可行的解决办法是在接收用户的调用时,将buffer转成page(page代表了物理页面)。这个时候,buffer可能还没有被映射,没有对应的page,所以需要把它手动建立一下映射。然后,内核模块记录下这个page,以后就通过它来读写buffer。但是,这个方法实现起来相当麻烦,要考虑的边界条件实在太多了(buffer与page边界不对齐;buffer跨多个page;buffer可能已经被用户释放,但是直接使用page的话却不知道这个事情;等等……)
后来,在较新的linux内核(2.6.2x)中看到了真正的异步IO——AIO,原来linux已经实现了异步IO。(其实,AIO早在linux 2.4时就已经被作为内核patch提供了。)
AIO提供了专门的系统调用(aio_read、aio_write、...),作为异步IO的接口。
AIO也是利用内核线程来完成读写工作的,那么它是怎么解决前面提到的读写用户buffer的问题的呢?
AIO的做法是记录下用户传入的buffer,以及用户进程的mm,然后在要存取buffer之前,将页表切换成对应用户页表(通过一个叫use_mm函数),于是就可以直接使用buffer了。
在这里,通过切换页表来使得用户传入的buffer可用,把问题变得简单了。
可惜use_mm这个函数并没有导出符号,不能被内核模块所引用(除非改一下内核),这一招不能用在我的内核模块上面了……