写读书笔记的目的是加强理解,记录自己学习的过程
在microsoft Windows 应用程序中,线程是我们最好的工具,可以用来对工作进行划分。
为了不让线程闲下来,我们需要让各个线程就他们正在执行的操作相互通信。有一种非常好的机制来进行这类通信。 它就叫IO完成端口,它可以帮助我们创建高性能而且伸缩性好的应用程序。通过使用IO完成端口,我们可以让线程在读取设备和写入设备的时候不必等待设备的响应,从而显著的提高吞吐量。
作为一名Windows开发人员,必须完全理解IO完成端口的工作方式,IO完成端口可以与设备有关系,也可以与设备无关,它是一种有无数种用途的绝佳的线程间通信机制。
1 打开和关闭设备
Windows 的优势之一是它支持的设备数量,就是我们与之能够通信的任何东西.
用来打开各种设备的函数如下:
一般的设备句柄使用closehandle来关闭,套接字使用colosesocket.
GetFileType来查询设备的类型,函数返回下列值.
细看CrateFile函数
HANDLE CreateFile(
LPCTSTRlpFileName, 文件名
DWORDdwDesiredAccess, 访问模式
DWORDdwShareMode, 共享模式
LPSECURITY_ATTRIBUTESlpSecurityAttributes, 安全属性
DWORDdwCreationDisposition, 创建标志
DWORDdwFlagsAndAttributes, 读取标志位和文件属性 非常大的文件这个标志位 FILE_FLAG_NO_BUFFERING
HANDLEhTemplateFile); 模板文件
函数成功返回文件句柄,失败时返回invalid_handle_value
Handle hFile = CreateFile(...);
if( invalid_handle_value == hFile)
{
打开文件句柄失败.
}
else
{
打开成功.
}
2 使用文件设备
GetfileSize(handle,pDWORD) 获得文件大小,当文件大于4GB时,Pword参数表示高位,返回值表上地位.
GetCompressedFileSize 返回物理文件大小
设置文件指针的位置
BOOL SetFilePointerEx(
HANDLEhFile, 文件句柄对象
LARGE_INTEGERliDistanceToMove, 需要移动的字节数
PLARGE_INTEGERlpNewFilePointer, 更新后的文件指针位置
DWORDdwMoveMethod); FILE_BEGIN ,FILE_CURRENT , FILE_END
设置文件尾部
强制文件变小或者变大
DWORD dSize,dsize1;
LARGE_INTEGER LagreSize;
LagreSize.QuadPart = 1024;
SetFilePointerEx(hFile, LagreSize, NULL, FILE_BEGIN);
SetEndOfFile(hFile);
CloseHandle(hFile);
3 执行同步设备IO
最常用的对设备数据的读取的函数是Readfile,WriteFile函数
将数据刷新至设备
FlushFileBuffers(HANDLE hfile);//把所以缓冲区的数据刷新到指定文件句柄
同步IO比如,Createfile函数会阻塞其他调用线程。必须等待函数返回才可以继续执行.
在WindowsVista中
CancelSynchronousIo(HANDLE hThread);
取消指定线程没有完成的同步IO请求
hThread 线程句柄必须要有 THREAD_TERMINATE权限
返回非0表示函数成功,返回0表示失败,更多信息见MSDN
4 异步设备IO基础
把异步IO请求加入队列是开发高性能和可伸缩高的应用程序的本质所在。
为了以异步方式访问设备,在CreatFile时 dwFlagsAndAttributes标示必须指定FILE_FLAG_OVERLAPPED 异步重叠IO。 表示我们想要用异步方式来访问设备。
OVERLAPPED 结构
The OVERLAPPED structure contains information used in asynchronous (or overlapped) input and output (I/O). 用于异步通信的一个结构体信息.
typedef struct _OVERLAPPED {
ULONG_PTR Internal; 由驱动程序设置 这个成员用来保存已处理的IO请求的错误码
ULONG_PTR InternalHigh;由驱动程序设置 表示IO完成时已经传输的字节数
union { struct { DWORD Offset; 如果设备是文件时需要设置读取偏移量,低32位 如果不是文件时这2个参数都设置为0 DWORD OffsetHigh;如果设备是文件时需要设置读取偏移量,高32位 大于4GB文件需要设置
}; PVOID Pointer; 系统保留 }; HANDLE hEvent; 事件对象,当Io完成时,它变成有信号状态
} OVERLAPPED,
*LPOVERLAPPED;
HasOverlappedIoCompleted宏使用的就是第一个参数, 函数返回true表示IO请求完成,false表示IO请求没有完成.
异步设备IO的注意事项
1. 异步IO的请求不是先进先出,也就是说设备驱动根据效率自己选择执行哪一个IO请求。
1ReadFile
2WriteFile 先调用读文件,在调用写文件,他们都是异步请求的,那么有可能先写文件,在读文件。
2. 如何正确的来检查错误 如果请求的IO异步方式执行,那么Readfile或者WriteFile返回 false,那么需要调用GetLastError 来获得信息,如果返回的是 ERROR_IO_PENDING 表示IO请求已经加入队列,晚些时候完成.
3. 异步IO完成前,一定不能销毁,或者移动数据缓存和overlapped结构体信息.
必须为每个IO请求初始化和分配一个不同的overlapped结构体信息
void ReadData(handle hfile)
{
overlapped ol = {0};
byte b[100];
reatefile(hfile, b, 100, NULL, &ol);
}
看上去这段代码没有问题,但其实有很大隐患。异步IO请求加入队列以后,函数返回了,局部变量释放。处理IO请求的驱动程序并不知道,它会去修改那个地址,有可能引起崩溃。如果设备驱动以同步方式处理可能没有问题,但是以异步方式处理时,就修改了内存内容,并且很难发现。
取消列队中的设备IO请求
1 CancelIO(handle hFile)
2 关闭设备句柄来取消所有添加到列表的请求,不管是哪个线程添加的
3 线程终止时,系统自动取消该线程添加的所有IO请求。
4 给定文件句柄的一个指定的IO请求关闭时,调用CancleioEx(HANDLE hfile, LPOVERLAPPED lOverlapped);
如果loverlapped不为NULL那么取消一个特定的IO请求,如果为NULL取消所有句柄为HFile的IO请求。
5 接收IO请求完成通知
Windows 提供了4种不同的方法来通知IO请求已经完成的通知. 下列表格表示从容易到困难的
IO完成端口无疑是最好的。
1 触发设备内核对象 没有多大用处,只能处理一个IO请求
代码如下:
//异步方式打开一个文件句柄
HANDLE hFile = CreateFile(L"sn_new.txt",GENERIC_READ | GENERIC_WRITE,FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED,NULL);
if (INVALID_HANDLE_VALUE == hFile)
{
printf("file open faild\n");
return 0;
}
OVERLAPPED overlapped = {0};
overlapped.Offset = 100; //表示从100的位置开始读取 大于4GB文件才需要设置 OffsetHigh
byte buffer[100]= {0};
bool bReadDone = ReadFile(hFile, buffer, 100, NULL, &overlapped);
if (!bReadDone && ERROR_IO_PENDING == GetLastError())
{
//表示IO请求已经加入队列,稍候完成
bool bStop = false;
while(!bStop)
{
//表示设备内核触发了
DWORD dRes = WaitForSingleObject(hFile, 1000);//等待1秒
switch(dRes)
{
case WAIT_OBJECT_0:
//表示IO处理完毕,buffer有数据了
bStop = true;
break;
case WAIT_TIMEOUT:
//表示IO没有处理完毕
break;
default: WAIT_FAILED:
break;
}
Sleep(2000);//休息2秒
}
}
else
{
//表示读取文件错误,更多信息见GetLastError();
}
2 触发事件内核对象 比设备内核对象好用,可以处理多个IO请求
下面的代码是内核对象处理IO请求的局限性
//异步方式打开一个文件句柄
HANDLE hFile = CreateFile(L"sn_new.txt",GENERIC_READ | GENERIC_WRITE,FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED,NULL);
OVERLAPPED Readoverlapped = {0};
Readoverlapped.Offset = 100; //表示从100的位置开始读取 大于4GB文件才需要设置 OffsetHigh
byte buffer[100]= {0};
bool bReadDone = ReadFile(hFile, buffer, 100, NULL, &Readoverlapped);
OVERLAPPED WriteOverlapped = {0};
WriteOverlapped.Offset = 0; //表示从文件开始的位置写
byte writeBuffer[100]={"dfffffffffffff"};
bool bWirteDone = WriteFile(hFile, writeBuffer,100,NULL,&WriteOverlapped);
WaitForSingleObject(hFile, INFINITE);// 如果等待文件句柄有信息了,那么是读完成了,还是写完成了,无法区别
所以事件内核对象出场了.
OVERLAPPED 结构的最后一个成员变量 hevent 表示一个事件,利用CreateEvent函数来创建一个事件
当IO请求处理完毕时,设备驱动程序把这个事件设置为有信号状态
那么我们应该只等待事件对象,不在应该等待设备内核对象了。
关闭触发文件对象有一个函数
SetFileCompletionNotificationModes(HANDLE hfile, UCHAR flags = FILE_SKIP_SET_EVENT_ON_HANDLE);
表示这个文件内核对象有IO完成时,也不会触发
下面的代码介绍了怎么使用事件对象:
//下面使用事件内核对象来处理IO完成请求
//异步方式打开一个文件句柄
HANDLE hFile = CreateFile(L"sn_new.txt",GENERIC_READ | GENERIC_WRITE,FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED,NULL);
OVERLAPPED Readoverlapped = {0};
Readoverlapped.Offset = 100; //表示从100的位置开始读取 大于4GB文件才需要设置 OffsetHigh
Readoverlapped.hEvent = CreateEvent(NULL, FALSE, FALSE,NULL);//默认安全属性,自动事件,未触发,匿名
byte buffer[100]= {0};
bool bReadDone = ReadFile(hFile, buffer, 100, NULL, &Readoverlapped);
OVERLAPPED WriteOverlapped = {0};
WriteOverlapped.Offset = 0; //表示从文件开始的位置写
WriteOverlapped.hEvent = CreateEvent(NULL, FALSE, FALSE,NULL);
byte writeBuffer[100]={"dfffffffffffff"};//默认安全属性,自动事件,未触发,匿名
bool bWirteDone = WriteFile(hFile, writeBuffer,100,NULL,&WriteOverlapped);
HANDLE hEventArray[2];
hEventArray[0] = Readoverlapped.hEvent;
hEventArray[1] = WriteOverlapped.hEvent;
DWORD drs = WaitForMultipleObjects(_countof(hEventArray), hEventArray, FALSE, 2000);
switch(drs)
{
case WAIT_OBJECT_0:
//读完成
break;
case WAIT_OBJECT_0 + 1:
//写完成
break;
case WAIT_TIMEOUT:
//读和写都没有完成 去泡妞吧
break;
case WAIT_FAILED:
//句柄无效
break;
}
检查重叠IO的结果
BOOL GetOverlappedResult( HANDLE
hFile
, LPOVERLAPPED
lpOverlapped
, LPDWORD
lpNumberOfBytesTransferred
, BOOL
bWait
);
参数1 表示设备句柄
参数2 表示 重叠IO结构体
参数3 表示 返回的字节数
参数4 表示 是否一直等待直到返回结果为止
3 可提醒IO
一般不推荐使用,请注意它的一些基础设施。
允许我们手动添加一项到ACP
QueueUserAPC(
__in PAPCFUNC pfnAPC, 函数指针
__in HANDLE hThread, 线程ID
__in ULONG_PTR dwData 回调函数的值
);
如果线程创建的时候没有运行,那么如果有ACP请求,会先处理APC,然后才开始运行线程,并且一起处理所以的APC
即使线程挂起,也会把ACP请求添加到ACP队列中去,如果使用了 可提醒函数等待的话,那么每次函数执行时都会把所以的ACP列队中的请求处理完,才返回,返回值为WAIT_IO_COMPLETION,如果下次又有新的请求,那么继续要调用可提醒函数,才能处理列队中的请求。
IO完成端口另外发一篇文章单独详细说明