zoukankan      html  css  js  c++  java
  • 浅尝异步IO

    关于异步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这个函数并没有导出符号,不能被内核模块所引用(除非改一下内核),这一招不能用在我的内核模块上面了……

  • 相关阅读:
    【leetcode】416. Partition Equal Subset Sum
    【leetcode】893. Groups of Special-Equivalent Strings
    【leetcode】892. Surface Area of 3D Shapes
    【leetcode】883. Projection Area of 3D Shapes
    【leetcode】140. Word Break II
    【leetcode】126. Word Ladder II
    【leetcode】44. Wildcard Matching
    【leetcode】336. Palindrome Pairs
    【leetcode】354. Russian Doll Envelopes
    2017.12.22 英语面试手记
  • 原文地址:https://www.cnblogs.com/wangfengju/p/6173067.html
Copyright © 2011-2022 走看看