您的位置:首页 > 编程语言

Windows核心编程【10】小结

2012-05-05 17:48 246 查看

第十章 同步设备I/O与异步设备I/O

1、本章介绍的技术可以用来设计高性能、伸缩性好、响应性好,而且健壮的应用。一个伸缩性好的应用程序在处理大量并发操作时和处理少数并发操作时同样高效。

2、在Windows应用程序中,线程是最好的工具,可以用来对工作进行划分。

3、通过I/O完成端口,可以让线程在读取设备和写入设备的时候不必等待设备的响应,从而显著地提高吞吐量。

4、I/O完成端口最初被设计用来处理设备I/O,但这么多年来,MS已经构架出了越来越多能够非常好地适应I/O完成端口模型的操作系统设施。其中一个例子就是作业内核对象,它对进程进行监视并向I/O完成端口发送事件通知。

5、I/O完成端口也可以和设备I/O完全无关,简单地说,它是一种有无数用途的绝佳的线程间通信机制。

一、打开和关闭设备

1、Windows的优势之一是它所支持的设备数量。就这里讨论而言,把设备定义为能够与之进行通信的任何东西。各种设备及其常见用途:



2、本章将讨论应用程序如何与这些设备进行通信而不必等待设备响应。Windows极可能的对开发人员隐藏设备之间的差异。虽然不比过分关心设备的类型,但各个设备之间线程存在着差异。

3、下表列出了各种设备以及用来打开它们的函数



4、上表中的每个函数都返回一个用来标识设备的句柄,可以将该句柄传给许多函数来与设备进行通信。

5、在完成对设备的操作后,必须将其关闭。对大多数设备来说,最常用的CloseHandle;如果是套接字,那么closesocket。可以调用GetFileType来查处设备的类型。返回值如下表



6、CreateFile函数(MSDN更加详细也更新http://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx)当然可以用来创建和打开磁盘文件,但同样可以打开许多其他设备。其需要许多函数,这使得我们在打开设备的时候有相当大的灵活性。

7、调用CreateFile的时候,参数pszName即表示设备的类型,也表示该类设备的某个实例。

8、参数dwDesiredAccess用来指定我们想以何种方式来和设备进行数据传输。可以传入下表列出的4个普遍使用的标志。某些类型的设备还支持额外的访问控制标志。例如,当打开一个文件的时候,我们可以指定诸如FILE_READ_ATTRIBUTES之类的访问标志。



9、参数dwShareMode用来指定设备共享特权(device-sharing privilege)。当我们仍然打开着一个设备的时候(也就是说,我们尚未调用CloseHandle来关闭设备),该参数可以控制其他的CreateFile调用,能够以何种方式来打开设备。下表列出了一些可以的值:



10、在打开一个文件的时候,传入的路径名最长不超过MAX_PATH(在winDef.h中被定义为260)个字符。但是,调用CreateFileW(Unicode版本)并给路径名加上“\\?\”前缀,可以超越这个限制。CreateFileW会把前缀去除,它允许我们传入的路径名长度超过32000个Unicode字符。但需要注意的是,在使用这个前缀的时候,必须使用完整的路径名,系统不会对诸如“.”和“..”之类的相对路径进行处理。此外,路径中的每个独立的组成部分仍然不能超过MAX_PATH个字符。

11、参数psa指向SECURITY_ATTRIBUTES结构,用来指定安全信息以及我们是否希望CreateFile返回的句柄能够被继承。只有当我们在具备安全性的文件系统(比如NTFS)中创建文件的时候,才会用到结构内部的安全描述符,在其他所有情况下,该安全描述符会被忽略。

12、参数dwCreationDesposition对文件的含义更重大。下表列出了可选值。



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

13、dwFlagsAndAttributes参数有两个用途:其一,它允许我们设置一些标志来微调与设备之间的通信;其二,如果设备是一个文件,还能够设置文件的属性。这些通信标志中的大多数都是一个信号,用来告诉系统我们打算一何种方式来访问设备。这样系统就可以对缓存算法进行优化,来帮助我们提高应用程序的效率。

14、下面的CreateFile的高速缓存标志,主要关注文件系统对象。

a、FILE_FLAG_NO_BUFFERING,则合格标志表示在访问文件的时候不要使用任何数据缓存。一般情况下,文件数据的一部分会在内存中出现两次:高速缓存管理器有一个缓存,当我们在调用某些函数(比如ReadFile)的时候,我们自己会有一个缓存。系统高速缓存管理器的速度的提升是通过从文件中读取超出实际需要的数据量来达到的。如果我们不再从文件中读取数据,那么可能会浪费内存。通过指定FILE_FLAG_NO_BUFFERING标志,高速高速缓存管理器不要对任何数据进行缓存——我们会自己对数据进行缓存。这取决于我们正在做什么。则合格标志可以提高应用程序的性能和内存的使用效率。由于文件系统的设备驱动程序会讲文件数据直接写入到我们提供的缓存中,因此必

须遵循一定的规则:在访问文件的时候,使用的偏移量必须正好是磁盘卷的扇区大小的整数倍(用GetDiskFreeSpace函数来确定磁盘卷的扇区大小);读取/写入文件的字节数必须正好是扇区大小的整数倍;必须确保缓存在进程地址空间中的起始地址正好是扇区大小的整数倍。

b、FILE_FLAG_SEQUENTIAL_SCAN和FILE_FLAG_RANDOM_ACCESS,只有当我们允许系统对文件数据进行缓存的时候,这些标志才会有用。如果指定了FILE_FLAG_NO_BUFFERING标志,那么这两个标志都会被忽略。如果指定了FILE_FLAG_SEQUENTIAL_SCAN标志,那么系统会认为我们将顺序地访问文件。会先预读部分数据。但是如果我们要随机读取(会重新设置文件指针),那么还是指定FILE_FLAG_RANDOM_ACCESS标志。这标志是告诉系统不要提前读取文件数据。

c、为了对一个文件进行管理,高速缓存管理器必须为该文件保存一些内部数据结构——文件越大,所需的数据结构就越多。在出力非常大的文件时,高速缓存管理器可能无法分配它所需的内部数据结构,从而导致打开文件失败。为了访问非常大的文件,必须用FILE_FLAG_NO_BUFFERING标志来打开文件。

d、FILE_FLAG_WRITE_THROUGH,这是最后一个与高速缓存有关的标志。它禁止对文件写入操作进行缓存以减少数据丢失的可能性。当指定这个标志的时候,系统会将所有对文件的修改直接写入到磁盘中。但是,系统仍然会在内部的缓存中保存文件数据,这样文件读取操作会继续使用缓存中的数据(如果可供使用的话),而不必直接从磁盘中读取数据。如果用这个标志来打开网络服务器上的文件,那么只有在数据已经被写入服务器的磁盘之后,各个Windows文件写入函数才会返回到调用线程。

15、用来对高速缓存之外的各种行为进行定制。

a、FILE_FLAG_DELETE_ON_CLOSE,使用这个标志可以让文件系统在文件所有的句柄都被关闭后,删除该文件。这个标志通常和FILE_ATTRIBUTES_TEMPORARY属性一起使用。当这两个标志一起使用的时候,应用程序可以创建一个临时文件,想文件中写入数据,从文件中读取数据,最后关闭文件。当关闭文件的时候,系统胡i自动删除该文件。

b、FILE_FLAG_BACKUP_SEMANTICS,这个标志一般用于备份和恢复软件。在打开或创建人和文件之前,为了确保试图打开文件或创建文件的进程具有所需的访问特权,系统通常会执行安全性检查。但是,备份和恢复软件有一定的特殊性,它们会跳过某些文件安全性检查。当指定该标志的时候,系统会检查调用者的存取令牌(access token)是否具备对文件和目录进行备份/恢复的特权。如果调用者具备相应的特权,那么系统会允许它打开文件。也可以用该标志来打开一个目录的句柄。

c、FILE_FLAG_POSIX_SEMANTICS,区分文件大小写的方式来查找文件名。

d、FILE_FLAG_OPEN_REPARSE_POINT,该标志告诉系统忽略文件的重解析属性(即reparse attribute,如果有的话)。重解析属性允许一个文件系统过滤器对打开文件、读取文件、写入文件以及关闭文件这些行为进行修改。通常这样的修改行为是我们想要的,因此不推荐使用该标志。

e、FILE_FLAG_OPEN_NO_RECALL,该标志告诉系统不要将文件内容从脱机存储器(既是offline storage,比如磁带)回复到联机存储器(既是online storage,比如硬盘)。当文件很长时间没有被访问的时候,系统可以将文件内容转移到脱机存储器,从而腾出硬盘空间。(?)当系统执行这个操作的时候,并不会销毁硬盘上的文件,而只是销毁文件中的数据。当文件被打开的时候,系统会自动地从脱机存储器中恢复到文件数据。而该标志告诉系统不要恢复数据,这会导致系统对脱机存储媒介进行I/O操作。

f、FILE_FLAG_OVERLAPPED,这个标志告诉系统我们想要以异步方式来访问设备。打开设备的默认方式是同步I/O(既是没有指定该标志的)。

16、还有文件属性标志,见下表。除非我们正在创建一个新文件,且传给hFileTemplate参数的值为NULL,否则系统会完全忽略dwFlagsAndAttributes参数所指定的这些标志。





17、如果要创建临时文件,那么应该使用FILE_ATTRIBUTES_TEMPORARY标志。这样,CreteFile会尽量将文件数据保存在内存中,而不是在磁盘上,这使得访问文件的内容变得更快。如果内存不够,那么系统会被迫开始讲数据写入到硬盘中。

18、除了所有这些通信标志和属性标志,还有其他一些标志,当打开一个命名管道的时候,这些标志可以运行我们对安全服务质量(security quality of service)进行控制。

19、CreateFile的最后一个参数hFileTemplate,既可以标志一个已经打开的文件的句柄,也可以是NULL。如果标志一个文件句柄,那么会完全忽略dwFlagsAndAttributes参数,并转而使用hFileTemplate所标识的文件的属性。为了能够让函数以这种方式工作,hFileTemplate标识的文件必须是一个已经用GENERIC_READ标志打开的文件。如果CreateFile要打开已经有的的文件(而不是创建新文件),那么它会忽略hFileTemplate参数。

20、CreateFile成功创建或打开了文件或设备,那么会返回文件或设备句柄。如果失败了,返回INVALID_HANDLE_VALUE。

二、使用文件设备

1、Windows的设计允许我们处理非常大的文件。MS最初的设计者选择了64位值来表示文件代销,而不是32位值。这意味着理论上一个文件最大可以达到16EB(exabytes)。

2、在32位操作系统中处理64位值在使用文件的时候会不方便,这是因为大量的Windows函数要求将一个64位值分为两个32位值来传入。

3、LARGE_INTEGER是一个联合类型(http://msdn.microsoft.com/en-us/library/aa383713(v=VS.85).aspx)。这个联合运行我们以一个64位有符号数的形式来引用一个64位有符号数,或者两个32位值的形式来引用一个64位有符号数。在处理文件的大小和偏移量时,这会相当方便。大概原型如下:

typedef union _LARGE_INTEGER {
struct {
DWORD LowPart;
LONG  HighPart;
};
struct {
DWORD LowPart;
LONG  HighPart;
} u;
LONGLONG QuadPart;
} LARGE_INTEGER, *PLARGE_INTEGER;


4、GetFileSizeEx可以用来获得文件的逻辑大小,而GetCompressedFileSize返回的是文件的物理大小。例如,假设一个100KB的文件经过压缩后只占用85KB。GetFileSizeEx返回逻辑大小100KB,GetCompressedFileSize返回的则是文件在磁盘上实际占用的字节数85KB。

5、与GetFileSizeEx不同的是,GetCompressedFileSize(http://msdn.microsoft.com/en-us/library/windows/desktop/aa364930(v=vs.85).aspx)要求将文件名通过一个字符串的形式传入,而不是将文件在第一个参数中以句柄的形式传入。且,通过一种不同寻常的方式来返回64位的文件大小:

ULARGE_INTEGER ulFileSize;
ulFileSize.LowPart = GetCompressedFileSize(TEXT("SomeFile.data"), &ulFileSize.HighPart);
//六十四位文件大小现在就存储在ulFileSize.QuadPart中


6、一个文件内核对象内部有一个文件指针,它是一个64位偏移量,表示应该在哪里执行下一次同步读取或写入操作。这个文件指针一开始被设为0。由于每个文件内核对象都有自己的文件指针,因此两次打开同一个文件得到的结果会略有不同。

7、如果需要随机访问文件,那么我们需要改变与文件内核对象相关联的文件指针。通过SetFilePointerEx函数。(http://msdn.microsoft.com/en-us/library/windows/desktop/aa365542(v=vs.85).aspx

BOOL WINAPI SetFilePointerEx(
__in       HANDLE hFile,
__in       LARGE_INTEGER liDistanceToMove,
__out_opt  PLARGE_INTEGER lpNewFilePointer,
__in       DWORD dwMoveMethod
);


参数hFie表示想要修改的文件内核对象的文件指针。参数liDistanceToMove告诉系统想要把指针移动多少字节。系统会把指定的数值与文件指针的当前值相加,因此使用负数可以将文件指针向后移动。参数dwMoveMethod来指定移动文件指针时的起始位置。三个值如下表。



SetFilePointerEx更新了文件对象的文件指针之后,会在pliNewFilePointer参数指向LARGE_INTEGER结构中返回文件指针的新值。如果对新值不感兴趣,可以传入NULL给pliNewFilePointer参数。

8、如果SetFilePointerEx操作的文件是用FILE_FLAG_NO_BUFFERING标志打开的,那么文件指针只能被设置为扇区大小的整数倍。

9、Windows没有提供一个GetFilePointerEx函数,可以调用SetFilePointerEx将文件指针移动0个字节来得到更新指针值。

10、通常在关闭文件的时候,系统会负责设置文件尾。但是,可以通过SetEndOfFile来强制试文件变小或变大。该函数会根据文件对象的文件指针当前所在的位置来阶段文件的大小或增大文件的大小。

三、执行同步设备I/O

1、本节讨论的Windows函数允许我们执行同步设备I/O。设备既可以是文件,也可以是邮件槽、管道、套接字,等等。

2、ReadFile和WriteFile是最方便和最常用的对设备数据进行读/写的函数。

3、参数hFile用来标识想要访问的设备的句柄。在打开设备的时候,一定不能指定FILE_FLAG_OVERLAPPED标识,否则系统会认为我们想要与该设备执行异步I/O。在执行同步I/O的时候,最后一个参数pOverlapped应该被设为NULL。ReadFile只能用于GENERIC_READ标识打开的设备。同样,WriteFile只能用于那些用GENERIC_WRITE标识打开的设备。

4、调用FlushFileBuffers可以强制系统将与hFile参数所标识的设备相关联的所有缓存数据写入到设备。设备必须是通过GENERIC_WRITE标志打开的,这样FlushFileBuffers才能正常工作。

5、用来进行同步I/O的函数很容易使用,但它们会阻塞住来自同一个线程(即发出I/O请求的线程)的任何其他操作。CreateFile操作就是一个极好的列子。当用户在用鼠标和键盘进行输入的时候,窗口消息会被添加到响应的消息队列中,这个消息队列隶属于创建窗口的线程。如果线程,由于正在等待CreateFile返回而被阻塞,那么窗口消息将无法得到处理,该线程创建的所有窗口都会停滞在那里。应用程序停止响应的最常见原因,就会因为要等待同步I/O操作完成耳背阻塞住。

6、在Vista中,CancelSynchronousIon运行将一个给定线程尚未完成的同步I/O请求取消。参数hThread是由于等待同步I/O请求完成而被挂起的线程的句柄。这个句柄必须是用THREAD_TERMINATE访问权限创建的。

四、异步设备I/O基础

1、与计算机执行的大多数其他操作相比,设备I/O是其中最慢、最不可预测的操作之一。

2、假设一个线程向设备发出一个异步I/O请求。这个I/O请求被传给设备驱动程序,后者负责完成实际的I/O操作。当驱动程序在等待设备响应的时候,应用程序的线程并没有因为要等待I/O请求完成而被挂起,线程会继续运行并执行其他有用的任务。

3、为了以异步的方式来访问设备,必须先调用CreateFile,并在dwFlagsAndAttributes参数中指定FILE_FLAG_OVERLAPPED标志来打开设备。该标志告诉系统要以异步的方式来访问设备。

4、为了将I/O请求加入设备驱动程序的队列中,必须使用ReadFile和WriteFile函数。当调用这两个函数中任何一个时,函数会检查hFile参数标识的设备是否用FILE_FLAG_OVERLAPPED标志打开的。如果打开设备时指定了这个标志,那么函数会执行异步设备I/O。顺便提一下,当调用者两个函数来进行异步I/O的时候,可以(通常也会)穿NULL给pdwNumBytes参数。比较我们希望这两个函数在I/O请求完成之前就返回,因此这时就检查已经传输的字节数是没有意义的。

5、OVERLAPPED结构(http://msdn.microsoft.com/en-us/library/windows/desktop/ms684342(v=vs.85).aspx),意思是执行I/O请求的时间与线程执行其他任务的时间是重叠的(overlapped)。下面是OVERLAPPED结构(MSDN给出最新版,与本书中讲解有点点不同)定义:

typedef struct _OVERLAPPED {
ULONG_PTR Internal;
ULONG_PTR InternalHigh;
union {
struct {
DWORD Offset;
DWORD OffsetHigh;
};
PVOID  Pointer;
};
HANDLE    hEvent;
} OVERLAPPED, *LPOVERLAPPED;


Offset和OffsetHigh成员,构成一个64位的偏移量,它们表示当访问文件的时候应该从哪里开始进行I/O操作。每个文件内核对象都有一个与之相关联的文件指针。在执行异步I/O的时候,系统会忽略文件指针。这是为了避免在对同一个对象进行多个异步调用的时候出现混淆,所有异步I/O请求必须在OVERLAPPED结构中指定起始偏移量。非文件设备会忽略这两个参数,必须将其初始化为0,否则I/O请求会失败。

6、hEvent成员用来接收I/O完成通知的4中方法中,其中一个方法(使用I/O完成端口)会用到。当使用可提醒I/O通知(alertable I/O notification)函数时,可以根据自己的需要来使用这个成员。许多开发人员会在hEvent中保存一个C++对象的地址。

7、Internal成员用来保存已处理的I/O请求的错误代码。一旦我们发出一个异步I/O请求,设备驱动程序会立即将Internal设为STATUS_PENDING,表示没有错误,因为操作尚未开始。实际上,WinBase.h中定义的HasOverlappedIoCompleted宏(http://msdn.microsoft.com/en-us/library/windows/desktop/ms683244(v=vs.85).aspx)运行我们检查一个异步I/O操作是否已经完成。

BOOL HasOverlappedIoCompleted(
LPOVERLAPPED lpOverlapped
);


((lpOverlapped)->Internal != STATUS_PENDING)

8、InternalHigh成员,用来保存已传输的字节数。在最初设计OVERLAPPED结构的时候,MS不公开Internal和InternalHigh成员(名符其实)。后来才公开,不过由于源码频繁用到,MS不愿为此修改源码,所以还是没有名字。用Internal和InternalHigh。

9、异步I/O请求完成的时候,会收到一个OVERLAPPED结构的地址,它就是我们发出请求时候使用的那个。在OVERLAPPED结构中传入更多的上下文系想你在很多时候是有用的。比如,想把发出I/O请求时使用的设备句柄保存在OVERLAPPED结构中。所以,时常会创建一个派生自OVERLAPPED结构的C++类。这个C++类能够存放我们想要的任何附加信息。当收到OVERLAPPED结构的地址时,只需要把这个地址转型为该C++类。

10、异步设备I/O的注意事项

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

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

那么这两个函数会返回FALSE,必须调用GetLastError来检查到底发生了什么。如果GetLastError返回的是ERROR_IO_PENDING,那么I/O请求已经被成功添加入队列,会在晚些时候完成。

c、如果GetLastError返回的是其它值,那么I/O请求无法被添加到设备驱动程序的队列中。常见错误码有:ERROR_INVALID_USER_BUFFER或ERROR_NOT_ENOUGH_MEMORY,每个设备驱动程序会在非分页缓冲池(nonpaged pool)中维护一个固定大小的列表来管理待处理的I/O请求。这错误是表示列表已经满了。ERROR_NOT_ENOUGH_QUOTA,某些设备要求将我们的数据缓存所在的存储器页面锁定,这样当I/O在等待处理的时候,数据就不会被换出内存。但是系统对一个进程能够锁定的存储器页面数量做了限制。所以,会有不能锁定的情况。需要调用SetProcessWorkingSetSize来增加进程的锁定页面配额数。而如果使用了FILE_FLAG_NO_BUFFERIGN标志,是不会有这个错误的。

d、在异步I/O请求完成之前,一定不能移动或是销毁在发出I/O请求时所使用的数据缓存和OVERLAPPED结构。系统将I/O请求加入设备驱动程序的队列中时,会讲数据缓存的地址和OVERLAPPED结构的地址传给驱动程序。注意,传的只是地址而不是实际的数据块。因为,内存复制是非常耗时的,会浪费大量的CPU时间。

11、在设备驱动程序对一个已经加入队列的设备I/O请求进行处理之前将其取消,有以下方式:(被取消的I/O请求会放回错误码EOORO_OPERATION_ABORTED)

a、调用CancelIo来取消由给定句柄所标识的线程添加到队列中的所有I/O请求(除非该句柄具有与之相关联的I/O完成端口)。

b、可以关闭设备句柄,来取消该线程发出的所有I/O请求,而不管它们是由哪儿线程添加的。

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

d、需要将发往给定文件句柄的一个指定的I/O请求取消,那么可以调用CancelIoEx。

五、接收I/O请求完成通知

1、Windows提供了4种不同的方法来接收I/O请求已经完成的通知。如下表列举:(从易到难)



虽然是异步执行,但是线程最终还是需要与I/O操作的完成状态进行同步。换句话说,我们会运行到线程代码中的一个点,在这个点上,除非设备数据已经被载入到缓存中,否则线程将无法继续执行后续操作。

2、触发设备内核对象

a、在Windows中,设备内核对象可以用来进行线程同步,因此对象既可能处于触发状态,也可能处于未触发状态。ReadFile和WriteFile函数在将I/O请求添加到队列之前,会先将设备内核对象设为未触发状态。当设备驱动程序完成了请求之后,驱动程序会讲设备内核对象设为触发状态。

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

3、触发事件内核对象

a、上面的触发设备内核对象,实际上并不怎么有用,因为不能处理多个I/O请求。(一旦任何一个操作完成,内核对象就会被触发,无法确定是否全部读取操作都完成了。)

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

c、为了略微提高性能,可以告诉Windows在操作完成时候不要触发文件对象,调用SetFileCompletionNotificationModes函数。

d、如果想要同时执行多个异步设备I/O请求,必须为每个请求创建不同的事件对象,并初始化给每个请求的OVERLAPPED结构中的hEvent成员,然后再调用ReadFile或者WriteFile,当运行到代码中的那个点的时候,调用WaitForMultipleObjects。

e、GetOverlappedResult函数,由于MS之前没公布Internal和InternalHigh成员,所以有用,现在没用。
4、可提醒I/O

a、可提醒I/O非常糟糕,应该避免使用。但是,为了是可提醒I/O能够正常工作,MS在操作系统中添加了一些基础设施,而这些基础设施非常有用,也很有价值。

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

c、Ex函数和普通Read或Write函数不同之处有两方面:一是没有指向DWORD的指针作为参数来保存已传输的字节数,该信息只有回调函数才能得到。其次,Ex函数要求传入一个回调函数的地址,这个回调函数被称为完成函数(completion routine)。

————————完成函数原型

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

e、当一个可提醒I/O完成时,设备驱动程序不会试图去触发一个事件对象。事实上,设备就没有用到OVERLAPPED结构的hEvent成员。因此,如果需要,可以将hEvent据为己用。

f、当现场处于可提醒状态的时候,系统会检查它的APC队列,对队列中的每一项,系统会调用完成函数,并传入I/O错误码,已传输的字节数,以及OVERLAPPED结构的地址。(错误码和已传输的字节数可以通过OVERLAPPED结构成员得到)

g、APC队列是由系统内部维护的。队列中的I/O请求顺序和发出I/O请求的顺序没有直接关系。

h、为了对线程APC队列中的项进行处理,线程必须将自己置为可提醒状态。这只不过意味着我们的线程在执行的过程中已经到达了一个点,在这个点上它能够处理被中断的情况。Windows提供了6个函数可以将线程置为可提醒状态:SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx、SignalObjectAndWait、GetQueuedCompletionStatusEx和MsgWaitForMultipleObjectsEx。

i、调用上面6个函数之一并将线程置为可提醒状态时,系统会首先检查线程的APC队列。如果队列中至少有一项,那么系统不会让线程进入睡眠状态。系统会将APC队列中的那一项去除,让线程调用回调函数,并传入数据。当回调函数返回时,系统会检查APC队列中是否还有其他的项,如果还有,会继续处理。如果没有,对可提醒函数的调用会返回。(调用这些函数的时候APC队列中至少有一项,线程就不会进入睡眠状态。)

j、有两个糟糕的问题:回调函数和线程问题。回调函数会使得代码实现变得更加复杂,由于这些回调函数一般来说并没有足够的与某个问题有关的上下文信息,因此最终不得不将大量的信息放在全局变量中。线程问题是大问题:发出I/O请求的线程必须同时对完成通知进行处理。如果一个线程发出多个请求,那么即使其他线程完全处于空闲状态,该线程也必须对每个请求的完成通知做出响应。由于不存在负载均衡机制,因此应用程序的伸缩性不会太好。

k、QueueUserAPC允许我们手动地将一项添加到APC队列中。可以使用其进行非常高效的线程间通信,甚至能跨越进程的界限。但遗憾的是,我们只能传递一个值。也可以用来强制让线程退出等待状态(干净退出)。

5、I/O完成端口

a、I/O完成端口背后的理论是并发运行的线程的数量必须有一个上限——也就是说,同时发出的500个客户请求不应该允许出现500个可运行的线程。一旦可运行线程的数量大于可用的CPUS数量,系统就必须花时间来执行线程上下文切换,而这会浪费宝贵的CPU周期——这也是并发模型的一个潜在缺点。

b、并发模型的另一个缺点是需要为每个客户请求创建一个新的线程,配合线程池使用性能高。I/O完成端口的设计初衷就是与线程池配合使用。

c、I/O完成端口可能是最复杂的内核对象。调用CreateIoCompletionPort函数可以创建一个I/O完成端口。

————————————原型

该函数执行两项不同的任务,不仅会创建一个I/O完成端口,而且会将一个设备与一个I/O完成端口关联起来。应该按照功能来使用。

d、只创建I/O完成端口,前三个参数分别是INVALID_HANDLE_VALUE,NULL和0即可。

e、dwNumberOfConcurrentThreads告诉I/O完成端口在同一时间最多能有多少线程处于可运行状态。如果传入为0,那么I/O完成端口会使用默认值,也即是运行并发执行的线程数量等于主机的CPU数量。(避免额外的上下文切换)如果处理一个客户请求需要长时间的计算,而且中间很少被阻塞,那么就需要增大这个值。

f、CreateIoCompletionPort不需要一个SECURITY_ATTRIBUTES结构,因为I/O完成端口的设计初衷就是只在一个进程中使用。

g、当创建一个I/O完成端口的时候,系统内核实际上会创建5个不同的数据结构。如下图:



6、第一个数据结构是一个设备列表,表示与该端口相关联的一个或多个设备。通过CreateIoCompletionPort来讲设备与端口关联起来,把一项添加到一个已有I/O完成端口的设备列表中。需要传入一个已有I/O完成端口的句柄(由CreateIoCompletionPort前三个无关创建)、设备的句柄(可以是文件、套接字、邮件槽、管道等)以及一个完成键(即completion key,一个对我们有意义的值,操作系统并不关心我们在这里传入的到底是什么值)。每次将一个设备与该端口关联起来的时候,系统会将这些信息追加到I/O完成端口的设备列表中。

7、第二个数据结构是一个I/O完成队列。当设备的一个异步I/O请求完成时,系统会检查设备是否与一个I/O完成端口相关联,如果设备与一个I/O完成端口相关联,那么系统会讲该项已完成的I/O请求追加到I/O完成端口的I/O完成队列的末尾。这个队列中的每一项包含的信息有:已传输的字节数、最初设备与端口关联在一起的时候所设的完成键的值、一个指向I/O请求的OVERLAPPED结构的指针以及一个错误码。

8、想设备发出I/O请求,但不把该项已完成的I/O请求添加到I/O完成端口的队列中也是有可能的。通常并不需要这样做,但这样做偶尔还是有用的——比如,通过一个套接字发送数据,但并不关心数据实际上到底有没有送达。为了发出一个在完成的时候不要被添加到队列中的I/O请求,必须在OVERLAPPED结构的hEvent成员中保存一个有效的事件句柄,并将它与1按位或起来,如下面代码所示:

Overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
Overlapped.hEvent = (HANDLE)((DOWRD_PTR)Overlapped.hEvent | 1);
ReadFile(..., &Overlapped);
//关闭事件句柄不能忘了将最低位清掉
CloseHandle((HANDLE) ((DWORD_PTR) Overlapped.hEvent & ~1));


9、I/O完成端口的周边架构。标准的经验法则是取主机的CPU数量并将其乘以2来确定线程池中应该有多少线程。

10、线程池中的所有线程应该执行同一个函数。一般来说,这个线程函数会先进行一些初始化工作,然后进入一个循环,当服务进程被告知要停止的时候,这个循环也应该就此终止。在循环内部,线程将自己切换到睡眠状态,来等待设备I/O请求完成并进入完成端口。调用GetQueuedCompletionStatus可以达到这一目的。(http://msdn.microsoft.com/en-us/library/windows/desktop/aa364986(v=vs.85).aspx

BOOL WINAPI GetQueuedCompletionStatus(
__in   HANDLE CompletionPort,
__out  LPDWORD lpNumberOfBytes,
__out  PULONG_PTR lpCompletionKey,
__out  LPOVERLAPPED *lpOverlapped,
__in   DWORD dwMilliseconds
);


第一个参数hCompletionPort表示线程希望对哪个完成端口进行监视。许多服务应该程序只使用一个I/O完成端口,并让所有I/O请求的完成通知进入这个端口。GetQueuedCompletionStatus的任务基本上就是将调用线程切换到睡眠状态,直到指定的完成端口的I/O完成队列中出现一项,或者等待的时间已经超出(在dwMilliseconds参数中)指定的时间为止。

11、与I/O完成端口相关的第三个数据结构是等待线程队列。当线程池中的每个线程调用GetQueuedCompletionStatus的时候,调用线程的线程标识符会被添加到这个等待线程队列,这使得I/O完成端口内核对象始终都能够知道,有哪些线程当前正在等待对已完成的I/O请求进行处理。当端口的I/O完成队列中出现一项的时候,该完成端口会唤醒等待线程队列中的一个线程。该现场会得到已完成I/O项中的所有信息。这些信息都是通过传给GetQueuedCompletionStatus的pdwNumberOfBytesTransferred,pCompletionKey以及ppOverlapped参数来返回给线程的。

12、移除I/O完成队列中的各项是以先入先出的方式来进行的。但是,唤醒那些调用了GetQueuedCompletionStatus的线程是以后入先出的方式来进行的。举个例子,假设有4个线程在等待现场队列中等待。如果出现了一个已完成的I/O项,那么最后一个调用GetQueuedCompletionStatus的线程会被唤醒来处理这一项。当最后这个线程完成对该项的处理后,线程再次调用GetQueuedCompletionStatus来进入等待线程队列。如果现在又出现了另一个已完成的I/O项,那么处理上一项的同一个线程会被唤醒,来处理这个新的项。

13、如果I/O请求完成得足够慢,使得一个线程就能够将它们全部处理完,那么系统会不断地唤醒同一个线程,而让其他线程继续睡眠。通过使用这种后入先出算法,系统可以将那些违背调度的线程的内存资源(比如栈空间)换出到磁盘,并将它们从处理器的高速缓存中清楚。这意味着让许多线程等待一个完成端口并不是什么坏事。如果正在等待的线程数量大于已完成的I/O请求的数量,那么系统会讲多余线程的大多数资源换出内存。

14、Vista中,可以调用GetQueuedCompletionStatusEx来获得多个I/O请求的结果,而不必让许多线程等待完成端口,从而可以避免由此产生的上下文切换所带来的开销。

15、如果一个设备有完成端口与之相关联,那么即使发出的异步请求是以同步方式完成的,Windows仍然会将其结果添加到完成端口的队列中。为了能够略微提高性能,可以通过调用SetFileCompletionNotificationModes函数。传入FILE_SKIP_COMPLETION_PORT_ON_SUCCESS标志,以此来告诉Windows不要将以同步方式完成的异步请求添加到与设备相关联的完成端口中。

16、I/O完成端口为何如此有用,当我们创建I/O完成端口的时候,需要指定允许多少个线程并发运行。(通常会将则合格值设为主机的CPU数量)当已完成的I/O项被添加到队列中的时候,I/O完成端口想要唤醒正在等待的线程。但是,完成端口唤醒的线程数量最多不会超过我们指定的数量。因此,如果有4个I/O请求已完成,有4个线程正在等待GetQueuedCompletionStatus,那么I/O完成端口只会唤醒两个线程,而让其他两个线程继续睡眠。当每个线程处理完一个已完成的I/O项时,会再次调用GetQueuedCompletionStatus。这时系统发现队列中还有其他的项,于是会唤醒同一个线程来对剩余的项进行处理。

17、当完成端口唤醒一个线程的时候,会将该线程的线程标识符保存在与完成端口相关联的第4个数据结构中,也就是已释放线程列表(release thread list)。这使得完成端口能够记住哪些线程已经被唤醒,并监视它们的执行情况。如果一个已释放的线程调用的任何函数将该线程切换到了等待状态,那么完成端口会检测到这一情况,此时它会更新内部的数据结构,将该线程的线程标识符从已释放线程列表中移除,并将其添加到已暂停线程列表(paused thread list)(与I/O完成端口相关联的第5个数据结构)中。

18、完成端口的目标是根据在创建完成端口时指定的并发线程的数量,将尽可能多的线程保持在已释放线程列表中。如果一个已暂停的线程被唤醒,那么它会离开已暂停线程列表并重新进入已释放线程列表。这意味着此时已释放线程列表中的线程数量将大于最大允许的并发线程数量。在线程数量降到低于指定线程数量(CPU数量)之前,是不会再唤醒任何线程的。

19、线程池中该有多少线程,需要考虑两个问题。一是最少,这样就不必经常地创建和销毁线程(浪费CPU时间);二是最多,创建太多的线程同样会浪费系统资源。即便这些资源中的大多数可以被换出内存,但是如果不能够有效地管理,将系统资源的使用减到最少,甚至连页交换文件中的空间也不浪费,那么这对我们将是有利的。

21、PostQueuedCompletionStatus函数,用来将一个已完成的I/O通知追加到I/O完成端口的队列中。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: