完成端口学习笔记(一):完成端口+控制台 实现文件拷贝

来源:互联网 发布:通达信软件电脑版 编辑:程序博客网 时间:2024/05/01 13:32

        最近在整理手里一个项目的后台服务端归档程序,重新梳理了一下有关“完成端口”的知识,发现还是有很多模棱两可的地方,下面记录一下再次学习的点滴,该篇博文还会有后续的补充章节,不知道什么时间会再补充^_^。


IO概念

        还记得,自己对IO的初步了解还是从BIOS开始的,那时候就建立了IO即代表输入和输出(input & output)的印象。但是跟IO关联的词有很多,很容易产生歧义,例如IO端口、IO接口、IO空间、IO请求或者IO操作等等。学习就是一个“排除不确定因素”的过程,下面我们来简单的对比一下跟IO相关的概念:

(1)IO设备:大家都知道计算机有一个核心叫做CPU,它用来管理整个计算机,在管理过程中CPU与其他组件之间的交互无非就是读或写,换个词来说就是输入或输出。那么除了CPU以外,计算机的其他组件都可被当成输入或者输出设备,简称为IO设备。

(2)IO端口:每个连接到I/O总线上的设备都有自己的I/O地址集,即所谓的I/O端口(I/O port)。I/O端口还可以被映射到物理地址空间,因此处理器和I/O设备之间的通信就可以直接使用内存进行操作的汇编指令(如mov、and、or等)。现代的硬件设备更倾向于映射I/O,这样的处理速度较快,并可以与DMA结合起来使用。(摘自《深入分析Linux内核源码》)

(3)IO接口:接口在现实世界中有多种意义,例如我们平时最常说的USB接口,这往往是两种设备之间交互的一个渠道。这里我们指的是“处于一组I/O端口与对应的设备控制器之间的一种硬件电路”。它起到翻译器的作用,即把I/O端口中的值转换成设备所需要的命令和数据。从另一个角度,它检测设备状态的变化,并对其状态寄存器作用的I/O端口进行相应的更新。(摘自《深入分析Linux内核源码》)

(4)IO空间:很多硬件都有自己的内存,通常称之为I/O空间。例如所有比较新的显卡都有独立的RAM显存。用它来存储要在屏幕上显示的屏幕影像。

(5)IO请求:了解了上述几个IO词汇的定义后,IO请求或者说IO操作就容易理解多了。按字面意思来解释,就是CPU与IO设备进行交互时发送的指令请求,可以直观的理解为IO请求指令。要了解这个IO请求指令,就要介绍一些关于操作系统“设备管理”的相关内容。所谓的设备管理是操作系统对设备处理的一种抽象。即所有硬件设备都被看成普通文件,可以通过用操作普通文件相同的系统调用来打开、关闭、读取和写入设备。系统中每个设备都有一种设备特殊文件来表示,例如系统中第一个IDE硬盘被表示成/dev/hda。(摘自《深入分析Linux内核源码》)

(6)IO操作:IO操作与IO请求是差不多的概念,就是指操作系统与IO设备进行的一种交互,当然这种操作可以由用户自己发起,有操作系统来实现,而操作系统要控制IO设备往往需要通过驱动程序,基本示意图如下:


IOCP——IO完成端口


        好了,介绍完了IO的相关概念,下面进入主题“完成端口”。在Jeffrey Richter的描述中,IO完成端口是Wnidows系统提供的最复杂的内核对象,是一种解决并发IO请求的最佳模型,是用来实现高容量网路服务器的最佳方法(一听到内核就让人头疼,由于本人能力有限,这里就不深入了,有兴趣的读者可以自行脑补)。既然是一个对象,那么就直接分析一下操作系统眼中的完成端口的具体定义吧。Windows中利用CreateIoCompletionPort命令创建完成端口对象时,系统内部自动创建了5个相应的数据结构,分别是:设备列表(Device List)、IO完成请求队列(I/O Completion Queue-FIFO)、等待线程队列(WaitingThread List-LIFO)、释放线程队列(Released Thread List)和暂停线程队列(Paused Thread List)。示意图(图片节选自《Windows via C/C++》英文版 第五版)如下:


        

        文章中的图片不是很清楚,这里将其中的英文描述简单的列在下面的表格中,

设备列表

ADD

每当调用CreateIoCompletionPort绑定到某个设备时,系统会将该设备句柄添加到设备列表中;

REMOVE

每当调用CloseHandle关闭了某个设备句柄时,系统会将该设句柄从设备列表中删除;

I/O操作

完成队列

ADD

当I/O请求操作完成时,或者调用了PostQueuedCompeltionStatus函数时,系统会将I/O请求完成状态添加到I/O完成队列中,该队列是FIFO。

REMOVE

当完成端口从等待线程队列中取出某一个工作线程时,系统会同时从I/O完成队列中取出一个元素。

等待

线程

队列

ADD

当线程中调用GetQueuedCompletionStatus函数时,系统会将该线程压入到等待线程队列中,该队列是LIFO(为了减少线程切换)。

REMOVE

当I/O完成队列非空,且工作线程并未超出总的并发数时,系统从等待线程队列中取出线程,该线程从GetQueuedCompletoinStatus函数返回开始工作。

释放

线程

队列

ADD

1)当系统从等待线程队列中激活了一个工作线程时,或者挂起的线程重新被激活时,该线程被压入释放线程队列中

2)当线程重新调用GetQueuedCompeltionStatus函数时,线程被添加到等待线程队列中;

REMOVE

当线程调用其他函数使得线程挂起时,该线程被添加到挂起线程队列中。

挂起

线程

队列

ADD

释放线程队列中的线程被挂起的时候,线程被压入到挂起线程队列中;

REMOVE

当挂起的线程重新被唤醒时,从挂起线程队列中取出。


        由此可以看出,完成端口就是一个由操作系统来创建和控制的一个复杂结构体,并且结构中的某些成员变量需要与用户或者IO设备进行交互。交互的基本流程用下面的示意图来表示,



        示意图中形象的表示了完成端口内部的指令的流向,其中用户可以通过三种方式来与完成端口进行交互,来操作内部的5个结构体。所以简单一句话总结如下:完成端口是连接操作系统、IO设备和用户的纽带。


IOCP+控制台实现单线程的文件拷贝


        只讲概念,云里雾里的很难明白。还是从实际的应用入手来学习概念印象深刻。上面我们已经知道了完成端口就是一个具有5个特殊结构体的复杂数据结构,那么探索出怎样来控制和使用好这五个结构自然就会使用完成端口了。下面我们由浅入深,逐步探索这5个结构的使用和控制。文件拷贝,就是打开源文件进行读操作,然后创建目标文件进行写操作。
(1)打开源文件和创建目标文件都使用CreateFile API函数。代码如下:
TCHAR SrcFileName[MAXSIZE];TCHAR DesFileName[MAXSIZE];cout<<"请输入源文件名:\n";cin>>SrcFileName;cout<<"请输入目的文件名:\n";cin>>DesFileName;HANDLE hSrcFile=CreateFile(SrcFileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_FLAG_OVERLAPPED,NULL);if(hSrcFile==INVALID_HANDLE_VALUE){printf("文件打开失败!");}HANDLE hDstFile=CreateFile(DesFileName,GENERIC_WRITE,0,NULL,CREATE_NEW,FILE_FLAG_OVERLAPPED,NULL);

(2)打开了文件后,接下来创建完成端口,代码如下:

//创建完成端口HANDLE hIOCP=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,4);

(3)CreateIoCompletionPort函数的各个参数含义自行参考MSDN的解释。我们知道,此时创建了完成端口hIOCP后(此处hIOCP就是代表创建的完成端口的句柄),系统在后台为我们创建的其实是5个特殊的数据结构。第一个结构是设别列表,我们可以调用第一个参数为指定设备句柄的CreateIoCompletionPort函数向设备列表中添加我们操作的设备,此处文件的拷贝就是指磁盘的操作。下面绑定源文件和目标文件

//绑定完成端口CreateIoCompletionPort(hSrcFile,hIOCP,READ_KEY,0);CreateIoCompletionPort(hDstFile,hIOCP,WRITE_KEY,0);

(4)绑定完成后,观察上一节的指令流向示意图可以发现,要想完成端口内部开始流动,需要发出IO操作指令或者使用PostQueuedCompletionStatus指令来向“IO操作队列”中添加记录。为了简单起见我们手动调用PostQueuedCompletionStatus指令来启动完成端口内部的指令流。

PostQueuedCompletionStatus(hIOCP,0,WRITE_KEY,&ov);
(5)其中WRITE_KEY是我们自定义的枚举类型的单句柄数据,用来告诉操作系统对磁盘进行的操作时读取还是写入。此时操作系统已经接收到了WRITE_KEY指令了,也就是此时的写入操作已经完成,需要再一次从源文件读取数据。(注意此处WRITE_KEY启动的是读取操作;READ_KEY启动的是写入操作)。此时完成端口内部由操作系统来判断“线程等待队列”中是否有等待的工作线程,因为我们此处为了简化演示程序,并未使用多线程技术,整个文件拷贝程序只有一个主线程也就是控制台进程本身。所以调用PostQueuedCompletionStatus指令后完成端口并未直接启动工作线程,因为“线程等待队列”为空。下面要做的就是手动填充“线程等待队列”,如上一节的示意图,线程可以调用GetQueuedCompletionStatus命令来讲自己添加到“线程等待队列”中。下面我们将控制台主线程添加到“等待线程队列”中,即将控制权交由操作系统来操作。

GetQueuedCompletionStatus(hIOCP,&nTransfer,&CompletionKey,&o,INFINITE);

后续的控制台线程就等待操作系统的通知,当“IO完成队列”中有结果后,操作系统会通知控制台主线程返回,并将IO操作的结果通过“单IO数据”,即GetQueuedCompletionStatus函数的第四个参数返回给控制台主线程。

(6)当控制台主线程再一次获得控制权时,需要根据GetQueuedCompletionStatus中返回的“单IO数据”的具体类别来进行分类处理,此处我们只有两种操作类型WRITE_KEY和READ_KEY。需要对这两种操作分别进行处理,即使用一个switch语句即可。

switch(CompletionKey){case READ_KEY://代表读取IO操作已经完成,进行下一步写入操作WriteFile(hDstFile,pBuffer,o->InternalHigh,NULL,&ovDes);cout<<"write:"<<++i<<endl;ovDes.Offset+=o->InternalHigh;//if(ovDes.Offset== FileSize/1024 )//return 0;break;case WRITE_KEY://代表写入IO操作已经完成,进行下一步读取操作memset(pBuffer,0,BUFFERSIZE*sizeof(BYTE));if(ovSrc.Offset < FileSize)//文件读取未完成{DWORD nBytes;if(ovSrc.Offset+BUFFERSIZE < FileSize)nBytes=BUFFERSIZE;elsenBytes=FileSize-ovSrc.Offset;ReadFile(hSrcFile,pBuffer,nBytes,NULL,&ovSrc);cout<<"read:"<<++j<<endl;ovSrc.Offset+=nBytes;}elsereturn 0;break;default:break;}

(注:这段代码中使用了重叠结构OVERLAPPED,该部分的详细介绍参见MSDN的官方解释,这里可以简单的将其理解为一个操作系统和用户之间的参数传递变量,属于“单IO数据”的一部分)

        

        至此整个代码就完成了,此时应该能够看出整个程序的流动顺序了。首先控制台主线程(也是本程序的唯一线程)手动调用PostQueuedCompletionStatus指令给完成端口的“IO完成队列”添加一个项,此时由于“等待线程队列”为空,控制权依然在控制台主线程。随后控制台主线程调用GetQueuedCompletionStatus指令将自身在“等待线程队列”中进行登记。此后操作系统按照“判别等待线程序列”——》“读取IO完成队列”的顺序进行循环处理,也就是操作系统通过“IO完成队列”与控制台主线程进行互相交流,来完成整个文件的拷贝。由于操作系统并不能在文件拷贝完成后自动关闭控制台主线程。所以当控制台主线程最后一次“认领”文件写入IO完成状态后,应该自己退出程序。至此整个文件就拷贝完成了,虽然利用一个线程比较浪费资源,但是却很好的演示了完成端口内部的指令流向。

        最后将整个程序的流程画出来供大家学习一下:

完成端口中的指令想流动起来必备的两个元素是:IO完成队列+等待线程队列,二者缺一不可。


为了运行代码方便,将代码整体粘贴在博文中,仅供参考:

#include <Windows.h>#include <stdio.h>#include <iostream>#include <tchar.h>using namespace std;#define MAXSIZE 256#define BUFFERSIZE 1000000enum STATEFILE{READ_KEY,WRITE_KEY};int main(){TCHAR SrcFileName[MAXSIZE];TCHAR DesFileName[MAXSIZE];cout<<"请输入源文件名:\n";cin>>SrcFileName;cout<<"请输入目的文件名:\n";cin>>DesFileName;HANDLE hSrcFile=CreateFile(SrcFileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_FLAG_OVERLAPPED,NULL);if(hSrcFile==INVALID_HANDLE_VALUE){printf("文件打开失败!");}DWORD FileSizeHigh;DWORD FileSize=GetFileSize(hSrcFile,&FileSizeHigh);HANDLE hDstFile=CreateFile(DesFileName,GENERIC_WRITE,0,NULL,CREATE_NEW,FILE_FLAG_OVERLAPPED,NULL);//创建完成端口HANDLE hIOCP=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,4);if(hIOCP==NULL){printf("完成端口创建失败!");}//绑定完成端口CreateIoCompletionPort(hSrcFile,hIOCP,READ_KEY,0);CreateIoCompletionPort(hDstFile,hIOCP,WRITE_KEY,0);OVERLAPPED ov={0};PostQueuedCompletionStatus(hIOCP,0,WRITE_KEY,&ov);OVERLAPPED ovSrc={0};OVERLAPPED ovDes={0};ULONG_PTR CompletionKey;BYTE* pBuffer=new BYTE[BUFFERSIZE];int i=0;int j=0;while(true){DWORD nTransfer;OVERLAPPED* o;GetQueuedCompletionStatus(hIOCP,&nTransfer,&CompletionKey,&o,INFINITE);switch(CompletionKey){case READ_KEY://代表读取IO操作已经完成,进行下一步写入操作WriteFile(hDstFile,pBuffer,o->InternalHigh,NULL,&ovDes);cout<<"write:"<<++i<<endl;ovDes.Offset+=o->InternalHigh;//if(ovDes.Offset== FileSize/1024 )//return 0;break;case WRITE_KEY://代表写入IO操作已经完成,进行下一步读取操作memset(pBuffer,0,BUFFERSIZE*sizeof(BYTE));if(ovSrc.Offset < FileSize)//文件读取未完成{DWORD nBytes;if(ovSrc.Offset+BUFFERSIZE < FileSize)nBytes=BUFFERSIZE;elsenBytes=FileSize-ovSrc.Offset;ReadFile(hSrcFile,pBuffer,nBytes,NULL,&ovSrc);cout<<"read:"<<++j<<endl;ovSrc.Offset+=nBytes;}elsereturn 0;break;default:break;}}return 0;}

(程序中为了演示效果明显,定义了一次读取文件字节为1000K,这个数字可以自行设置,但是如果设置的过小,文件拷贝的整个过程会分漫长)下面的截图是在自己的电脑上拷贝一个1.6GB大小的视频文件的结果:


参考博文:

http://blog.csdn.net/sodme/article/details/427405

http://www.cppblog.com/sleepwom/archive/2009/04/13/79766.html

http://msdn.microsoft.com/en-us/library/windows/desktop/aa365198(v=vs.85).aspx

http://msdn.microsoft.com/en-us/library/windows/desktop/aa365683(v=vs.85).aspx

http://msdn.microsoft.com/en-us/library/windows/desktop/aa365465(v=vs.85).aspx


(未完待续)


Date:2013-12-08

Author:zssure

E-mail:zssure@163.com





原创粉丝点击