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

  • 相关阅读:
    Flexbox兼容性
    响应式布局(Responsive Layout)/流式布局(Fluid Layout)/自适应布局(Adaptive)
    Flex布局之box-flex
    Flex布局(伸缩盒布局)
    input美化问题
    用纯css改变下拉列表select框的默认样式
    超越icon font
    CSS居中之美
    php中yaf框架的服务器配置
    MySQL优化
  • 原文地址:https://www.cnblogs.com/wangfengju/p/6173067.html
Copyright © 2011-2022 走看看