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

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

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

  1. Python 学习笔记(3)对txt文件的读与写操作(下)

    上一章节我们讨论了如何对txt文本文件进行读写操作,这一张将讨论如何进行二进制文件的写与读.<Python 学习笔记(3)对txt文件的读与写操作(上)>的链接如下https://blog ...

  2. FPGA学习笔记之Altera FPGA使用JIC文件配置固化教程

    FPGA学习笔记之Altera FPGA使用JIC文件配置固化教程 很多做过单片机的朋友都知 道,我们在对MCU烧写完程序固件后,那么该程序固件就存储在了该MCU内部.即使MCU断电了再重新上电,程序 ...

  3. oracle 怎么看监听文件,【学习笔记】Oracle11G关于监听文件位置与监听文件大小限制...

    [学习笔记]Oracle11G关于监听文件位置与监听文件大小限制 时间:2016-11-07 21:21   来源:Oracle研究中心   作者:HTZ   点击: 次 天萃荷净 Oracle研究中 ...

  4. Ubuntu学习笔记:VMware 导入虚拟机 .ova 文件

    Ubuntu学习笔记:VMware 导入虚拟机 .ova 文件 首先要有一个需要导入的虚拟化格式程序包,(.ova:开放虚拟化格式分发程序包) 打开VMware,文件→打开,选择需要的导入的包后,为虚 ...

  5. VBA学习笔记1:将同个文件夹中的工作簿汇总为一个工作簿,并建立目录超链接

    VBA学习笔记1:将同个文件夹中的工作簿汇总为一个工作簿,并建立目录超链接 1.将文件夹中的xlsx文件名复制到新工作簿: 2.将文件夹中的xlsx数据簿中的sheet复制到新表并重命名: 3.插入超 ...

  6. Java NIO 学习笔记(五)----路径、文件和管道 Path/Files/Pipe

    目录: Java NIO 学习笔记(一)----概述,Channel/Buffer Java NIO 学习笔记(二)----聚集和分散,通道到通道 Java NIO 学习笔记(三)----Select ...

  7. Linux学习笔记(一):关于文件和目录权限的一小点内容

    我的Linux学习笔记(一):关于文件和目录权限的一小点内容 前言: 之前说踏上了一条真正的程序员之路,实在是打脸,才三个月不到,就更换了工作,跑到单位上班来了.一是因为之前上班那家公司实在让我很恶心 ...

  8. kali linux学习笔记(四) : 网络端口大全介绍

    端口大全介绍 2端口:管理实用程序 3端口:压缩进程 5端口:远程作业登录 7端口:回显 9端口:丢弃 11端口:在线用户 13端口:时间 17端口:每日引用 18端口:消息发送协议 19端口:字符发 ...

  9. CST学习笔记4----------初级端口

    1.偶极子:离散端口,点到点,边到边 离散端口适合一点到另一点,一端到另一端(一边到他的参考),是比较简单的 波导端口需要定义一个面,分析这个面所能形成的模式 对于双极子天线: ①点到点,两个中心点& ...

  10. Java学习笔记(十)--控制台输入输出

    输入输出 一.控制台输入 在程序运行中要获取用户的输入数据来控制程序,我们要使用到 java.util 包中的 Scanner 类.当然 Java 中还可以使用其他的输入方式,但这里主要讲解 Scan ...

最新文章

  1. 22条 API 设计规范,API 一致性设计
  2. [转载] 【Java】将一个字符串的字符排序,按ASCII表的顺序从小到大
  3. Linux内核中流量控制(4)
  4. CVPR 2020 Oral |神奇的自监督场景去遮挡
  5. SQL Server 变量名称的Collcation跟Instance还是跟当前DB?
  6. wxpython安装2.9安装后提示找不到.exe文件_py2exe生成exe后,运行exe时提示No module named * 的解决办法...
  7. 学习笔记之lvm基本应用及其扩展和缩减实现
  8. Linux下C程序进程地址空间布局[转]
  9. 电商数据之战背后利益纠葛:触动最敏感神经
  10. 思维导图:亿图的部分使用方法
  11. 2019年java全栈工程师学习大全
  12. server sql 去 反斜杠_mssql sqlserver 检索字段中是否包含反斜杠的方法
  13. linux驱动之输入子系统
  14. Java获取国内手机号码归属地
  15. android studio app字体大小设置,Android Studio App设置TextView文字内容大小颜色
  16. Kafka配置SASL_SSL认证传输加密
  17. 【机器学习】李宏毅-预测PM2.5
  18. 网站服务器如何选择?
  19. TCP/IP卷一:87---TCP拥塞控制之(对标准算法的改进:NewReno、采用选择确认机制、转发确认(FACK)和速率减半、限制传输、拥塞窗口校验)
  20. 马斯克坚信的“矩阵模拟”,是一种怎样“烧脑”的存在?

热门文章

  1. C# Color颜色RGB对照表
  2. 高德地图聚合自定义样式
  3. 应用随机过程——张波
  4. ATA和ATAPI类型硬盘区别方法
  5. 戴尔台式计算机怎么安装的,戴尔台式机怎么安装无线网卡驱动
  6. onpropertychange
  7. Python 正则表达式详解(建议收藏!)
  8. (转)C#中 DirectoryEntry组件应用实例
  9. MyBatis出现参数索引越界
  10. IE6——png图片的修复