• 理解I/O Completion Port 【转】


    什么是IOCP

    微软在Winsock2中引入了IOCP这一概念 。IOCP全称I/O Completion Port,中文译为I/O完成端口。IOCP是一个异步I/O的API,它可以高效地将I/O事件通知给应用程序。与使用select()或是其它异步方法不同的是,一个套接字[socket]与一个完成端口关联了起来,然后就可继续进行正常的Winsock操作了。然而,当一个事件发生的时候,此完成端口就将被操作系统加入一个队列中。然后应用程序可以对核心层进行查询以得到此完成端口。
     这里我要对上面的一些概念略作补充,在解释[完成]两字之前,我想先简单的提一下同步和异步这两个概念,逻辑上来讲做完一件事后再去做另一件事就是同步,而同时一起做两件或两件以上事的话就是异步了。你也可以拿单线程和多线程来作比喻。但是我们一定要将同步和堵塞,异步和非堵塞区分开来,所谓的堵塞函数诸如accept(…),当调用此函数后,此时线程将挂起,直到操作系统来通知它,”HEY兄弟,有人连进来了”,那个挂起的线程将继续进行工作,也就符合”生产者-消费者”模型。堵塞和同步看上去有两分相似,但却是完全不同的概念。大家都知道I/O设备是个相对慢速的设备,不论打印机,调制解调器,甚至硬盘,与CPU相比都是奇慢无比的,坐下来等I/O的完成是一件不甚明智的事情,有时候数据的流动率非常惊人,把数据从你的文件服务器中以Ethernet速度搬走,其速度可能高达每秒一百万字节,如果你尝试从文件服务器中读取100KB,在用户的眼光来看几乎是瞬间完成,但是,要知道,你的线程执行这个命令,已经浪费了10个一百万次CPU周期。所以说,我们一般使用另一个线程来进行I/O。重叠IO[overlapped I/O]是Win32的一项技术,你可以要求操作系统为你传送数据,并且在传送完毕时通知你。这也就是[完成]的含义。这项技术使你的程序在I/O进行过程中仍然能够继续处理事务。事实上,操作系统内部正是以线程来完成overlapped I/O。你可以获得线程所有利益,而不需要付出什么痛苦的代价。

    完成端口中所谓的[端口]并不是我们在TCP/IP中所提到的端口,可以说是完全没有关系。我到现在也没想通一个I/O设备[I/O Device]和端口[IOCP中的Port]有什么关系。估计这个端口也迷惑了不少人。IOCP只不过是用来进行读写操作,和文件I/O倒是有些类似。既然是一个读写设备,我们所能要求它的只是在处理读与写上的高效。在文章的第三部分你会轻而易举的发现IOCP设计的真正用意。
     


    IOCP和网络又有什么关系?
    int main()
    {
     WSAStartup(MAKEWORD(2, 2), &wsaData);
     ListeningSocket = socket(AF_INET, SOCK_STREAM, 0); 
     bind(ListeningSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr));
     listen(ListeningSocket, 5);
     int nlistenAddrLen = sizeof(ClientAddr);
     while(TRUE)
     {
    NewConnection = accept(ListeningSocket, (SOCKADDR*)&ClientAddr, &nlistenAddrLen);
    HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, (void*) NewConnection, 0, &dwTreadId);
      CloseHandle(hThread);
     }
     return 0;
    }
    相信只要写过网络的朋友,应该对这样的结构在熟悉不过了。accept后线程被挂起,等待一个客户发出请求,而后创建新线程来处理请求。当新线程处理客户请求时,起初的线程循环回去等待另一个客户请求。处理客户请求的线程处理完毕后终结。
     在上述的并发模型中,对每个客户请求都创建了一个线程。其优点在于等待请求的线程只需做很少的工作。大多数时间中,该线程在休眠[因为recv处于堵塞状态]。
     但是当并发模型应用在服务器端[基于Windows NT],Windows NT小组注意到这些应用程序的性能没有预料的那么高。特别的,处理很多同时的客户请求意味着很多线程并发地运行在系统中。因为所有这些线程都是可运行的[没有被挂起和等待发生什么事],Microsoft意识到NT内核花费了太多的时间来转换运行线程的上下文[Context],线程就没有得到很多CPU时间来做它们的工作。

    大家都感觉到并行模型的瓶颈在于它为每一个客户请求都创建了一个新线程。创建线程比起创建进程开销要小,但也远不是没有开销的。
    我们不妨设想一下:如果事先开好N个线程,让它们在那hold[堵塞],然后可以将所有用户的请求都投递到一个消息队列中去。然后那N个线程逐一从消息队列中去取出消息并加以处理。就可以避免针对每一个用户请求都开线程。不仅减少了线程的资源,也提高了线程的利用率。理论上很不错,你想我等泛泛之辈都能想出来的问题,Microsoft又怎会没有考虑到呢?!
    这个问题的解决方法就是一个称为I/O完成端口的内核对象,他首次在Windows NT3.5中被引入。
    其实我们上面的构想应该就差不多是IOCP的设计机理。其实说穿了IOCP不就是一个消息队列嘛!你说这和[端口]这两字有何联系。我的理解就是IOCP最多是应用程序和操作系统沟通的一个接口罢了。
    至于IOCP的具体设计那我也很难说得上来,毕竟我没看过实现的代码,但你完全可以进行模拟,只不过性能可能…,如果想深入理解IOCP, Jeffrey Ritchter的Advanced Windows 3rd其中第13章和第14张有很多宝贵的内容,你可以拿来窥视一下系统是如何完成这一切的。

    实现方法
    Microsoft为IOCP提供了相应的API函数,主要的就两个,我们逐一的来看一下:
    HANDLE CreateIoCompletionPort (
      HANDLE FileHandle,                // handle to file
      HANDLE ExistingCompletionPort,     // handle to I/O completion port
      ULONG_PTR CompletionKey,        // completion key
      DWORD NumberOfConcurrentThreads // number of threads to execute concurrently
    );
     在讨论各参数之前,首先要注意该函数实际用于两个截然不同的目的:
      1.用于创建一个完成端口对象
      2.将一个句柄[HANDLE]和完成端口关联到一起
     在创建一个完成一个端口的时候,我们只需要填写一下NumberOfConcurrentThreads这个参数就可以了。它告诉系统一个完成端口上同时允许运行的线程最大数。在默认情况下,所开线程数和CPU数量相同,但经验给我们一个公式:
      线程数 = CPU数 * 2 + 2
     要使完成端口有用,你必须把它同一个或多个设备相关联。这也是调用CreateIoCompletionPort完成的。你要向该函数传递一个已有的完成端口的句柄,我们既然要处理网络事件,那也就是将客户的socket作为HANDLE传进去。和一个完成键[对你有意义的一个32位值,也就是一个指针,操作系统并不关心你传什么]。每当你向端口关联一个设备时,系统向该完成端口的设备列表中加入一条信息纪录。

    另一个API就是
    BOOL GetQueuedCompletionStatus(
      HANDLE CompletionPort,       // handle to completion port
      LPDWORD lpNumberOfBytes,     // bytes transferred
      PULONG_PTR lpCompletionKey,  // file completion key
      LPOVERLAPPED *lpOverlapped,  // buffer
      DWORD dwMilliseconds         // optional timeout value
    );
     第一个参数指出了线程要监视哪一个完成端口。很多服务应用程序只是使用一个I/O完成端口,所有的I/O请求完成以后的通知都将发给该端口。简单的说,GetQueuedCompletionStatus使调用线程挂起,直到指定的端口的I/O完成队列中出现了一项或直到超时。同I/O完成端口相关联的第3个数据结构是使线程得到完成I/O项中的信息:传输的字节数,完成键和OVERLAPPED结构的地址。该信息是通过传递给GetQueuedCompletionSatatus的lpdwNumberOfBytesTransferred,lpdwCompletionKey和lpOverlapped参数返回给线程的。

     根据到目前为止已经讲到的东西,首先来构建一个frame。下面为您说明了如何使用完成端口来开发一个echo服务器。大致如下:
     1.初始化Winsock
     2.创建一个完成端口
     3.根据服务器线程数创建一定量的线程数
     4.准备好一个socket进行bind然后listen
     5.进入循环accept等待客户请求
     6.创建一个数据结构容纳socket和其他相关信息
     7.将连进来的socket同完成端口相关联
     8.投递一个准备接受的请求
     以后就不断的重复5至8的过程

    关于.Net中如何实现IOCP的文章:
    http://msdn.microsoft.com/zh-cn/magazine/cc163356.aspx
    http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socketasynceventargs.aspx

  • 相关阅读:
    MySQL存储引擎--MyISAM与InnoDB区别
    HTTP Keep-Alive模式
    php通过curl下载远程图片实例
    使用PHP QR Code生成二维码
    PHP中输出文件,怎么区别什么时候该用readfile() , fread(), file_get_contents(), fgets()
    SSDB 一个高性能的支持丰富数据结构的 NoSQL 数据库, 用于替代 Redis.
    html头文件设置常用之<meta>设置缓存
    redis使用watch完成秒杀抢购功能
    Linux信号(signal) 机制分析
    php信号处理
  • 原文地址:https://www.cnblogs.com/tuyile006/p/1575554.html
Copyright © 2020-2023  润新知