《Windows核心编程》读书笔记十 同步设备I/O与异步设备I/O
来源:互联网 发布:js获取select所有的值 编辑:程序博客网 时间:2024/06/06 14:21
第十章 同步设备I/O与异步设备I/O
本章内容
10.1 打开和关闭设备
10.2 使用文件设备
10.3 执行同步设备I/O
10.4 异步设备I/O基础
10.5 接收I/O请求完成通知
希望线程在处理IO时不要被挂起(非阻塞),线程和他真正执行的IO操作进行通信。使用IO完成端口(I/O Comopletion port)
使用IO完成端口可以使得线程在读取设备和写入设备时候不比等待设备的响应,从而显著提高吞吐量。
(Windows程序员有必要完全理解IO完成端口的工作方式,它是一种有无数用途的绝佳的线程间通信机制)
10.1 打开和关闭设备
设备定义为能与之通信的任何东西。(类似linux系统的File)
Windows允许我们以相同的方式来从设备读取数据和向设备写入数据,而不必关心他是何种类型的设备。
当然各种设备之间还是存在差异。
例如对于串口来说,设置波特率是合理的,但在使用命名管道来进行跨网络传输,波特率就没有意义。
文件是最常用的设备之一。
以上函数都会返回一个设备的句柄,可以将句柄传递给许多函数来与设备进行通信。例如可以调用SetCommmConfig来设置波特率
WINBASEAPIBOOLWINAPISetCommConfig( _In_ HANDLE hCommDev, _In_reads_bytes_(dwSize) LPCOMMCONFIG lpCC, _In_ DWORD dwSize );
在读取数据的时候,可以调用SetMailslotInfo来设置一个超时值
WINBASEAPIBOOLWINAPISetMailslotInfo( _In_ HANDLE hMailslot, _In_ DWORD lReadTimeout );
当完成对设备的操作以后,必须将其关闭,调用CloseHandle
如果是套接字,调用closesocket
如果有一个设备句柄,可以调用GetFileType来查出设备类型。
WINBASEAPIDWORDWINAPIGetFileType( _In_ HANDLE hFile );
GetFileType函数的返回值。
细看CreateFile函数
可以用来打开和创建磁盘文件,也可以打开许多其他设备。
WINBASEAPIHANDLEWINAPICreateFileW( _In_ LPCWSTR lpFileName, _In_ DWORD dwDesiredAccess, _In_ DWORD dwShareMode, _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes, _In_ DWORD dwCreationDisposition, _In_ DWORD dwFlagsAndAttributes, _In_opt_ HANDLE hTemplateFile );
pszName 表明设备的类型,也可以是某个设备的实例。
dwDesireAccess用来指定想以何种方式来和设备进行数据传输。常用的如下
如果是文件设备,还可以传入FILE_READ_ATTRIBUTES之类的访问标志
参数dwShareMode用来指定设备的共享特权(device-sharing privilege)
参数psa指向一个SECURITY_ATTRIBUTES结构,可以用来指定安全信息以及我们是否希望CreateFile返回的句柄能被继承。
参数dwCreationDisposition对文件的含义更重大。
如果是打开非文件类型的设备,必须传递OPEN_EXISTING
dwFlagsAndAttributes参数有两个用途。
1)允许我们设置一些标志来微调与设备之间的通信
2)如果设备是一个文件,还可以设置文件的属性。这些通信标志中大多数都是一些信号,用来高速系统我们打算一何种方式访问设备。
1. CreateFile的高速缓存标志
FILE_FLAG_NO_BUFFERING 表明在访问文件时不要使用任何数据缓存。(我们会自己对数据进行缓存)
1)在访问文件的时候,使用的偏移量必须正好是磁盘卷的扇区大小的整数倍(可以用GetDiskFreeSpace函数来确定磁盘卷的扇区大小)
2)读取/写入文件的字节数必须正好是扇区大小的整数倍
3)必须确保缓存在进程地址空间中的其实地址正好是扇区大小的整数倍。
FILE_FLAG_SEQUENTIAL_SCAN 和 FILE_FLAG_RANDOM_ACCESS, 只有当我们允许堆文件数据进行缓存,这两个标志才有用。
如果指定了FILE_FLAG_NO_BUFFERING,这两个会被忽略。
FILE_FLAG_SEQUENTIAL_SCAN 系统假定我们堆文件进行顺序读取,系统会超额读取(缓存)
FILE_FLAG_RANDOM_ACCESS 告诉系统不要提前读取文件数据。
系统的高速缓存管理器内部会维护一些数据结构,文件越大所需的数据结构越多。
如果打开一个超大的文件,可能会失败。
因此读取大文件应该使用FILE_FLAG_NO_BUFFERING标志
FILE_FLAG_WRITE_THROUGH 禁止对写入数据进行高速缓存以减少数据丢失的可能性。所有对文件的修改会直接写入磁盘。(当然系统内部仍然会高速缓存文件,方便高速读取)
2. CreateFile的其他标志
FILE_FLAG_DELETE_ON_CLOSE
在文件的所有句柄都被关闭后,删除该文件。通常和FILE_ATTRIBUTE_TEMPORARY属性一起使用。
FILE_FLAG_BACKUP_SEMANTICS
用于备份和恢复软件。 系统会检查调用者的存取令牌(access token)是否具备堆文件和目录进行备份恢复的特权。如果调用者具备相应的特权,那么系统会运行其打开文件。
改标志也支持打开一个目录的句柄
FILE_FLAG_POSIX_SEMANTCS
表明创建或打开文件的时候,以区分大小写的方式来查找文件名。(因为POSIX子系统要求在查找文件名的时候区分大小写)
FILE_FLAG_OPEN_REPARSE_POINT
忽略文件的重解析属性(eparse attribute ,一般不使用此标志)
FILE_FLAG_OPEN_NO_RECALL
高速系统不要将文件内容从脱机存储器(offline storage 比如磁带)恢复到联机存储器(online storage 比如硬盘)
当文件很长一段时间没被访问可以将文件内容专一到脱机存储器,从而腾出硬盘空间。该标志高速系统不要恢复数据,这会导致系统对脱机存储媒介进行IO操作
FILE_FLAG_OVERLAPPED
告诉系统以异步方式来访问设备(默认是同步IO)
3. 文件属性标志
例如对于临时文件,可以使用FILE_ATTRIBUTE_TEMPORARY和前面的FILE_FLAG_DELETE_ON_CLOSE组合使用。
最后一个参数 hFileTemplate 可以表示一个已经打开的文件句柄也可以传入NULL
如果hFileTemplate是一个文件句柄,那么CreateFile忽略dwFlagsAndAttributes的参数使用hFileTemplate所标识的文件属性。
hFileTemplate必须是一个已经用GENERIC_READ标志打开的文件。
如果CreateFile成功会返回文件或设备句柄,否则返回INVALID_HANDLE_VALUE
10.2 使用文件设备
10.2.1 取得文件的大小
WINBASEAPIBOOLWINAPIGetFileSizeEx( _In_ HANDLE hFile, _Out_ PLARGE_INTEGER lpFileSize );
hFile 一个已经打开的文件句柄,
lpFileSize是一个LARGE_INTEGER联合类型的地址。
typedef union _LARGE_INTEGER { struct { DWORD LowPart; LONG HighPart; } DUMMYSTRUCTNAME; struct { DWORD LowPart; LONG HighPart; } u;#endif //MIDL_PASS LONGLONG QuadPart;} LARGE_INTEGER;还有一个ULARGE_INTEGER用了表示64位无符号值。
typedef union _ULARGE_INTEGER { struct { DWORD LowPart; DWORD HighPart; } DUMMYSTRUCTNAME; struct { DWORD LowPart; DWORD HighPart; } u;#endif //MIDL_PASS ULONGLONG QuadPart;} ULARGE_INTEGER;
可以用来取得文件大小的另外一个非常有用的函数是GetCompressedFileSize
WINBASEAPIDWORDWINAPIGetCompressedFileSizeW( _In_ LPCWSTR lpFileName, _Out_opt_ LPDWORD lpFileSizeHigh );
返回物理大小。
ULARGE_INTEGER ulFileSize;ulFileSize.LowPart = GetCompressedFileSize(TEXT("SomeFile.dat"),&ulFileSize.HighPart);
GetFileSizeEx返回逻辑大小
10.2.2 设置文件指针的位置
BYTE pb[10];DWORD dwNumBytes;HANDLE hFile = CreateFile(TEXT("MyFile.dat"), ...);// Pointer set to 0ReadFile(hFile, pb, 10, &dwNumBytes, NULL);// Reads bytes 0 - 9ReadFile(hFile, pb, 10, &dwNumBytes, NULL);// Read bytes 10 - 19
每个内核对象都维护自己的文件指针,用不同内核对象打开同一个文件,文件指针也是独立的。
如果两个内核对象句柄是复制的,其实内部指向的是同一个内核对象。那么他们将共用同一个文件指针。
BYTE pb[10];DWORD dwNumBytes;HANDLE hFile1 = CreateFile(TEXT("MyFile.dat"), ...);// Pointer set to 0HANDLE hFile2;DuplicateHandle(GetCurrentProcess(), hFile1,GetCurrentProcess(), &hFile2,0, FALSE, DUPLICATE_SAME_ACCESS);ReadFile(hFile1, pb, 10, &dwNumBytes, NULL);// Reads bytes 0 - 9ReadFile(hFile2, pb, 10, &dwNumBytes, NULL);// Read bytes 10 - 19
如果要随机访问文件,那么我们需要改变与文件内核对象相关联的文件指针。通过SetFilePointerEx来达到目的。
WINBASEAPIBOOLWINAPISetFilePointerEx( _In_ HANDLE hFile, _In_ LARGE_INTEGER liDistanceToMove, _Out_opt_ PLARGE_INTEGER lpNewFilePointer, _In_ DWORD dwMoveMethod );
liDistanceToMove高速系统我们想要把指针移动多少字节。(可以使用负数)
dwMoveMethod高速如何解释liDistanceToMove参数
lpNewFilePointer 指向更新后的新的文件指针,也可以传入NULL
一些与SetFilePointerEx有关的值值得注意:
1)文件指针的值设置超过文件当前大小是蒸蛋操作。除非是在该位置向文件写入数据或是调用SetEndOfFile,这样不会增加文件在磁盘上的实际大小。
2)如果SetFilePointerEx操作的文件是用FILE_FLAG_NO_BUFFERING标致打开的文件句柄,那么文件指针只能被设置为扇区大小的整数倍。
3)Windows没有提供一个GetFilePointerEx的函数,但是可以通过SetFilePointerEx将指针移动0字节
LARGE_INTERGER liCurrentPosition = {0};
SetFilePointerEx(hFile, liCurrentPosition, &liCurrentPosition, FILE_CURRENT);
10.2.3 设置文件尾
BOOL SetEndOfFile(HANDLE hFile);
强制设置文件结尾将改变文件的尺寸。例如设置文件大小为1024
HANDLE hFile = CreateFile(...);
LARGE_INTEGER liDistanceToMove;
liDistanceToMove.QuadPart = 1024;
SetFilePointerEx(hFile, liDistanceToMove, NULL, FILE_BEGIN);
SetEndOfFile(hFile);
CloseHandle(hFile);
10.3 执行同步设备I/O
同步设备IO, 可以是文件,邮件槽,管道,套接字等。
最常见的操作就是读写数据
WINBASEAPI_Must_inspect_result_BOOLWINAPIReadFile( _In_ HANDLE hFile, _Out_writes_bytes_to_opt_(nNumberOfBytesToRead, *lpNumberOfBytesRead) __out_data_source(FILE) LPVOID lpBuffer, _In_ DWORD nNumberOfBytesToRead, _Out_opt_ LPDWORD lpNumberOfBytesRead, _Inout_opt_ LPOVERLAPPED lpOverlapped );
WINBASEAPIBOOLWINAPIWriteFile( _In_ HANDLE hFile, _In_reads_bytes_opt_(nNumberOfBytesToWrite) LPCVOID lpBuffer, _In_ DWORD nNumberOfBytesToWrite, _Out_opt_ LPDWORD lpNumberOfBytesWritten, _Inout_opt_ LPOVERLAPPED lpOverlapped );
hFile表示要访问的设备句柄。 (打开设备时不要执行FILE_FLAG_OVERLAPPED标志,否则系统会认为我们想要与设备执行异步IO)
lpBuffer执行一个缓存,函数会把数据读取到该缓存中,或者把缓存中的数据写入设备。
nNumberOfBytesToRead(要读取的字节)
nNumberOfBytesToWrite (要写入多少字节)
lpNumberOfBytesRead(返回实际读取的字节)
lpNumberOfBytesWritten(返回实际写入的字节)
lpOverlapped (设置为NULL 表示执行同步读写)
ReadFile(用于使用GENERIC_READ打开的设备)
WriteFile (用于使用GENERIC_WRITE打开的设备)
10.3.1 将数据刷新至设备
默认系统会对数据进行缓存,采用强制写入
BOOL FlushFileBuffers(HANDLE hFile);
会强制将设备有关联的缓存数据写入设备。
设备必须是通过GENERIC_WRITE标志打开的,这样该函数才能正常工作。
10.3.2 同步I/O的取消
为了创建响应好的应用程序,应该尽可能执行异步IO操作。
BOOLCancelSynchronousIo(HANDLE hThread);
取消指定线程的未完成的同步IO请求
该函数需要对线程具有THREAD_TERMINATE权限,默认THREAD_ALL_ACCESS创建的线程具有Terminate权限。
如果操作线程池,通常要调用OpenThread来得到你当前线程标识符相对应的句柄,并传入THREAD_TERMINATE参数。
改函数会将由于执行同步IO而被挂起的线程唤醒,线程本身执行的IO操作将失败。
另外如果目标线程并不是因为执行同步IO而被挂起,该函数会返回FALSE
10.4 异步设备I/O基础
和计算机执行的其他操作相比,设备IO是其中最慢,最不可预测的操作之一。
异步IO就是,将IO请求加入请求队列,由设备的驱动程序来执行IO,应用程序当前的线程不会被挂起。
CreateFile dwFlagsAndAttributes参数中指定FILE_FLAG_OVERLAPPED标志来打开设备。
WINBASEAPI_Must_inspect_result_BOOLWINAPIReadFile( _In_ HANDLE hFile, _Out_writes_bytes_to_opt_(nNumberOfBytesToRead, *lpNumberOfBytesRead) __out_data_source(FILE) LPVOID lpBuffer, _In_ DWORD nNumberOfBytesToRead, _Out_opt_ LPDWORD lpNumberOfBytesRead, _Inout_opt_ LPOVERLAPPED lpOverlapped );
WINBASEAPIBOOLWINAPIWriteFile( _In_ HANDLE hFile, _In_reads_bytes_opt_(nNumberOfBytesToWrite) LPCVOID lpBuffer, _In_ DWORD nNumberOfBytesToWrite, _Out_opt_ LPDWORD lpNumberOfBytesWritten, _Inout_opt_ LPOVERLAPPED lpOverlapped );
为了将IO请求加入设备驱动程序的队列。使用ReadFile和WriteFile函数。(参考10.3)
执行异步的时候第四个参数传入NULL,并不需要等待其返回。
10.4.1 OVERLAPPED结构
在执行异步设备IO时,必须给pOverlapped传入一个OVERLAPPED结构。
typedef struct _OVERLAPPED { ULONG_PTR Internal; ULONG_PTR InternalHigh; union { struct { DWORD Offset; DWORD OffsetHigh; } DUMMYSTRUCTNAME; PVOID Pointer; } DUMMYUNIONNAME; HANDLE hEvent;} OVERLAPPED, *LPOVERLAPPED;
1. Offset 和OffsetHigh成员
这两个成员构成一个64位偏移量,他们表示访问文件的时候应该从哪里开始进行IO操作。
异步调用不使用同步调用时候的文件指针来访问文件,而使用这两个成员。
非文件设备会忽略这两个成员。必须将其初始化为0,否则IO请求将会失败。
2. hEvent成员
用来接收IO完成通知的4种方法之一(触发事件通知)
3. Internal成员
设备保留使用,用来保存已处理的IO请求的错误代码。 一旦发出一个异步请求,设备驱动会立即将Internal设置为STATUS_PENDING表示没有错误。
WinBase宏可以检查IO操作是否完成
#define HasOverlappedIoCompleted(lpOverlapped) (((DWORD)(lpOverlapped)->Internal) != STATUS_PENDING)
4. InternalHigh成员
当异步IO请求完成时,这个成员用来保存已传输的字节数。
10.4.2 异步设备I/O的注意事项
1)设备驱动程序不必以先入先出的方式来处理队列中的IO请求。例如一下代码:
OVERLAPPED o1 = { 0 };OVERLAPPED o2 = { 0 };BYTE bBuffer[100];ReadFile(hFile, bBuffer, 100, NULL, &o1);WriteFile(hFile, bBuffer, 100, NULL, &o2);
设备驱动可能会先写入文件后再读取文件。
如果不按顺序执行IO请求能提高性能,那么设备驱动一般都会这样做。
设备驱动在执行某些操作的时候总是以同步方式进行,而不会将其添加到队列。
参考MSDN https://support.microsoft.com/en-us/help/156932/asynchronous-disk-i-o-appears-as-synchronous-on-windows
1)比如NTFS文件压缩。
2)NTFS加密
3)当请求访问的设备数据正好在缓存中,可以立即返回(看似同步)
4)当数据不在缓存中,一般会立即返回(执行异步IO)
如果请求是同步执行,ReadFile和WriteFile返回非0.
如果是异步方式执行,或者调用ReadFile或WriteFile时发生错误,那么返回值是FALSE。必须调用GetLastError检测到底发生了什么。
如果返回ERROR_IO_PENDING那么请求已经成功加入队列,会在晚些时候完成。
如果返回ERROR_IO_PENDING以外的值,那么IO请求无法被添加到设备驱动程序的队列中。
常见错误代码。
ERROR_INVALID_USER_BUFFER或 ERROR_NOT_ENOUGH_MEMORY
ERROR_NOT_ENOUGH_QUOTA
一个错误的例子
VOID ReadData(HANDLE hFile) {OVERLAPPED o = { 0 };BYTE b[100];ReadFile(hFile, b, 100, NULL, &o);}该函数看似没问题,实际上这是异步IO请求。ReadFile返回以后栈上的BYTE数组和OVERLAPPED结构会被销毁。
等设备驱动真正执行ReadFile请求的时候,其地址已经不可用。
10.4.3 取消队列中的设备I/O请求
有时候需要将一个已经加入IO队列中的异步IO请求取消。
BOOL CancelIo(HANDLE hFile);
取消hFile句柄所表示的线程添加到队列中的所有IO请求。
可以直接关闭设备句柄,这样会取消其已经添加到队列中的所有IO请求。
单线程终止时,系统会自动取消该线程发出的所有IO请求,如果请求被发往的设备句柄具有与之关联的IO完成端口,那么他们不再被取消之列。
如果要将文件句柄的一个指定的IO请求取消,使用
BOOL CancelIoEx(HANDLE hFile, LOVERLAPPED pOverlapped);
将Overlapped关联的异步IO请求取消。
如果pOverlapped为NULL 会将该设备句柄的所有IO请求都取消。
被取消的IO请求会返回错误代码 ERROR_OPERATION_ABORTED
10.5 接收IO请求完成通知
10.5.1 触发设备的内核对象
ReadFile和WriteFile函数将IO请求添加到队列之前,会先将设备内核对象设置为未触发状态。
当设备驱动完成了请求以后,驱动程序会将设备内核对象设置为非触发状态。
以下是一个简单的例子:
HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);BYTE bBuffer[100];OVERLAPPED o = { 0 };o.Offset = 345;BOOL bReadDone = ReadFile(hFile, bBuffer, 100, NULL, &o);DWORD dwError = GetLastError();if (!bReadDone && (dwError == ERROR_IO_PENDING)) {// The I/O is being performaed asynchronously; wait for it to completeWaitForSingleObject(hFile, INFINITE);bReadDone = TRUE;}if (bReadDone) {// o.Internal contains the I/O error// o.InternalHIgh contains the number of bytes transferred// bBuffer contains the read data}else {// An error occured; see dwError}
这代码只是阐述如何等待异步IO的概念,实际应用不会这样写。
1)设备必须是用FILE_FLAG_OVERLAPPED标志以异步方式打开的。
2)必须对OVERLAPPED结构的Offset, OffsetHigh和hEvent成员初始化,以上例子Offset设为345,其他值都为0.
3)ReadFile返回值被保存在bReadDone中,它表示该IO请求不是以同步方式完成的。
4)IO不是以同步方式完成, ,代码继续检查是否有错误发生,判断GetLastError()的返回是否为异步IO方式。
5)为了等待数据,调用WaitForSingleObject 将当前线程挂起,知道该内核对象被触发为止。当设备驱动完成IO调用的时候,会触发该内核对象。
并将bReadDone设为TRUE
6)读取完成后,可以查看bBuffer中的数据,可以查看保存在OVERLAPPED结构的Internal成员中的错误代码。可以查看OVERLAPPED结构的InternalHigh成员中已传输的字节数。
7)如果真的发生了错误,那么dwError的错误码可以给出更多的信息。
10.5.2 触发事件内核对象
使用内核对象无法确定多个异步IO触发了内核对象究竟是哪一个IO完成了。
例如一下例子:
HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);BYTE bBuffer[100];OVERLAPPED oRead = { 0 };oRead.Offset = 0;BOOL bReadDone = ReadFile(hFile, bBuffer, 100, NULL, &oRead);BYTE bWriteBuffer[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };OVERLAPPED oWrite = { 0 };oWrite.Offset = 10;WriteFile(hFile, bWriteBuffer, _countof(bWriteBuffer), NULL, &oWrite);//...WaitForSingleObject(hFile, INFINITE);// We don't know what completed: read? write? both?
OVERLAPPED结构的hEvent成员若为非NULL 那么异步操作完成以后会触发该事件内核对象。
等待线程等待该事件触发即可。
一下代码展示了使用事件来进行异步IO同步的方法:
HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);BYTE bBuffer[100];OVERLAPPED oRead = { 0 };oRead.Offset = 0;oRead.hEvent = CreateEvent(...);BOOL bReadDone = ReadFile(hFile, bBuffer, 100, NULL, &oRead);BYTE bWriteBuffer[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };OVERLAPPED oWrite = { 0 };oWrite.Offset = 10;oWrite.hEvent = CreateEvent(...);WriteFile(hFile, bWriteBuffer, _countof(bWriteBuffer), NULL, &oWrite);//...HANDLE h[2];h[0] = oRead.hEvent;h[1] = oWrite.hEvent;DWORD dw = WaitForMultipleObjects(2, h, FALSE, INFINITE);switch (dw - WAIT_OBJECT_0) {case 0:// read completedbreak;case 1:// write compltedbreak;}
实际应用中会采用一个循环来等待IO请求完成,当每个请求完成的时候,线程会执行它想执行的任务,将另一个异步IO请求添加到队列中,进入下一次循环并等待更多的IO请求完成。
GetOverlappedResult函数
获取IO处理过程中传输了多少字节。
WINBASEAPIBOOLWINAPIGetOverlappedResult( _In_ HANDLE hFile, _In_ LPOVERLAPPED lpOverlapped, _Out_ LPDWORD lpNumberOfBytesTransferred, _In_ BOOL bWait );逆向工程展示了该函数的实现方式。
BOOL GetOverlappedResult(HANDLE hFile,OVERLAPPED * po,PDWORD pdwNumBytes,BOOL bWait) {if (po->Internal == STATUS_PENDING) {DWORD dwWaitRet = WAIT_TIMEOUT;if (bWait) {// Wait for the IO to completedwWaitRet = WaitForSingleObject((po->hEvent != NULL) ? po->hEvent : hFile, INFINITE);}if (dwWaitRet == WAIT_TIMEOUT) {// I/O not complete and we're not supposed to waitSetLastError(ERROR_IO_INCOMPLETE);return FALSE;}if (dwWaitRet != WAIT_OBJECT_0) {// Error calling WaitForSingleObjectreturn FALSE;}}//IO is coomplete; return number of bytes transferred*pdwNumBytes = po->InternalHigh;if (SUCCEEDED(po->Internal)) {return TRUE;}// Set last error to IO errorSetLastError(po->Internal);return FALSE;}
10.5.3 可提醒IO
(作者建议不要使用可提醒IO,非常糟糕,但是Microsoft为了使用可提醒IO添加了一些基础设施,这些基础设施非常有用)
系统创建一个线程的时候,会同时创建一个与线程相关联的队列,这个队列被称为异步过程调用(asynchronous procedure call, APC)队列。
为了将IO完成通知添加到APC队列中,我们应该调用ReadFileEx和WriteFileEx函数
WINBASEAPI_Must_inspect_result_BOOLWINAPIReadFileEx( _In_ HANDLE hFile, _Out_writes_bytes_opt_(nNumberOfBytesToRead) __out_data_source(FILE) LPVOID lpBuffer, _In_ DWORD nNumberOfBytesToRead, _Inout_ LPOVERLAPPED lpOverlapped, _In_ LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
WINBASEAPIBOOLWINAPIWriteFileEx( _In_ HANDLE hFile, _In_reads_bytes_opt_(nNumberOfBytesToWrite) LPCVOID lpBuffer, _In_ DWORD nNumberOfBytesToWrite, _Inout_ LPOVERLAPPED lpOverlapped, _In_ LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
Ex函数没有直接返回一个保存已经传输多少数据的地址,相反需要传入一个回调函数。这成为完成函数
VOID WINAPI CompletionRoutine(DWORD dwError,DWORD dwNumBytes,OVERLAPPED * po);
当使用ReadFileEx和WriteFileEx发出一个IO请求队列的时候,这两个函数会将回调函数地址传给设备驱动程序。当设备驱动程序完成IO请求的时候,会发出IO请求的线程APC队列中添加一项。该项保护完成函数的地址,以及在发出IO请求所使用的OVERLAPPED结构的地址
当一个可提醒IO完成时,设备驱动不会去试图触发一个事件对象。
一个例子,添加3个异步完成通知的IO
HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);ReadFileEx(hFile, ...);// Perform first ReadFileExWriteFileEx(hFile, ...);// Perform first WriteFileExReadFile(hFile, ...);// Perform second ReadFileExSomeFunc(); // do something else.当3个IO完成以后,APC队列可能是这样
first WriteFileEx completed
second ReadFileEx completed
First ReadFileEx completed
此时完成函数不会立即通知线程,需要等线程进入可提醒状态。
以下6个函数可以使当前线程进入可提醒状态。
WINBASEAPIDWORDWINAPISleepEx( _In_ DWORD dwMilliseconds, _In_ BOOL bAlertable );WINBASEAPIDWORDWINAPIWaitForSingleObjectEx( _In_ HANDLE hHandle, _In_ DWORD dwMilliseconds, _In_ BOOL bAlertable );WINBASEAPIDWORDWINAPIWaitForMultipleObjectsEx( _In_ DWORD nCount, _In_reads_(nCount) CONST HANDLE * lpHandles, _In_ BOOL bWaitAll, _In_ DWORD dwMilliseconds, _In_ BOOL bAlertable );WINBASEAPIDWORDWINAPISignalObjectAndWait( _In_ HANDLE hObjectToSignal, _In_ HANDLE hObjectToWaitOn, _In_ DWORD dwMilliseconds, _In_ BOOL bAlertable );WINBASEAPIBOOLWINAPIGetQueuedCompletionStatusEx( _In_ HANDLE CompletionPort, _Out_writes_to_(ulCount, *ulNumEntriesRemoved) LPOVERLAPPED_ENTRY lpCompletionPortEntries, _In_ ULONG ulCount, _Out_ PULONG ulNumEntriesRemoved, _In_ DWORD dwMilliseconds, _In_ BOOL fAlertable );WINUSERAPIDWORDWINAPIMsgWaitForMultipleObjectsEx( _In_ DWORD nCount, _In_reads_opt_(nCount) CONST HANDLE *pHandles, _In_ DWORD dwMilliseconds, _In_ DWORD dwWakeMask, _In_ DWORD dwFlags);
前5个函数的最后一个参数表示调用函数是否应该将自己设置为可提醒状态。
对于MsgWaitForMultipleObjectsEx来说,必须使用MWMO_ALERTABLE标志来让线程进入可提醒状态。
Sleep, WaitForSingleObject和WaitForMultipleObjects函数在内部调用其对应的Ex函数并传入FALSE给bAlertable参数。
当线程进入可提醒状态以后,系统会检查线程的APC队列。如果至少有一项,那么系统就不会让线程进入睡眠状态。系统会将APC队列中的那一项取出,让线程调用回调函数,并传入已经完成IO请求的错误代码,一传入字节,以及OVERLAPPED结构的地址。
当回调函数返回以后,系统会检查APC队列是否还有其他项,如果后则会继续处理。
只有当线程APC队列中一项都没有的时候,这些函数才会将线程挂起。
一旦APC队列中出现一项,系统会唤醒线程然后函数会理解返回--线程不会再次进入睡眠状态来等待内核对象触发。
可提醒IO的优劣
1.回调函数 每个IO都需要创建回调函数,并且回调函数返回的信息又不够需要维护全局变量。
2.线程问题 发出请求IO的线程必须同时完成通知的处理。如果一个线程发出多个请求,那么即使其他线程完成处于空闲状态,那么该线程也必须对每个请求的完成通知做出响应。由于不存在负载均衡机制,因此应用程序的伸缩性不太好。
可提醒IO的优点:
Windows提供一个函数,允许手动添加一个项目到APC队列中
WINBASEAPIDWORDWINAPIQueueUserAPC( _In_ PAPCFUNC pfnAPC, _In_ HANDLE hThread, _In_ ULONG_PTR dwData );
第一个参数是执行APC回调函数的指针
VOID WINAPI APCFunc(ULONG_PTR dwParam);
第二个参数是线程句柄,将该项目添加到哪个线程的队列中。(可以是系统中的任何线程,如果hThread是另一个进程中的线程,那么pfnAPC函数的内存地址也必须在另一个进程的地址空间中)
dwData 一个传递给回调函数的值。
QueueUserAPC可以进行跨进程间的线程通信,可惜只能传递一个值。
QueueUserAPC可以用来强制让线程退出等待状态。参考一下代码:
// The APC callback function has nothing to doVOID WINAPI APCFunc(ULONG_PTR dwParam) {// Nothing to do in here}UINT WINAPI ThreadFunc(PVOID pvParam) {HANDLE hEvent = (HANDLE)pvParam;// Handle is passed to this thread// Wait in an altertable state so that we can be forced to exit cleanlyDWORD dw = WaitForSingleObjectEx(hEvent, INFINITE, TRUE);if (dw == WAIT_OBJECT_0) {// object became signaled}else if (dw == WAIT_IO_COMPLETION) {// QueueUserAPC forced us out of a wait statereturn 0;// Thread dies cleanly}//..return 0;}int _tmain(int argc, TCHAR* argv[], TCHAR * env[]){HANDLE hEvent = CreateEvent(...);HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (PVOID)hEvent, 0, NULL);// Force the secondary thread to exit cleanlyQueueUserAPC(APCFunc, hThread, NULL);WaitForSingleObject(hThread, INFINITE);CloseHandle(hThread);CloseHandle(hEvent);return 0;}
10.5.4 /O完成端口
一个服务器应用程序可以采用两种架构模型
1)串行模型:一个线程等待客户发出的请求,当请求到达,线程会被唤醒并对客户的请求处理
2)并发模型:一个线程等待一个客户的请求,并创建一个新的线程来处理请求。当新线程正在处理客户请求的时候,原来的线程会进入下一次循环并等待另一个客户请求。当处理客户请求的线程完成整个处理,该线程就会终止。
1. 创建IO完成端口
IO完成端口背后的理论是,并发运行的线程数量必须有一个上限。也就是说同时并发出500个客户请求不允许出现500个可运行的线程。(一旦可运行线程数量大于CPU数量,系统就必须花时间来切换线程的CONTEXT,这会浪费cpu周期--并发模型的一个潜在缺点)
大量的创建销毁线程也是不少的系统开销,通过在应用程序初始化的时候创建一个线程池,并让线程池中的线程在应用程序运行期间保持一致可用的状态,那么服务器应用程序的性能就能够得到提高。 IO完成端口设计初衷就是配合线程池使用。
WINBASEAPI_Ret_maybenull_HANDLEWINAPICreateIoCompletionPort( _In_ HANDLE FileHandle, _In_opt_ HANDLE ExistingCompletionPort, _In_ ULONG_PTR CompletionKey, _In_ DWORD NumberOfConcurrentThreads );
HANDLE CreateNewCompletionPort(DWORD dwNumberOfConcurrentThreads) {return (CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0,dwNumberOfConcurrentThreads));}这个函数只有一个参数dwNumberOfConcurrentThreads,并调用了CreateIoCompletionPort并在前三个参数传入固定值,最后一个参数传入dwNumberOfConcurrentThreads
告知IO完成端口同一时间最多能有多少线程处于可运行状态。 如果传入0,那么IO完成端口会使用默认值,也就是运行并发执行的线程数量等于主机cpu的数量。
IO完成端口设计的初衷就是只在一个进程使用,不需要传入SECURITY_ATTRIBUTES结构给它。
将设备与IO完成端口关联起来
IO完成端口内部维护多个数据结构如图所示:
第一个数据结构是一张设备列表,表示与该端口相关联的一个或多个设备。通过CreateIoCompletionPort将设备与端口关联起来。
作者创建了一个自己的函数
BOOL AssociateDeviceWithCompletionPort(HANDLE hCompletionPort, HANDLE hDevice, DWORD dwCompletionKey) {HANDLE h = CreateIoCompletionPort(hDevice, hCompletionPort, dwCompletionKey, 0);return (h == hCompletionPort);}
该函数把一个项添加到一个已有的IO完成端口的设备列表中。
需要给这个函数传入一个已有IO完成端口的句柄(前一个由CreateNewCompletionPort调用返回),设备句柄(文件,套接字,邮件槽,管道等)
以及一个完成键(即completion key,对我们有意义的值,操作系统并不关心我们传入的到底是什么值)
每次调用该函数成功系统会将这些信息追加到IO完成端口的设备列表中。
第二个数据结构是一个IO完成队列。当设备的一个异步IO请求完成时,系统会检查设备是否与一个IO完成端口相关联,如果设备与其关联,那么系统会将该项已经完成的IO请求追加到IO完成端口的IO完成队列末尾(FIFO)
这个队列的每一项包含的信息有:已传输的字节数,最初将设备与端口关联在一起时所使用的完成键值,一个指向IO请求的OVERLAPPED结构的指针和一个错误码
2. IO完成端口的周边架构
当我们的服务器程序初始化的时候,
1)调用CreateNewCompletionPort创建IO完成端口, 默认线程池的线程数采用(主机cpux2)
2)在线程池的线程函数内部会执行一个循环等待进程退出通知,然后调用GetQueuedCompletionStatus等待IO完成端口的通知,同时将自己挂起。
将调用线程切换到睡眠状态,直到指定的完成端口的IO完成队列中出现一项,或者等待事件超出了dwMilliseconds
WINBASEAPIBOOLWINAPIGetQueuedCompletionStatus( _In_ HANDLE CompletionPort, _Out_ LPDWORD lpNumberOfBytesTransferred, _Out_ PULONG_PTR lpCompletionKey, _Out_ LPOVERLAPPED * lpOverlapped, _In_ DWORD dwMilliseconds );
与IO完成端口关联的第三个数据结构是“等待线程队列”。 当线程池中的每个线程调用GetQueuedCompletionStatus的时候,调用线程标识符会被添加到这个等待线程队列,这使得IO完成端口内核对象始终都能够知道,有哪些线程当前正在等待对已完成的IO请求进行处理。
当IO完成队列中出现一项的时候,该完成端口会唤醒等待线程队列中的一个线程。这个线程会得到已完成IO项中的所有信息:已传输字节数,完成键以及OVERLAPPED结构地址。通过GetQueuedCompletionStatus的参数 lpNumberOfBytesTransferred,lpCompletionKey 和 lpOverlapped传递。
以下代码展示了确定GetQueuedCompletionStatus返回的原因。
DWORD dwNumBytes;ULONG_PTR CompletionKey;OVERLAPPED * pOverlapped;// hIOCP is initialized somewhere else in the programBOOL bOk = GetQueuedCompletionStatus(hIOCP,&dwNumBytes, &CompletionKey, &pOverlapped, 1000);DWORD dwError = GetLastError();if (bOk) {// Process a successfully completed I/O request}else {if (pOverlapped != NULL) {// Process a failed completed I/O request// dwError contains the reason for failure}else {if (dwError == WAIT_TIMEOUT) {// Time-out while waiting for completed I/O entry}else {// Bad call to GetQueuedCompletionStatus// dwError contains the reason for the bad call}}}
移出IO完成队列是以先入先出的方式进行的。 但是唤醒那些调用GetQueuedCompletionStatus的线程是以后入先出的方式来进行的。
这种算法使得尽可能多的等待线程能够让系统将那些未调度的线程资源换出到磁盘(虚拟内存),并将它们从处理器的高速缓存中清除。
在Vista中,如果预计会不断地收到大量IO请求,可以调用以下函数来同时取得多个IO请求,而不必让许多线程等待完成端口,
WINBASEAPIBOOLWINAPIGetQueuedCompletionStatusEx( _In_ HANDLE CompletionPort, _Out_writes_to_(ulCount, *ulNumEntriesRemoved) LPOVERLAPPED_ENTRY lpCompletionPortEntries, _In_ ULONG ulCount, _Out_ PULONG ulNumEntriesRemoved, _In_ DWORD dwMilliseconds, _In_ BOOL fAlertable );
CompletionPort 想要监视的完成端口
lpCompletionPortEntries 该函数会取出指定完成端口中存在的个项存放在此参数中。
ulCount 取出的项的数量
ulNumEntriesRemoved 完成队列中被移除的IO请求的数量
fAlertable 是否进入可提醒状态
数组lpCompletionPortEntries的每一个项目是一个OVERLAPPED_ENTRY结构,用来保存已完成的IO请求的相关信息。
typedef struct _OVERLAPPED_ENTRY { ULONG_PTR lpCompletionKey; LPOVERLAPPED lpOverlapped; ULONG_PTR Internal; DWORD dwNumberOfBytesTransferred;} OVERLAPPED_ENTRY, *LPOVERLAPPED_ENTRY;
字段Internal未公开含义 不应该使用。
3. I/O完成端口如何管理线程池
完成端口的第四个数据结构,已释放线程列表(released thread list)
完成端口第五个数据结构,已暂停线程列表(paused thread list)
4. 线程池中有多少线程
可以创建下面的变量来管理线程池:
LONG g_nThreadsMin;// Minimum number of threads in poolLONG g_nThreadsMax;// Maximum number of threads in poolLONG g_nThreadsCrnt;// Current number of threads in poolLONG g_nThreadsBusy;// Number of busy threads in pool
在线程池初始化的时候,可以创建g_nThreadsMin个线程,所有线程都执行同一个线程池函数。
LONG g_nThreadsMin;// Minimum number of threads in poolLONG g_nThreadsMax;// Maximum number of threads in poolLONG g_nThreadsCrnt;// Current number of threads in poolLONG g_nThreadsBusy;// Number of busy threads in poolDWORD WINAPI ThreadPoolFunc(PVOID pv) {// Thread is entering poolInterlockedIncrement(&g_nThreadsCrnt);InterlockedIncrement(&g_nThreadsBusy);for (BOOL bStayInPool = TRUE; bStayInPool;) {// Thread stops executing and waits for something to doInterlockedDecrement(&g_nThreadsBusy);BOOL bOk = GetQueuedCompletionStatus(...);DWORD dwIOError = GetLastError();// Thread has something to do, so it's busyint nThreadsBusy = InterlockedIncrement(&g_nThreadsBusy);// Should we add another thread to the pool?if (nThreadsBusy == g_nThreadsCrnt) { // All threads are busyif (nThreadsBusy < g_nThreadsMax) { // The pool isn't fullif (GetCPUUSage() < 75){// CPU usage is below 75%// Add thread to poolCloseHandle(_beginthreadex(...));}}}if (!bOk && (dwIOError == WAIT_TIMEOUT)) {// Thread timed out// There isn't much for the server to do, and this thread// can die even if it still has outstanding I/O requestbStayInPool = FALSE;}if (bOk || (po != NULL)) {// Thread woke to process something; process it// ...if (GetCPUUsage() > 90) {// CPU usage is above 90%if (g_nThreadsCrnt > g_nThreadsMin)// Pool above minbStayInPool = FALSE;// Remove thread from pool}}}// Thread is leaving poolInterlockedDecrement(&g_nThreadsBusy);InterlockedDecrement(&g_nThreadsCrnt);return 0;}
10.5.5 模拟已完成的I/O请求
PostQueuedCompletionStatus
WINBASEAPIBOOLWINAPIPostQueuedCompletionStatus( _In_ HANDLE CompletionPort, _In_ DWORD dwNumberOfBytesTransferred, _In_ ULONG_PTR dwCompletionKey, _In_opt_ LPOVERLAPPED lpOverlapped );
将一个项添加完成端口的到已完成队列中。
hCompletionPort 目标IO完成端口
dwNumberOfBytesTransferred 已传输字节
dwCompletionKey 完成键
lpOverlapped 指向OVERLAPPED的结构
可以使用该函数来模拟和线程池中的线程进行通信。
FileCopy 示例程序
源代码
/******************************************************************************Module: FileCopy.cppNotices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre******************************************************************************/#include "..\CommonFiles\CmnHdr.h"#include "..\CommonFiles\IoCompletionPort.h"#include "..\CommonFiles\EnsureCleanup.h"#include <windowsx.h>#include "resource.h"// C RunTime Header Files#include <stdlib.h>#include <malloc.h>#include <memory.h>#include <tchar.h>//////////////////////////////////////////////////////////////////////////// Each I/O Request needs an OVERLAPPED structure and a data bufferclass CIOReq : public OVERLAPPED {public:CIOReq() {Internal = InternalHigh = 0;Offset = OffsetHigh = 0;hEvent = NULL;m_nBuffSiz = 0;m_pvData = NULL;}~CIOReq() {if (m_pvData != NULL)VirtualFree(m_pvData, 0, MEM_RELEASE);}BOOL AllocBuffer(SIZE_T nBuffSiz) {m_nBuffSiz = nBuffSiz;m_pvData = VirtualAlloc(NULL, m_nBuffSiz, MEM_COMMIT, PAGE_READWRITE);return (m_pvData != NULL);}BOOL Read(HANDLE hDevice, PLARGE_INTEGER pliOffset = NULL) {if (pliOffset != NULL) {Offset = pliOffset->LowPart;OffsetHigh = pliOffset->HighPart;}return (::ReadFile(hDevice, m_pvData, m_nBuffSiz, NULL, this));}BOOL Write(HANDLE hDevice, PLARGE_INTEGER pliOffset = NULL) {if (pliOffset != NULL) {Offset = pliOffset->LowPart;OffsetHigh = pliOffset->HighPart;}return (::WriteFile(hDevice, m_pvData, m_nBuffSiz, NULL, this));}private:SIZE_T m_nBuffSiz;PVOID m_pvData;};//////////////////////////////////////////////////////////////////////////#define BUFFSIZE(64 * 1024)// The size of an I/O buffer#define MAX_PENDING_IO_REQS4// The maximum # of I/Os// The completion key values indicate the type of completed I/O.#define CK_READ1#define CK_WRITE2//////////////////////////////////////////////////////////////////////////BOOL FileCopy(PCTSTR pszFileSrc, PCTSTR pszFileDst) {BOOL bOk = FALSE;// Assume file copy failsLARGE_INTEGERliFileSizeSrc = { 0 }, liFileSizeDst;try {{// Open the source file without buffering & get its sizeCEnsureCloseFile hFileSrc = CreateFile(pszFileSrc, GENERIC_READ,FILE_SHARE_READ, NULL, OPEN_EXISTING,FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED, NULL);if (hFileSrc.IsInvalid()) goto leave;// Get the file's sizeGetFileSizeEx(hFileSrc, &liFileSizeSrc);// Nonbuffered I/O requires sector-sized transfers.// I'll use buffer-size transfers since it's easier to calculate.liFileSizeDst.QuadPart = chROUNDUP(liFileSizeSrc.QuadPart, BUFFSIZE);// Open the desination file without buffering & set its sizeCEnsureCloseFile hFileDst = CreateFile(pszFileDst, GENERIC_WRITE,0, NULL, CREATE_ALWAYS,FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED, hFileSrc);if (hFileDst.IsInvalid()) goto leave;// File systems extend files synchronously. Extend the destination file// now so that I/Os execute asynchronously improving performance.SetFilePointerEx(hFileDst, liFileSizeDst, NULL, FILE_BEGIN);SetEndOfFile(hFileDst);// Create an I/O completion port and associate the files with it.CIOCP iocp(0);iocp.AssociateDevice(hFileSrc, CK_READ);// Read from source fileiocp.AssociateDevice(hFileDst, CK_WRITE);// Write to destination file// Initialize record-keeping variablesCIOReq ior[MAX_PENDING_IO_REQS];LARGE_INTEGER liNextReadOffset = { 0 };int nReadsInProgress = 0;int nWritesInProgress = 0;// Prime the file copy engine by simulating that writes have completed.// This causes read operations to be issued.for (int nIOReq = 0; nIOReq < _countof(ior); nIOReq++) {// Each I/O request requires a data buffer for transferschVERIFY(ior[nIOReq].AllocBuffer(BUFFSIZE));nWritesInProgress++;iocp.PostStatus(CK_WRITE, 0, &ior[nIOReq]);}BOOL bResult = FALSE;// Loop while outstanding I/O requests still existwhile ((nReadsInProgress > 0) || (nWritesInProgress > 0)) {// Suspend the thread until an I/O completesULONG_PTR CompletionKey;DWORD dwNumBytes;CIOReq* pior;bResult = iocp.GetStatus(&CompletionKey, &dwNumBytes, (OVERLAPPED**)&pior, INFINITE);switch (CompletionKey) {case CK_READ:// Read completed, write to destinationnReadsInProgress--;bResult = pior->Write(hFileDst);// Write to same offset read from sourcenWritesInProgress++;break;case CK_WRITE:// Write completed, read from sourcenWritesInProgress--;if (liNextReadOffset.QuadPart < liFileSizeDst.QuadPart) {// Not EOF, read the next block of data from the source file.bResult = pior->Read(hFileSrc, &liNextReadOffset);nReadsInProgress++;liNextReadOffset.QuadPart += BUFFSIZE;// Advance source offset}break;}}bOk = TRUE;}leave:;}catch (...) {}if (bOk) {// The destination file size is a multiple of the page size. Open the// file WITH buffering to shrink its size to the source file's size.CEnsureCloseFile hFileDst = CreateFile(pszFileDst, GENERIC_WRITE,0, NULL, OPEN_EXISTING, 0, NULL);if (hFileDst.IsValid()) {SetFilePointerEx(hFileDst, liFileSizeSrc, NULL, FILE_BEGIN);SetEndOfFile(hFileDst);}}return (bOk);}//////////////////////////////////////////////////////////////////////////BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam) {chSETDLGICONS(hWnd, IDI_FILECOPY);// Disable Copy button since no file is selected yet.EnableWindow(GetDlgItem(hWnd, IDOK), FALSE);return (TRUE);}//////////////////////////////////////////////////////////////////////////void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {TCHAR szPathname[_MAX_PATH];switch (id) {case IDCANCEL:EndDialog(hWnd, id);break;case IDOK:// Copy the source file to the destination file.Static_GetText(GetDlgItem(hWnd, IDC_SRCFILE),szPathname, _countof(szPathname));SetCursor(LoadCursor(NULL, IDC_WAIT));chMB(FileCopy(szPathname, TEXT("FileCopy.cpy"))? "File Copy Successful" : "File Copy Failed");break;case IDC_PATHNAME:OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 };ofn.hwndOwner = hWnd;ofn.lpstrFilter = TEXT("*.*\0");lstrcpy(szPathname, TEXT("*.*"));ofn.lpstrFile = szPathname;ofn.nMaxFile = _countof(szPathname);ofn.lpstrTitle = TEXT("Select file to copy");ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST;BOOL bOk = GetOpenFileName(&ofn);if (bOk) {// Show user the source file's sizeStatic_SetText(GetDlgItem(hWnd, IDC_SRCFILE), szPathname);CEnsureCloseFile hFile = CreateFile(szPathname, 0, 0, NULL,OPEN_EXISTING, 0, NULL);if (hFile.IsValid()) {LARGE_INTEGER liFileSize;GetFileSizeEx(hFile, &liFileSize);// NOTE: Only shows bottom 32bits of sizeSetDlgItemInt(hWnd, IDC_SRCFILESIZE, liFileSize.LowPart, FALSE);}}EnableWindow(GetDlgItem(hWnd, IDOK), bOk);break;}}//////////////////////////////////////////////////////////////////////////INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {switch (uMsg) {chHANDLE_DLGMSG(hWnd, WM_INITDIALOG, Dlg_OnInitDialog);chHANDLE_DLGMSG(hWnd, WM_COMMAND, Dlg_OnCommand);}return FALSE;}//////////////////////////////////////////////////////////////////////////int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int) {DialogBox(hInstExe, MAKEINTRESOURCE(IDD_FILECOPY), NULL, Dlg_Proc);return 0;}
- 《Windows核心编程》读书笔记十 同步设备I/O与异步设备I/O
- Windows核心编程:同步设备I/O与异步设备I/O
- windows核心编程--同步设备I/O与异步设备I/O
- 【读书笔记】同步设备I/O与异步设备I/O
- Windows核心编程学习笔记(20)--同步设备I/O与异步设备I/O1
- Windows核心编程学习笔记(21)--同步设备I/O与异步设备I/O2
- Windows核心编程(十)异步设备I/O
- 《Windows核心编程 5th》部分读书笔记----第10章 同步设备I/O与异步设备I/O
- 十、同步设备I/O与异步设备I/O(I/O完成端口)
- <<Windows核心编程(第五版)>>第十章同步设备I/O与异步设备I/O:10.5接收I/O请求完成通知
- Windows核心编程(九)同步设备I/O
- 《Windows Via C/C++》学习之同步设备I/O与异步设备I/O
- 异步设备I/O
- 第十章:同步设备I/O与异步设备I/O
- 第十章:同步设备I/O与异步设备I/O(二)
- I/O Concepts(3) : 同步设备I/O 和 异步设备I/O
- Linux设备驱动中的异步通知与同步I/O
- Chapter10-“I/O设备的同步和异步”之I/O设备同步操作
- word插入分页符后在下一页开始出现一个回车符,这个回车符如何删掉,但不影响分页
- HTML 学习笔记1 ——html样式
- 局域网内建立git服务器,并实现不同主机的代码共享(无需通过github网站)
- ITK问题记录之SetFileName()
- 环境变量
- 《Windows核心编程》读书笔记十 同步设备I/O与异步设备I/O
- Spring Cloud中,如何使用Feign构造多参数的请求
- springboot项目以war包形式部署
- JS深度克隆变量
- 海康NVR设备RTSP协议转RTMP协议
- Android-JobScheduler
- vector【Template】
- c#继承
- 异步加载