《Windows via C/C++》学习笔记(六) 设备I/O

本文详细介绍了Windows系统中设备输入输出的基本概念,包括同步和异步设备I/O的工作原理及其实现方法。重点讲解了使用CreateFile函数打开各种设备的方法,并深入探讨了文件设备的特性及其管理。

《Windows via C/C++》学习笔记 —— 设备I/O之“设备的打开和关闭”

设备的输入输出,即设备I/O,可以分为“同步”和“异步”两种方式。同步的设备I/O,调用的API函数总是等到设备I/O完成才返回。而异步的设备I/O,可以通过多种方法来实现,但是其根本原理是得到“设备I/O的完成通知”。

 

  本篇主要讨论如何打开和关闭一个设备。注意,这里的设备,不是指像键盘、显示器那种实体。而是一种抽象的概念,指一种与外界通信的对象,可以接受外界的输入,也可以对外界的请求作出响应,称之为设备I/O。这个概念比较抽象,这些设备往往和某个内核对象关联。要打开这些设备,就要创建相关的内核对象。

  这些设备包括文件、目录、逻辑磁盘驱动、物理磁盘驱动、串行端口、并行端口、邮槽、管道、套接字、控制台(如下表):

设备

主要用途 

文件

保存数据

目录

属性和文件压缩设置

逻辑磁盘驱动

磁盘格式化

物理磁盘驱动

访问分区表

串行端口

串行传输数据

并行端口

多位数据同时传输,主要是将数据传输给打印机

邮槽

一对多传输数据,往往适用于一个网络中的一台计算机向其他机器发送数据

命名管道

一对一传输数据,往往适用于一个网络中的一台计算机向其他机器发送数据

匿名管道

一对一传输数据,适用于简单的数据传输,不适用于网络

套接字

以流或数据报的形式发送数据,适用于一个网络中的通信

控制台

一个文字窗口显示缓冲区

 

  要使用这些设备,你首先应该打开这些设备。

  Windows努力隐藏这些设备的差异,所以,很多设备的打开的I/O工作可以通过同一个API函数完成,如下表:

设备

经常用来打开设备的API函数和用法

文件

CreateFile —— 打开设备的函数。

将参数pszName是一个文件路径名。

目录

CreateFile —— 打开设备的函数。

将参数pszName是一个目录名。Windows允许你打开一个目录,通过使用参数FILE_ FLAG_BACKUP_SEMANTICS旗标来呼叫CreateFile函数。打开目录之后,就可以这是目录属性,即文件夹属性,比如正常、隐藏、系统、只读等。

逻辑磁盘驱动

CreateFile —— 打开设备的函数。

将参数pszName设置为字符串“\\.\x:”。 比如要打开C盘,就将其设置为“\\.\C:”。

物理磁盘驱动

CreateFile —— 打开设备的函数。

将参数pszName设置为“\\.\PHYSICALDRIVEx”。比如打开第一个物理硬盘扇区:可以这么调用CreateFile函数:

CreateFile(TEXT("\\.\PHYSICALDRIVE0"), ...);

这样就可以打开一个物理磁盘驱动,并且可以直接访问硬盘分区表。

但是打开物理磁盘驱动是存在潜在危险的,特别是当错误的写入,会造成物理磁盘内容的破坏。

串行端口

CreateFile —— 打开设备的函数。

将参数pszName设置为“COMx”,比如打开COM1串口设备,只要将其设置为“COM1”。

并行端口

CreateFile —— 打开设备的函数。

将参数pszName设置为“LPTx”,比如打开LPT1并行端口,将其设置为“LPT1”。

邮槽(服务器端)

CreateMailslot —— 打开设备的函数。

将参数pszName设置为“\\.\mailslot\mailslotname”,其中,“mailsoltname”是为邮槽取的名字,可以任意,前面的字符串是固定的。

邮槽(客户端)

CreateFile —— 打开设备的函数。

将参数pszName设置为“\\servername\mailslot\mailslotname”,其中,“mailsoltname”是为邮槽取的名字,可以任意,前面的字符串是固定的。

命名管道

(服务器端)

CreateFile —— 打开设备的函数。

将参数pszName设置为“\\.\pipe\pipename”,其中,“pipename”是为命名管道取的名字,可以任意,前面的字符串是固定的。

命名管道

(客户端)

CreateFile —— 打开设备的函数。

将参数pszName设置为“\\servername\pipe\pipename”,其中,“pipename”是为命名管道取的名字,可以任意,前面的字符串是固定的。

匿名管道

CreatePipe —— 打开设备的函数。

无论是客户端还是服务器端都以该函数创建或打开匿名管道。

套接字

socket —— 创建一个套接字描述符accept, or AcceptEx.

控制台

Console

CreateConsoleScreenBuffer,GetStdHandle —— 打开设备的函数

 

  从上表可以发现,很多设备都以CreateFile函数来创建和打开。这个函数以后会讲。

  打开了设备,你得到了一个设备的句柄,你就可以通过该句柄使用其他函数,来对相关设备进行设置。

  比如,现在打开了一个串行端口,然后要设置它的传输波特率:

BOOL SetCommConfig(
   HANDLE       hCommDev,
   LPCOMMCONFIG pCC,
   DWORD        dwSize);

 

  或者,你获得了一个邮槽句柄,可以设置读取数据的等待时间:

BOOL SetMailslotInfo(
   HANDLE hMailslot,
   DWORD  dwReadTimeout);

 

  最后,不要忘记关闭句柄,从而正确地关闭设备:

BOOL CloseHandle(HANDLE hObject);
int  closesocket(SOCKET s);    // 套接字的关闭

 

  如果你有了一个设备句柄,你可以调查它的设备类型,通过使用GetFileType函数,该函数的返回值表明了它是一个什么类型的设备,可以参考MSDN。

DWORD GetFileType(HANDLE hDevice);

 

  好了,现在让我们来讨论一下CreateFile函数:

复制代码
HANDLE CreateFile(
   PCTSTR pszName,     
// 指明设备类型或一个特定的设备实体
   DWORD dwDesiredAccess,      // 访问限制
   DWORD dwShareMode,          // 共享方式
   PSECURITY_ATTRIBUTES psa,      // 安全描述结构
   DWORD dwCreationDisposition,   // 创建和打开方式
   DWORD dwFlagsAndAttributes,    // 属性旗标,与缓冲区和文件操作属性有关
   HANDLE hFileTemplate);         // 设备模版,一个设备句柄
复制代码

 

  该函数成功,返回句柄,失败返回INVALID_HANDLE_VALUE(值为-1)。

  如果设置了最后一个参数hFileTemplate,那么就照着这个参数所代表的设备,创建一个属性相同的设备,当然,这个参数所表示的设备要具有“可读”的权限,即有GENERIC_READ访问权限。

  至于该函数的具体用法,可以参看本书或MSDN。

《Windows via C/C++》学习笔记 —— 设备I/O之“文件设备”

本来不打算写这篇的,但是文件的重要性大家都知道。在设备I/O中,有一种设备叫文件设备,这是一个抽象的概念,就把它理解为文件就行了。

  文件设备,可以通过CreateFile函数打开,得到一个文件对象句柄。

 

  在文件中,有两个比较重要的属性:

1、文件大小:在32位中最大为4GB,64位中可以达到16EB。

2、文件读写指针:这个指针表明读写位置,大小范围可以超出文件的大小。

 

  先讨论文件的大小。

  要得到文件的大小,可以使用GetFileSizeEx函数:

BOOL GetFileSizeEx(
   HANDLE         hFile,     
// 文件对象句柄
   PLARGE_INTEGER pliFileSize);   // LARGE_INTEGER联合的指针,返回大小

 

  这个函数接受一个LARGE_INTEGER联合的指针,用来返回文件大小,这个结构可以表示64位值:

复制代码
typedef union _LARGE_INTEGER {
   
struct  {
      DWORD LowPart;    
//  低32位值
      LONG HighPart;        //  高32位值

   };
   LONGLONG QuadPart;   
//  64位值得
} LARGE_INTEGER,  * PLARGE_INTEGER;
复制代码

 

  从这个定义可以看出,该联合可以用QuadPart表示一个64位值,也可以差分成两个32位值。这个联合有一个无符号数版本,叫做ULAGER_INTEGER联合,对应的3个成员都是保存的无符号数。

 

  还有一个函数可以得到一个文件的大小:

DWORD GetCompressedFileSize(
   PCTSTR pszFileName,          
// 文件路径名
   PDWORD pdwFileSizeHigh);  // 文件大小如果大于4GB,高32位值由该参数返回

 

  这个函数接受一个文件的路径名称,返回文件大小的低32位值,高32位值由参数pdwFileSizeHigh返回。与GetFileSizeEx不同的是,该函数返回一个文件的物理大小,而GetFileSizeEx返回文件的逻辑大小。

  比如一个文件大小为100KB,它被压缩为85KB,如果使用GetFileSizeEx,则返回100KB,使用GetCompressedFileSize则返回85KB。

  与GetFileSizeEx不同的是,该函数接受一个字符串,指明文件路径,这就可以直接查询某个文件大小,而不要先打开它获得它的句柄。

  可以如下使用该函数:

ULARGE_INTEGER ulFileSize;      // 与LARGE_INTEGER联合类似,保存无符号数
ulFileSize.LowPart  =  GetCompressedFileSize(TEXT( " SomeFile.dat " ),
   
& ulFileSize.HighPart);      // 取得当前目录下的SomeFile.dat文件大小


  这样,64位的文件大小存储在ulFileSize.QuadPart中。

 

  讨论完了文件的大小,下面来讨论文件读写指针。

  CreateFile函数创建或打开了一个文件内核对象,该内核对象中管理着一个“文件读写指针”。该指针指明了一个64位的偏移量。初始情况下,该指针设置为0,即你读取或写入数据的时候从文件开始处进行,即从偏移量为0的地方开始。每次读取或写入N字节的数据,系统更新该读写指针,使偏移量加上N个字节。比如下面代码反映了读取文件前100个字节的数据:

BYTE pbFirst[ 50 ], pbSecond[ 50 ];
DWORD dwNumBytes;
HANDLE hFile 
=  CreateFile(TEXT( " MyFile.dat " ), ...);  // 指针初始化为0
ReadFile(hFile, pbFirst,  50 & dwNumBytes, NULL);     // 读取第0~49字节
ReadFile(hFile, pbSecond,  50 & dwNumBytes, NULL); // 读取第50~99字节

 

  需要注意的是,一个文件对象句柄对应一个读写指针,如果一个文件被打开多次,那么就有多个文件对象,每个文件对象管理着一个读写指针,这些指针相互之间不影响。比如下面的代码:

复制代码
BYTE pb[ 10 ];
DWORD dwNumBytes;
HANDLE hFile1 
=  CreateFile(TEXT( " MyFile.dat " ), ...);  // 指针初始化为0
HANDLE hFile2  =  CreateFile(TEXT( " MyFile.dat " ), ...);  // 指针初始化为0
ReadFile(hFile1, pb,  10 & dwNumBytes, NULL);   // 读取第0~9字节
ReadFile(hFile2, pb,  10 & dwNumBytes, NULL);   // 也是读取第0~9字节
复制代码

 

  上面这段代码,hFile1和hFile2是同一个文件的两个不同的文件内核对象的句柄,这两个内核对象管理着两个不同文件指针,所以改变其中一个的读写指针,不会影响另一个。

  下面这段代码更能说明问题:

复制代码
BYTE pb[ 10 ];
DWORD dwNumBytes;
HANDLE hFile1 
=  CreateFile(TEXT( " MyFile.dat " ), ...);  // 读写指针初始化为0
HANDLE hFile2;      // 另一个文件句柄

// 将本进程内hFile1句柄值复制给本进程中的hFile2
DuplicateHandle(
   GetCurrentProcess(), hFile1,
   GetCurrentProcess(), 
& hFile2,
   
0 , FALSE, DUPLICATE_SAME_ACCESS);
ReadFile(hFile1, pb, 
10 & dwNumBytes, NULL);    // 读取第0~9字节
ReadFile(hFile2, pb,  10 & dwNumBytes, NULL);    // 读取第10~19字节
复制代码

 

  上面这段代码,使用DuplicateHandle函数复制句柄,使得两个句柄hFile1和hFile2共用同一个文件内核对象,因此读写指针也是共用的。

 

  可以使用SetFilePointerEx函数来定位文件读写指针:

BOOL SetFilePointerEx(
   HANDLE         hFile,     
// 文件内核对象句柄
   LARGE_INTEGER  liDistanceToMove,      // 64位数,移动字节数
   PLARGE_INTEGER pliNewFilePointer,     // 返回新的文件读写指针位置
   DWORD          dwMoveMethod);         // 移动方式


  该函数中dwMoveMethod告诉系统如何移动。FILE_BEGIN,表示从文件头开始移动;FILE_END,表示从文件尾往前移动;FILE_CURRENT,表示从当前读写指针位置移动。移动的位移量在第2个参数liDistaceToMove中。

 

  有几点需要注意

  • 将文件读写指针的位置设置为超过文件大小范围是合法的。这么做不会使得文件大小变大,除非调用函数SetEndOfFile。
  • 当打开文件使用函数CreateFile时,该函数的dwFlagsAndAttributes参数中包括FILE_FLAG_NO_BUFFERING,文件读写指针只能被设置为硬盘扇区的单位大小。
  • 没有GetFilePointerEx函数来取得当前文件指针位置,可以调用SetFilePointerEx函数来得到其位置,要把第二个参数设置为0,如下代码:
LARGE_INTEGER liCurrentPosition  =  {  0  };
SetFilePointerEx(hFile, liCurrentPosition,
                        
& liCurrentPosition,FILE_CURRENT);

 

  当文件被关闭的时候,系统会在文件上设置一个结束位置,以确定该文件的大小。当然,你也可以自己设置文件的结束位置,以此来改变文件的大小。使用SetEndOfFile函数:

BOOL SetEndOfFile(HANDLE hFile);

 

  该文件在当前的文件读写指针处设置文件的结束标志,来截断或扩展文件的大小。比如,你想设置一个文件的大小为1024字节的话,可以通过以下代码实现:

复制代码
HANDLE hFile  =  CreateFile(...);
LARGE_INTEGER liDistanceToMove;
liDistanceToMove.QuadPart 
=   1024 ;
// 设置文件指针
SetFilePointerEx(hFile, liDistanceToMove, NULL, FILE_BEGIN);
SetEndOfFile(hFile);     
// 在文件指针处设置结束标志
CloseHandle(hFile);
复制代码

《Windows via C/C++》学习笔记 —— 设备I/O之“同步的设备I/O”

前面曾经讲过,设备I/O的方式有两种:同步和异步。本篇介绍一下同步设备I/O。主要涉及到两个函数:ReadFile和WriteFile。

  不要被这两个函数的名称迷惑,不仅可以将这两个作用于文件,也可以作用于其他设备:比如管道、邮槽等。

 

  最简单的设备I/O,可以通过ReadFile和WriteFile这两个函数来实现:

复制代码
BOOL ReadFile(
   HANDLE      hFile,         
// 设备对象句柄
   PVOID       pvBuffer,       // 读取缓冲区
   DWORD       nNumBytesToRead,      // 读取的字节数
   PDWORD      pdwNumBytes,          // 返回实际读取的字节数
   OVERLAPPED *  pOverlapped);         // 重叠结构指针,仅在异步方式有用
复制代码
复制代码
BOOL WriteFile(
   HANDLE      hFile,
   CONST VOID  
* pvBuffer,
   DWORD       nNumBytesToWrite,
   PDWORD      pdwNumBytes,     
// 返回实际写入的字节数
   OVERLAPPED *  pOverlapped);
复制代码

 

  在同步方式下使用这2个函数进行设备I/O,在同步模式下,两个函数的最后一个参数pOverlapped都要设置为NULL。另外,必须要注意这一点:就是在用CreateFile创建或打开设备之时,其FLAG参数不能包括FLAG_FILE_OVERLAPPED,否则系统认为你想要异步地实现设备I/O。

  另外,ReadFile只能读取这些设备,即在使用CreateFile创建或打开设备的时候,该函数的FLAG参数中包括GENERIC_READ。而WriteFile只能写入这些设备,即使用CreateFile函数的时候,FLAG参数包括GENERIC_WRITE。

 

  邮槽、管道、文件、串行端口等设备是有自己的高速缓存的。如果在CreateFile函数的FLAG参数中没有包括FILE_FLAG_NO_BUFFERING,也就是可以将写入的数据暂存在缓冲区中,那么可以通过FlushFileBuffers来强行将暂存在与设备有关的缓冲区中的全部数据写入到设备中。

BOOL FlushFileBuffers(HANDLE hFile);

 

  同步方式的设备I/O实现简单,但是缺点也是明显的,就是会阻碍有关线程中的其他与设备I/O无关的操作。因为设备I/O函数直到设备I/O请求结束才返回,如果数据量大,很可能会阻碍其他无关的操作。

  为了解决这个问题,你应该尽量使用异步的设备I/O。但是可惜的是,Windows API中,没有为CreaetFile这个函数提供任何异步的方式来实现。Windows Vista提供了另一种方法:中途取消同步设备I/O。可以通过使用函数CancelSynchronousIo来取消一个线程之内的正在进行的同步设备I/O操作。

BOOL CancelSynchronousIo(HANDLE hThread);      // 参数是线程句柄

 

  该函数接受一个线程句柄,该句柄是一个正在等待同步设备I/O操作完成的线程的句柄。该句柄在创建或打开的时候,必须具有THREAD_TERMINATE操作权限。如果你调用CreateThread或_beginthreadex函数来创建线程,那么返回的线程句柄就包含THREAD_TERMINATED的操作权限。如果使用OpenThread函数来获得一个已创建的线程的句柄,那么就传递THREAD_TERMINATED给dwDesiredAccess参数(第1个参数)。如果没有设置该权限,那么CancelSynchronousIo返回FALSE,调用GetLastError返回ERROR_ACCESS_DENIED(访问拒绝错误)。

  如果线程已经结束了等待设备I/O,那么再调用CancelSynchronousIo函数会返回TRUE,而不是FLASE,调用GetLastError则返回ERROR_OPERATION_ABORTED(操作失败错误)。

  如果线程并不是等待在设备I/O的返回上,那么调用该函数会返回FALSE,随后调用GetLastError会返回ERROR_NO_FOUND(未找到的错误)。

《Windows via C/C++》学习笔记 —— 设备I/O之“异步设备I/O请求”

异步设备I/O适用于大数据量和高性能的场合,比如服务器。

  要使用异步设备I/O,在调用CreateFile来打开或创建一个设备的时候,让参数dwFlagsAndAttributes包括FILE_FALG_OVERLAPPED,这意味着想让打开的设备可以被异步访问。

  为了发送一个I/O请求给一个设备,也就是让一个I/O请求进入I/O队列,你可以使用ReadFile和WriteFile这两个函数:

复制代码
BOOL ReadFile(
   HANDLE      hFile,
   PVOID       pvBuffer,
   DWORD       nNumBytesToRead,
   PDWORD      pdwNumBytes,
   OVERLAPPED
*  pOverlapped);
复制代码
复制代码
BOOL WriteFile(
   HANDLE      hFile,
   CONST VOID  
* pvBuffer,
   DWORD       nNumBytesToWrite,
   PDWORD      pdwNumBytes,
   OVERLAPPED
*  pOverlapped);
复制代码

 

  当这两个函数被呼叫,系统通过第一个参数hFile,来查看该句柄指明的设备在打开的时候是否使用了FILE_FLAG_OVERLAPPED,如果使用了,这两个函数执行异步设备I/O,反之,则执行同步设备I/O。当使用异步I/O方式的时候,在调用这两个函数的时候,可以将NULL传递给pdwNumBytes参数,因为不知道何时设备I/O完成,因此使用这个参数没有多大意义。

  注意最后一个参数,是一个OVERLAPPED结构的指针:

复制代码
typedef  struct  _OVERLAPPED {
   DWORD  Internal;     
//  错误代码(出口参数,返回)
   DWORD  InternalHigh;  //  传输的数据大小,以字节为单位(出口参数,返回)
   DWORD  Offset;        //  低32位偏移量(入口参数,输入)
   DWORD  OffsetHigh;    //  高32位偏移量(入口参数,输入)
   HANDLE hEvent;        //  事件内核对象句柄(入口参数,输入)
} OVERLAPPED,  * LPOVERLAPPED;
复制代码

 

  该结构包含5个成员,其中的3个——Offset、OffsetHigh、hEvent应该在调用ReadFile和WriteFile之前被初始化,另外的2个——Internal、InternalHigh会在I/O完成的时候被设备驱动程序所设置,下面细述一下:

  • Offset、OffsetHigh —— 在使用异步设备I/O来操纵“文件设备”的时候,文件读写指针被忽略,此时I/O的偏移量由OVERLAPPED结构中的Offset和OffsetHigh决定。另外,在“非文件设备”中,这两个成员不会被忽略,一般必须要设置为0。
  • hEvent —— 一个事件内核对象句柄,可以有多种使用方法,后面会讲到。
  • Internal —— 保存I/O错误码,当你发送一个I/O请求的时候,该参数被设置为STATUS_PENDING,指明没有错误发生,因为操作还没有开始。你可以使用HasOverlappedIoCompleted宏来查看一个异步设备I/O是否完成,该结构接受一个OVERLAPPED结构指针,如果I/O请求完成返回TRUE。如果I/O请求仍然没有开始,返回FALSE。
  • InternalHigh —— 异步I/O请求完成的时候,该成员里保存了传送数据量的字节数。

  当异步I/O请求完成之后,你可以接受到一个OVERLAPPED结构的指针。一般可以让一个C++类从OVERLAPPED结构派生,类中加入一些其他信息,使得更容易处理。然后当使用ReadFile和WriteFile函数的时候,可以传递这个C++类对象的指针,当I/O完成之后,接受该结构的时候,可以将其转换为C++类对象,不但可以获得其5个成员,还可以获得类中的其他信息。

 

  使用异步设备I/O的时候,要注意以下三点:

  1、设备驱动程序不一定会按照一个“先进先出”(FIFO)的顺序来处理设备I/O请求,因此如下编码不会保证先读后写:

OVERLAPPED o1  =  {  0  };
OVERLAPPED o2 
=  {  0  };
BYTE bBuffer[
100 ];
ReadFile (hFile, bBuffer, 
100 , NULL,  & o1);    //
WriteFile(hFile, bBuffer,  100 , NULL,  & o2);    //

 

  2、以异步的方式进行I/O请求的是,驱动程序可能会选择同步的方式。当你读取一个文件的时候,如果系统发现读取的数据在cache中,且数据有效,那么该I/O请求就不需要驱动程序了,而是直接将cache中的数据复制到你的缓冲区中。驱动在某些操作上一直使用同步方式,比如在NTFS格式上的文件压缩,扩展文件长度,添加文件信息等。

  这个时候,如果ReadFile和WriteFile返回非0值,则表明它以同步方式进行。如果返回FLASE,说明发生了一个错误,这个时候可以通过GetLastError来取得信息,如果返回ERROR_IO_PENDING,则说明I/O请求成功提交,但没有完成。

 

  3、数据缓冲区和OVERLAPPED结构在异步I/O请求完成之前不能被移动或释放。当设备驱动准备处理你的I/O请求的时候,它将数据传送到pvBuffer参数对应的地址上去,并访问OVERLAPPED结构中的Offset等成员。当I/O请求完成之后,设备驱动更新OVERLAPPED结果中的Internal和InternalHigh成员。因此,不能在I/O请求完成之前移动或释放数据缓冲区和OVERLAPPED结构,否则,内存数据会被破坏,而且在每次调用ReadFile或WriteFile的时候,都必须分配一个单独的OVERLAPPED结构。

  比如,下面的代码是有BUG的:

复制代码
VOID ReadData(HANDLE hFile)
{
   OVERLAPPED o 
=  {  0  };
   BYTE b[
100 ];
   ReadFile(hFile, b, 
100 , NULL,  & o);
}  
// 此时缓冲区b和OVERLAPPED结构o都被释放
复制代码

 

  你可以将一个设备I/O请求取消排队,即撤消该请求。可以有如下方法:

 

  1、在一个线程中调用CancelIo函数,可以取消该线程发送给指定设备有关的所有I/O请求,除了指定的设备是“I/O完成端口”。

BOOL CancelIo(HANDLE hFile);      // 参数是设备对象句柄

 

  2、取消与一个设备有关的所有I/O请求,关闭这个设备句柄即可。

 

  3、当一个线程结束,系统自动取消该线程发送的所有I/O请求,除了发送给“I/O完成端口的”I/O请求。

 

  4、如果想取消某一个特定的I/O请求,可以使用CancelIoEx函数,传递一个OVERLAPPED结构指针给它:

BOOL CancelIoEx(HANDLE hFile, LPOVERLAPPED pOverlapped);

 

  该函数可以跨线程使用,也就是在T1线程内发送的I/O请求,可以在T2线程内通过该函数结束之。因为每个I/O请求都需要一个唯一的OVERLAPPED结构,所以该OVERLAPPED结构就标识了一个I/O请求。如果传递NULL给CancelIoEx函数的第2个参数,那么就会取消与hFile对应的设备的所有I/O请求。

 

  取消一个I/O请求,该I/O请求会结束,同时错误码被设置为ERROR_OPERATION_ABORTED。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值