1500字范文,内容丰富有趣,写作好帮手!
1500字范文 > 《Windows核心编程》学习笔记(10)– 同步设备I/O与异步设备I/O

《Windows核心编程》学习笔记(10)– 同步设备I/O与异步设备I/O

时间:2019-07-21 11:54:26

相关推荐

《Windows核心编程》学习笔记(10)– 同步设备I/O与异步设备I/O

1.打开和关闭设备

Windows的优势之一是它所支持的设备数量。就我们的讨论而言,我们把设备定义为能够与之进行通信的任何东西。表1列出了一些设备及其常见用途。

表1:各种设备及其常见用途

表2:用来打开各种设备的函数

CreateFile函数当然可以用来创建和打开磁盘文件,它同样可以打开许多其它设备:

HANDLECreateFile(

LPCTSTRlpFileName,//即表示设备的类型,也表示该类设备的某个实例;

DWORDdwDesiredAccess,//指定我们想以何种方式来和设备进行数据传输;

DWORDdwShareMode,//指定设备共享特权;

LPSECURITY_ATTRIBUTESlpSecurityAttributes,

DWORDdwCreationDisposition,

DWORDdwFlagsAndAttributes,

HANDLEhTemplateFile

);

PS:

1.调用CreateFile函数来打开文件之外的其他设备时,必须将OPEN_EXISTING传给dwCreationDisposition参数

2.大多数以句柄为返回值的Windows函数在失败的时候会返回NULL。但是,CreateFile返回INVALID_HANDLE_VALUE。

3.在处理非常大的文件时,高速缓存管理器可能无法分配它所需的内部数据结构,从而导致打开文件失败。为了访问非常大的文件,我们必须用dwCreationDisposition参数赋予FILE_FLAG_NO_BUFFERING标志来打开文件。

2.使用文件设备

1.在使用文件的时候,我们经常需要得到文件的大小。要达到这一目的,最简单的方法是调用GetFileSizeEx:

BOOL GetFileSizeEx(

HANDLE hFile,

PLARGE_INTEGER pliFileSize);

可以用来取得文件大小的另一个非常有用的函数是GetCompressedFileSize:

DWORD GetCompressedFileSize(

PCTSTR pszFileName,

PDWORD pdwFileSizeHigh);

这个函数返回的是文件的物理大小,而GetFileSizeEx返回的是文件的逻辑大小。例如:假设一个100KB的文件经过压缩之后占用85KB,调用GetFileSizeEx后返回100KB, 调用GetCompressedFileSize返回85KB。

GetCompressedFileSize函数通过一种不通寻常的方式来返回64位的文件大小:文件大小的低32位是函数的返回地址,文件大小的高32位值被放在pdwFileSizeHigh参数指向的DWORD中。

例如:

ULARGE_INTEGER ulFileSize;

ulFileSize.LowPart = GetCompressedFileSize(TEXT("SomeFile.dat"), &ulFileSize.HighPart);

2.调用CreateFile会使系统创建一个文件内核对象来管理对文件的操作。在这个内核对象内部有一个文件指针,它是一个64位偏移量,表示应该在哪里执行下一次同步读取或写入操作。

例如:

BYTE pb[10];

DWORD dwNumBytes;

HANDLE hFile = CreateFile(TEXT("MyFile.dat"), ...); // Pointer set to 0

ReadFile(hFile, pb, 10, &dwNumBytes, NULL); // Reads bytes 0 - 9

ReadFile(hFile, pb, 10, &dwNumBytes, NULL); // Reads bytes 10 - 19

手动设置文件指针的位置:

BOOL SetFilePointerEx(

HANDLE hFile,

LARGE_INTEGER liDistanceToMove,

PLARGE_INTEGER pliNewFilePointer,

DWORD dwMoveMethod);

设置文件尾

BOOL SetEndOfFile(HANDLE hFile);

例如:想强制设置文件的大小1024字节

HANDLE hFile = CreateFile(….);

LARGE_INTEGER liDistanceToMove;

liDistanceToMove.QuadPart = 1024;

SetFilePointerEx(hFile, liDistanceToMove, NULL, FILE_BEGIN);

SetEndOfFile(hFile);

CloseHandle(hFile);

3.执行同步设备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);

执行同步I/O的时候,最后一个参数pOverlapped应该被设为NULL。

如果我们想要强制系统将缓存数据写入到设备,将数据刷新至设备:

BOOL FlushFileBuffers(HANDLE hFile);

windows vista中同步I/O的取消:

BOOL CancelSynchronousIo(HANDLE hThread);

参数hThread 是由于等待同步I/0请求完成而被挂起的线程的句柄,这个句柄必须是用THREAD_TERMINATE访问权限创建的。

4.异步设备I/O基础

1.与计算机执行的大多数其它操作相比,设备I/O是其中最慢、最不可预测的操作之一。但是,使用异步设备I/O使我们能够更好地使用资源并创建出更高效的应用程序。

假 设一个线程向设备发出一个异步I/O请求。这个I/O请求被传给设备驱动程序,后者负责完成实际的I/O操作。当驱动程序在等待设备响应的时候,应用程序 的线程并没有因为要等待I/O请求完成而被挂起,线程会继续运行并执行其它有用的任务。到了某一时刻,设备驱动程序完成了对队列中的I/O请求的处理,这 时它必须通知应用程序数据已发送,数据已收到,或发生了错误。

把异步I/O请求加入队列是设计高性能、可伸缩好的应用程序的本质所在。

为了以异步的方式来访问设备,我们必须在调用CreateFile时在dwFlagsAndAttributes参数中指定 FILE_FLAG_OVERLAPPED标志来打开设备。这个标志告诉系统我们想要以异步的方式来访问设备。为了将I/O请求加入设备驱动程序的队列 中,我们必须使用ReadFile和WriteFile函数。

当我们调用这两个函数的任何一个时,函数会检查hFile参数标识的设备是否是用FILE_FLAG_OVERLAPPED标志打开的。如果打开设备时指定了这个标志,那么函数会执行异步设备I/O。顺便提一下,当调用者两个函数来进行异步I/O的时候,我们可以(也通常会)传NULL给 pdwNumBytes参数。毕竟我们希望这两个函数在I/O请求完成之前就返回,因此这时就检查已经传输的字节数是没有意义的。

在执行异步设备I/O的时候,我们必须在ReadFile和WriteFile函数的pOverlapped参数中传入一个已初始化的OVERLAPPED结构。“overlapped”在这里的意 思是执行I/O请求的时间与线程执行其它任务的事件是重叠的(overlapped)。下面是OVERLAPPED结构的定义:

typedef struct _OVERLAPPED {

DWORD Internal; // [out] Error code

DWORD InternalHigh; // [out] Number of bytes transferred

DWORD Offset; // [in] Low 32-bit file offset

DWORD OffsetHigh; // [in] High 32-bit file offset

HANDLE hEvent; // [in] Event handle or data

} OVERLAPPED, *LPOVERLAPPED;

这个结构包含5个成员。其中的三个成员(即Offset,OffsetHigh,hEvent)必须在调用ReadFile和WriteFile之前进行初始化,其它两个成员(Internal,InternalHigh)由驱动程序来设置,当I/O操作完成的时候我们可以检查它们的值。

Offset,OffsetHigh:

这两个成员构成一个64位的偏移量,它们表示当访问文件的时候应该从哪里开始进行I/O操作。在执行异步I/O操作的时候,系统会忽略文件指针。为了避免在 对同一个对象进行多个异步调用的时候出现混淆,所有异步I/O请求必须在OVERLAPPED结构中指定起始偏移量。

注意:非文件设备会忽略Offset,OffsetHigh——我们必须将这两个成员都初始化为0,否则I/O请求会失败,这是调用GetLastError会返回ERROR_INVALID_PARAMETER。

hEvent:

在接收I/O完成通知的方法—使用I/O完成端口时会用到这个成员。当使用可提醒I/O通知函数时,许多开发人员会在hEvent中保存一个C++对象的地址。

Internal:

这个成员用来保存已处理的I/O请求的错误码。一旦发出一个异步I/O请求,设备驱动程序会立即将Internal设为STATUS_PENDING,表示没有错误,因为操作尚未开始。WinBase.h中定义的HasOverlappedIoCompleted宏允许我们呢检查一个异步I/O操作是否已经完成。如果请求还处在等待状态,那么该宏会返回FALSE,如果I/O请求已经完成,那么该宏会返回TRUE。

#define HasOverlappedIoCompleted(pOverlapped) \

((pOverlapped)->Internal != STATUS_PENDING)

InternalHigh:

当异步I/O请求完成的时候,这个成员用来保存已传输的字节数。

2.异步设备I/O的注意事项:

在执行异步I/O的时候,我们应该意识到一些问题。

首先,设备驱动程序不必以先入先出的方式来处理队列中的I/O请求。如果不按顺序来执行I/O请求能够提高性能,那么设备驱动程序一般都会这样做。例如, 为了降低磁头的移动和寻道时间,文件系统驱动程序可能会在I/O请求队列中寻找那些要访问的位置在物理硬盘撒谎那个相邻的请求。

其次,如何用正确的方式来检查错误。当我们试图将一个异步I/O请求添加到队列中的时候,设备驱动程序可能会选择以同步的方式来处理请求。当我们从文件中读取数据时,系统会检查我们想要的数据是否已经在系统的缓存中,这时可能发生这种情况。如果数据已经在缓存中,那么系统不会讲我们的I/O请求添加到设备驱动程序的队列中,而会将高速缓存中的数据复制到我们的缓存中,从而完成这个I/O操作。驱动程序总是会以同步的方式来执行某些操作,比如NTFS文件的压缩,增大文件的长度,或向文件追加信息。

如果请求的I/O操作时以同步方式执行的,那么ReadFile和WriteFile会返回非零值。如果请求的I/O操作时以异步方式执行的,或者在调用 ReadFile或WriteFile的时候发生了错误,那么这两个函数返回FALSE,我们必须调用GetLastError来检查到底发生了什么。如果返回的是ERROR_IO_PENDING,那么I/O请求已经被成功地加入了队列,会在晚些时候完成。如果返回的是ERROR_IO_PENDING以外的值,那么表示I/O请求无法被添加到设备驱动程序的队列中。

第三个问题是在异步I/O请求完成之前,一定不能移动或是销毁在发生I/O请求时所使用的数据缓存和OVERLAPPED结构。当系统将I/O请求加入设备驱动程序的队列中时,会将数据缓存的地址和OVERLAPPED结构的地址传给驱动程序。注意,传的只是地址而不是实际的数据块。这样做的原因是显而易 见的:内存复制是非常费时的,会浪费大量的CPU时间。

例如:

VOID ReadData(HANDLE hFile)

OVERLAPPED o ={0};

BYTE b[100];

ReadFile(hFile, b, 100, NULL,. &o);

这段代码看上去没有什么问题,但是当异步I/O请求被加入到队列只会,这个函数会返回。从而导致了位于线程栈上的缓存以及OVERLAPPED结构被释放。

3.取消队列中的设备I/O请求:

有时候,我们可能想要在设备驱动程序对一个已经加入队列的设备I/O请求进行处理之前将其取消。Windows提供了多种方式来达到这一目的:

1.我们可以调用CancelIo来取消由给定句柄所标识的线程添加到队列中的所有I/O请求:

BOOL CancelIo(HANDLE hFile);

2.我们可以关闭设备句柄,来取消已经添加到队列中的所有I/O请求,而不管它们是由哪个县城添加的;

3.当线程终止的时候,系统会自动取消该线程发出的所有I/O请求,但如果请求被发往的设备句柄具有与之相关联的I/O完成端口,那么它们不在被取消之列;

4.如果需要将发往给定文件句柄的一个指定的I/O请求取消,那么我们可以调用CancelIoEx:

BOOL CancelIoEx(HANDLE hFile, LPOVERLAPPED pOverlapped);

使用CancelIoEx,我们能够将调用线程之外的其它线程发出的待处理的I/O请求取消。CancelIoEx函数的调用应该只取消一个待处理的请求。如果pOverlapped为NULL,那么CancelIoEx会将hFile指定的设备的所有待处理I/O请求都取消掉。

5.接收I/O请求完成通知

Windows提供了4种不同的方法来接收I/O请求已经完成的通知。

表3:用来接收I/O完成通知的方法

触发设备内核对象:

一旦一个线程触发了一个异步I/O请求,该线程将会继续运行,以执行其它有用的任务。但即便如此,线程最终海华丝需要与I/O操作的完成状态进行同步。我 们会继续运行到线程代码中的一个点,在这个点上,除非设备数据已经被载入到缓存中,否则线程将无法执行后继操作。在Windows中设备内核对象可以用来 进行线程同步,因此对象既可能处于触发状态,也可能处于未触发状态。ReadFile和WriteFile函数在将I/O请求添加到队列之前,会先将设备 内核对象设为未触发状态,当设备驱动程序完成了请求之后,驱动程序会将设备内核对象设为触发状态。

线程可以通过嗲用WaitForSingleObject或WaitForMultipleObjects来检查一个异步I/O请求是否已经完成。

下面一个简单例子:

HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...); //指定以异步方式打开

BYTE bBuffer[100];

OVERLAPPED o = { 0 };

o.Offset = 345;

BOOL bReadDone = ReadFile(hFile, bBuffer, 100, NULL, &o); //bReadDone 指定I/O请求是不是以同步方式打开

DWORD dwError = GetLastError();

if (!bReadDone && (dwError == ERROR_IO_PENDING)) { //异步方式打开

// The I/O is being performed asynchronously; wait for it to complete

WaitForSingleObject(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 occurred; see dwError

}

触发事件内核对象:

上面描述的触发设备内核对象并不怎么用,因为它不能处理多个I/O请求。我们不能通过等待设备内核对象被触发的方式来对线程进行同步,这是因为一旦任何一个操作完成,该内核对象就会被触发。

OVERLAPPED结构的最后一个成员hEvent用来标识一个事件内核对象。我们必须通过CreateEvent来创建这个事件对象。当一个异步 I/O请求完成的时候,设备驱动程序会检查OVERLAPPED结构的hEvent成员是否为NULL。如果hEVent不为NULL,那么驱动程序会调 用SetEvent来触发事件。驱动程序仍然会像以前那样,将设备对象设为触发状态。但是,如果我们使用事件来检查一个设备操作是否已经完成,那么我们就不应该等待设备对象被触发,我们应该等待的是事件对象。

说明:向BOOL SetFileCompletionNotificationModes(HANDLE hFile, UCHAR uFlags);传入文件对象句柄和在I/O操作完成时的正常行为进行何种方式的定制。如果向参数uFlags传入 FILE_SKIP_SET_EVENT_ON_HANDLE,那么当文件操作完成时不会触发文件句柄。这可以略微提高点性能。

如 果想要同时执行多个异步设备I/O请求,我们必须为每个请求创建不同的事件对象,并初始化每个请求的OVERLAPPED结构中的hEvent成员,然后 再调用ReadFile或WriteFile。当运行到代码中的那个点,必须与I/O请求的完成状态进行同步的时候,我们只需要调用 WaitForMultipleObjects,并传入与每个待处理I/O请求的OVERLAPPED结构相关联的事件句柄。

可提醒I/O:

当系统创建一个线程的时候,会同时创建一个与线程相关联的队列。这个队列被称为异步过程调用(asynchronous procedure call,APC)队列。当发出一个I/O请求的时候,我们可以告诉设备驱动程序在调用线程的APC队列中添加一项。为了将I/O完成通知添加到线程的APC队列中,我们应该调用ReadFileEx和WriteFileEx函数:

BOOL ReadFileEx(

HANDLE hFile,

PVOID pvBuffer,

DWORD nNumBytesToRead,

OVERLAPPED* pOverlapped,

LPOVERLAPPED_COMPLETION_ROUTINE pfnCompletionRoutine);

BOOL WriteFileEx(

HANDLE hFile,

CONST VOID *pvBuffer,

DWORD nNumBytesToWrite,

OVERLAPPED* pOverlapped,

LPOVERLAPPED_COMPLETION_ROUTINE pfnCompletionRoutine);

首先,*Ex函数没有一个指向DWORD的指针作为参数来保存已传输的字节数,该信息只有回调函数才能得到。其次,*Ex函数要求我们传入一个回调函数的地址,这个回调函数被称为完成函数(completion routine)。

VOID WINAPI CompletionRoutine(

DWORD dwError,

DWORD dwNumBytes,

OVERLAPPED* po);

当 我们用ReadFileEx和WriteFileEx发出一个I/O请求的时候,这两个函数会将回调函数的地址传给设备驱动程序。当设备驱动程序完成I /O请求的时候,会在发出I/O请求的线程的APC队列中添加一项。该项包含了完成函数的地址,以及在发出I/O请求时所使用的OVERLAPPED结构 的地址。

当线程处于可提醒状态的时候,系统会检查它的APC队列,对队列中的每一项,系统会调用完成函数,并传入I/O错误码,已传输的字节数,以及OVERLAPPED结构的地址。Windows提供了6个函数,可以将线程置为可提醒状态:

DWORD SleepEx(

DWORD dwMilliseconds,

BOOL bAlertable);

DWORD WaitForSingleObjectEx(

HANDLE hObject,

DWORD dwMilliseconds,

BOOL bAlertable);

DWORD WaitForMultipleObjectsEx(

DWORD cObjects,

CONST HANDLE* phObjects,

BOOL bWaitAll,

DWORD dwMilliseconds,

BOOL bAlertable);

BOOL SignalObjectAndWait(

HANDLE hObjectToSignal,

HANDLE hObjectToWaitOn,

DWORD dwMilliseconds,

BOOL bAlertable);

BOOL GetQueuedCompletionStatusEx(

HANDLE hCompPort,

LPOVERLAPPED_ENTRY pCompPortEntries,

ULONG ulCount,

PULONG pulNumEntriesRemoved,

DWORD dwMilliseconds,

BOOL bAlertable);

DWORD MsgWaitForMultipleObjectsEx(

DWORD nCount,

CONST HANDLE* pHandles,

DWORD dwMilliseconds,

DWORD dwWakeMask,

DWORD dwFlags);

当我们调用以上6个函数之一并将线程置为可提醒状态时,系统会首先检查线程的APC队列。如果队列中至少有一项,那么系统不会让线程进入睡眠状态。

I/O完成端口:

I/O 完成端口背后的理论是并发运行的线程的数量必须有一个上限——也就是说,同时发出的500个客户请求不应该允许出现500个可运行的线程。如果能在应用程 序初始化的时候创建一个线程池,并让线程池中的线程在应用程序运行期间一直保持可用状态,那么服务应用程序的性能就能得到提高。I/O完成端口的设计初衷 就是与线程池配合使用。

I/O完成端口可能是最复杂的内核对象了。为了创建一个I/O端口,我们应该调用CreateIoCompletionPort:

HANDLE CreateIoCompletionPort(

HANDLE hFile,

HANDLE hExistingCompletionPort,

ULONG_PTR CompletionKey,

DWORD dwNumberOfConcurrentThreads);//I/O完成端口在同一时间最多能有多少线程处于可运行状态。0:默认CPU数量

这个函数执行两个不同的任务:它不仅会创建一个I/O完成端口,而且会将一个设备与一个I/O完成端口关联起来。若给 CreateIoCompletionPort的前三个参数分别传入INVALID_HANDLE_VALUE,NULL,0只创建I/O完成端口。

线程通过调用GetQueuedCompletionStatus来将自己切换到睡眠状态,来等待设备I/O请求完成并进入完成端口。

BOOL GetQueuedCompletionStatus(

HANDLE hCompletionPort,

PDWORD pdwNumberOfBytesTransferred,

PULONG_PTR pCompletionKey,

OVERLAPPED** ppOverlapped,

DWORD dwMilliseconds);

GetQueuedCompletionStatus的任务基本上就是将调用线程切换到睡眠状态,直到指定的完成端口的I/O完成队列中出现一项,或者等待的事件已经超出了(在dwMilliseconds参数中)指定的时间为止。

当满足以下条件时,会在列表中添加新项:

CreateIoCompletionPort被调用 当满足以下条件时,会将列表中的项删除:

设备句柄被关闭I/O完成队列(先入先出)

当满足以下条件时,会在列表中添加新项:

I/O请求完成PostQueuedCompletionStatus被调用 当满足以下条件时,会将列表中的项删除:

完成端口从等待线程队列中删除一项等待线程队列(后入先出)

当满足以下条件时,会在列表中添加新项:

线程调用GetQueuedCompletionStatus 当满足以下条件时,会将列表中的项删除:

I/O完成队列不为空,而且正在运行的线程数小于最大并发线程数(GetQueuedCompletionStatus会先从I/O完成队列中删除对应的项,接着将dwThreadId转移到已释放线程列表,最后函数返回)已释放线程列表

当满足以下条件时,会在列表中添加新项:

完成端口在等待线程队列中唤醒一个线程已暂停的线程被唤醒 当满足以下条件时,会将列表中的项删除:

线程再次调用GetQueuedCompletionStatus(dwThreadId再次回到等待线程队列)线程调用一个函数将自己挂起(dwThreadId转移到已暂停线程列表中)已暂停线程队列

当满足以下条件时,会在列表中添加新项:

已释放的线程调用一个函数将自己挂起 当满足以下条件时,会将列表中的项删除:

已挂起的线程被唤醒(dwThreadId回到已释放线程队列)图1:I/O完成端口的内部运作

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。