《Windows核心编程系列》十异步IO之IO完成端口

系统 2015 0

http://blog.csdn.net/ithzhang/article/details/8508161 转载请注明出处!!

 IO 完成端口

为了将 Windows 打造成一个出色的服务器环境, Microsoft 开发出了 IO 完成端口。完成端口需要与线程池配合使用。

完成端口背后的理论是并发运行的线程数量必须有一个上限。由于太多的线程将会导致系统花费很大的代价在各个线程 cpu 上下文进行切换。

使用并发模型与创建进程相比开销要低很多,但是也需要为每个客户请求创建一个新的线程。这开销仍然很大。通过使用线程池可以是性能有很大的提高。 IO 完成端口需要配合线程池配合使用。

IO 完成端口也是一个内核对象。调用以下函数创建 IO 完成端口内核对象。

    HANDLE CreateIoCompletionPort(

      HANDLE hFile,

      HANDLE hExistingCompletionPort,

      ULONG_PTR CompletionKey,

      DWORD dwNumberOfConcurrentThreads);


  


这个函数会完成两个任务:

一是创建一个 IO 完成端口对象。

二是将一个设备与一个 IO 完成端口关联起来。

hFile 就是设备句柄。

hExistingCompletionPort 是与设备关联的 IO 完成端口句柄。为 NULL 时,系统会创建新的完成端口。

dwCompletionKey 是一个对我们有意义的值,但是操作系统并不关心我们传入的值。一般用它来区分各个设备。

dwNumberOfConcurrentThreads 告诉 IO 完成端口在同一时间最多能有多少进程处于可运行状态。如果传入 0 ,那么将使用默认值(并发的线程数量等于 cpu 数量)。在第二章我们曾介绍说几乎所有的内核对象都需要安全属性参数。那时的几乎就是因为 IO 完成端口这个例外。它是唯一一个不需要安全属性的内核对象。这是因为 IO 完成端口在设计时就是只在一个进程中使用。

每次调用 CreateIoCompletionPort 时,函数会判断 hExistingCompletionKey 是否为 NULL ,如果为 NULL ,会创建新的完成端口内核对象。并为此完成端口创建设备列表然后将设备加入到此完成端口设备列表中(先入先出)。

设备列表存储与该完成端口相关联的所有设备。

设备列表只是调用 CreateIoCompletionPort 函数时的一个数据结构。除此之外还有四个结构。

第二个结构是 IO 完成队列。当设备的一个异步 IO 请求完成时,系统会检查该设备是否与一个完成端口相关联,如果关联,系统会将这个已完成的 IO 请求添加到完成端口的 IO 完成队列中。每一项包括已传输字节数,完成键( dwCompletionKey )值,以及一个指向 IO 请求的 OVERLAPPED 结构指针和错误码。

Windows IO 完成端口提供了一个函数,可以将线程切换到睡眠状态,来等待设备 IO 请求完成并进入完成端口。

    BOOL GetQueuedCompletionStatus(

     HANDLE hCompletionPort,

     PDWORD pdwNumberOfBytesTransferred,

     ULONG_PTR pCompletionKey,

     OVERLAPPED** ppOverlapped,

     DWORD dwMilliSeconds);


  


hCompletionPort 表示线程希望对哪个完成端口进行监视, GetQueuedCompletionStatus 的任务就是将调用线程切换到睡眠状态,也就是阻塞在此函数上,直到指定的 IO 完成端口出现一项或者超时。

pdwNumberOfBytesTransferred返回在异步IO完成时传输的字节数。

pCompletionKey返回完成键。

ppOverlapped返回异步IO开始时传入的OVERLAPPED结构地址。

dwMillisecond指定等待时间。

函数执行成功则返回true,否则返回false。

第三个结构是等待线程队列。当线程池中的每个线程调用 GetQueuedCompletionStatus 时,调用线程的线程标识符会被添加到这个等待线程队列,这使得 IO 完成端口对象能知道,有哪些线程当前 正在等待 对已完成的 IO 请求进行处理。当 IO 完成端口的 IO 完成队列中 出现一项时,完成端口会唤醒 等待线程队列 中的一个线程。这个线程会得到已完成 IO 项的所有信息:已传输字节数,完成键以及 OVERLAPPED 结构地址。这些信息是通过传给 GetQueuedCompletionStatus 的参数来返回的。

IO 完成队列中的各项是以先入先出方式来进行的。但是唤醒等待队列中的线程是按照后入先出的方式进行。假设有四个线程正在等待队列中等待,如果出现了一个已完成的 IO 项,那么最后一个由于调用 GetQueuedCompletionStatus 而被挂起的线程会被唤醒来处理这一项。当处理完该项后,线程会由于再次调用 GetQueuedCompletionStatus 而进入等待线程队列。使用这种算法,系统可以将哪些长时间睡眠的线程换出到磁盘。

作者一直在推崇 IO 完成端口。接下来我们来讨论下为什么 IO 完成端口这么有用!!!

前面我们提到过 IO 完成端口只有配合线程池才能发挥更大的作用。当我们创建并关联设备时,需要指定有多少个线程并发运行。一般将这个值设置为 cpu 的数量。当已完成的 IO 项被添加到完成队列中时, IO 完成端口会唤醒正在等待的线程,但是唤醒的线程数最多不会超过我们指定的数量。如果有四个 IO 请求已完成,且有四个线程等待 GetQueuedCompletionStatus 而被挂起,那么 IO 完成端口只唤醒两个线程处理,另外两个线程继续睡眠。此时读者可能会疑问:既然完成端口只允许唤醒指定数量的线程,那么为什么还指定更多的线程在线程池中呢?这就涉及到 IO 完成端口的第四个数据结构:已释放线程列表。它存储已被唤醒的线程句柄。这使得 IO 完成端口能够直到哪些线程已经被唤醒并监视它们的执行情况。如果此时已释放线程由于调用某些函数将线程切换到了等待状态,完成端口会将其从已释放队列中移除,并将其添加到已暂停线程列表。

已暂停队列是 IO 完成端口第五个数据结构。

完成端口的目标是根据创建完成端口时指定的并发线程数量,将尽可能多的线程保持在已释放线程列表中。如果一个已暂停的线程被唤醒,它会离开已暂停线程列表并重新进入已释放线程列表。 这意味着已释放列表中的线程数量将大于最大允许的并发线程数量。 这句话什么意思呢?正在运行的线程数量加上从暂停线程列表中被释放的线程数量使总数大于最大允许的数量。这可以使线程数量在短时间内超过指定数量。

IO 完成端口并不一定要用于设备 IO ,它还可以进行线程间通信。在可提醒 IO 中我们介绍了 QueueUserAPC 。该函数允许线程将一个 APC 项添加到另一个线程的队列中。 IO 完成端口也存在一个类似的函数:

>IO 通知追加到 IO 完成端口�size:14pt">

    BOOL PostQueuedCompletionStatus(

     HANDLE hCompletionPort,

     DWORD dwNumBytes,

     ULONG_PTR CompletionKey,

     OVERLAPPED*pOverlapped);


  


这个函数用来将已完成的 IO 通知追加到 IO 完成端口的队列中。

hCompletionPort 表示我们要将已完成的 IO 项添加到哪个完成端口的队列中。

剩下的三个参数表示应该返回给主调线程的值。

�每个线程都调用一次 GetQueuedCompletionStatus ,将它们都唤�E7��程通信。例如:当用户终止服务程序时,我们想要所有线程退出。如果各个线程还在等待完成端口但有没有已完成的 IO 请求,那么无法将它们唤醒。我们可以为线程池中的每个线程都调用一次 GetQueuedCompletionStatus ,将它们都唤醒。各线程对 GetQueuedCompletionStatus 函数返回值进行检查,如果发现应用程序正在终止,就会正常退出。(由于线程等待队列是以栈方式唤醒各线程,为了保证线程池中每个线程都有机会得到模拟 IO 项,我们还必须在程序中采用其他线程同步机制)

当对完成端口调用 CloseHandle 时,系统会将所有正在等待 GetQueuedCompletionStatus 返回的线程唤醒,并返回 false GetLastError 返回 ERROR_INVALID_HANDLE ,此时线程就可以知道应该退出了。

下面的例子使用异步IO 实现了文件复制工作。首先选择要复制的文件,并取得源文件大小。点击复制按钮创建一个线程。新线程将完成创建IO完成端口和关联设备及执行文件复制工作。将源文件和目标文件都关联到同一个完成端口,根据GetQueuedCompletionStatus返回时的完成键来区分到底是属于谁的异步IO返回。在启动循环时采用了一个小伎俩:使用PostQueuedCompletionStatus向IO完成端口发送一个模拟的异步IO请求。完成键设置为WRITE_KEY。此时程序将执行从源文件读数据操作。这样就开动了引擎。直到文件复制完成。注意源文件和目标文件以及GetQueuedCompletionStatus使用的OVERLAPPED结构不要使用同一个。
选择文件函数:
        void CIOCompletionPortDlg::OnBnClickedBtnChoosefile()
{
	// TODO: 在此添加控件通知处理程序代码
	CFileDialog dlg(true);
	dlg.DoModal();
	m_fileName=dlg.GetPathName();
	SetDlgItemText(IDC_EDIT_FILENAME,m_fileName);

	m_hSrcFile=CreateFile(m_fileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_FLAG_OVERLAPPED,NULL);
	if(m_hSrcFile==INVALID_HANDLE_VALUE)
	{
		return ;
	}
	DWORD filesize;
	DWORD filesizeHigh;
	m_SrcFileSize=GetFileSize(m_hSrcFile,&filesizeHigh);
	DWORD t=m_SrcFileSize/1024.0;
	//filesize/=1024.0;
	CString temp;
	temp.Format(TEXT("%d KB"),t);
	SetDlgItemText(IDC_EDIT_FILESIZE,temp);
}
      
//创建任务线程:
        void CIOCompletionPortDlg::OnBnClickedBtnCopy()
{
	// TODO: 在此添加控件通知处理程序代码

	m_hCopyThread=CreateThread(NULL,0,CopyThread,this,0,NULL);
	//CloseHandle(m_hCopyThread);

}
      

//新线程入口函数:
        
          DWORD WINAPI CIOCompletionPortDlg::CopyThread( PVOID ppram )
{
	CIOCompletionPortDlg *pdlg=(CIOCompletionPortDlg*)ppram;
	pdlg->m_hDesFile=CreateFile(TEXT("备份.exe"),GENERIC_WRITE,0,NULL,CREATE_ALWAYS,FILE_FLAG_OVERLAPPED,pdlg->m_hSrcFile);
	LARGE_INTEGER filesize;
	filesize.HighPart=0;
	filesize.LowPart=pdlg->m_SrcFileSize;
	SetFilePointerEx(pdlg->m_hDesFile,filesize,NULL,FILE_BEGIN);
	SetEndOfFile(pdlg->m_hDesFile);


	//创建IO完成端口。创建一个完成端口,将两个设备将关联到此完成端口上。
	HANDLE hIOCP=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,4);//创建完成端口,但不关联设备。
	if(hIOCP==NULL)
	{
		return 0;
	}
	CreateIoCompletionPort(pdlg->m_hSrcFile,hIOCP,READ_KEY,0);//与IO完成端口关联。
	CreateIoCompletionPort(pdlg->m_hDesFile,hIOCP,WRITE_KEY,0);//在关联时传入了完成键。可以根据完成键来区别从完成队列中取出的请求属于哪个设备。

	OVERLAPPED ov={0};
	PostQueuedCompletionStatus(hIOCP,0,WRITE_KEY,&ov);//发送模拟完成异步IO消息。
	BYTE *pBuffer=new BYTE[BUFFERSIZE];
	OVERLAPPED ovDes={0};
	OVERLAPPED ovSrc={0};
	while(true)
	{
		//memset(pBuffer,0,sizeof(pBuffer));
		DWORD nTransfer;
		OVERLAPPED *overlapped;
		ULONG_PTR CompletionKey;
		GetQueuedCompletionStatus(hIOCP,&nTransfer,&CompletionKey,(OVERLAPPED**)&overlapped,INFINITE);//IO完成队列没有请求项则挂起。否则从IO完成队列取出。
		switch(CompletionKey)
		{
		case READ_KEY://从IO完成端口取出读完成。
			{          		                                		                                BOOL r=WriteFile(pdlg->m_hDesFile,pBuffer,overlapped->InternalHigh,NULL,&ovDes);

				ovDes.Offset+=BUFFERSIZE;
			}
			break;
		case WRITE_KEY://从IO完成队列中取出写完成。
			{
				memset(pBuffer,0,BUFFERSIZE);
				if(ovSrc.Offset<pdlg->m_SrcFileSize)
				{
					DWORD nBytes;
					if(ovSrc.Offset+BUFFERSIZE<pdlg->m_SrcFileSize)
						nBytes=BUFFERSIZE;
					else
						nBytes=pdlg->m_SrcFileSize-ovSrc.Offset;
					ReadFile(pdlg->m_hSrcFile,pBuffer,nBytes,NULL,&ovSrc);//异步IO忽略文件指针。所有对文件的定位操作由OVERLAPPED结构指定。
					//一定要注意为每次异步IO请求提供一个OVERLAPPED结构。刚才由于在接收和发送使用了
					//同一个OVERLAPPED结构,导致出现重叠 I/O 操作在进行中。错误代码:997
					ovSrc.Offset+=BUFFERSIZE;//OVERLAPPED的OffsetHigh结构必须每次都得设置。
				}
				else
				{
					::MessageBox(NULL,TEXT("文件复制完成"),TEXT(""),MB_OK);
					return 0;
				}

			}
			break;
		default:
			break;
		}
	}
	return 0;
}
        
      

运行结果:

《Windows核心编程系列》十异步IO之IO完成端口


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论