windows核心编程--同步设备I/O与异步设备I/O

来源:互联网 发布:linux常用命令grep 编辑:程序博客网 时间:2024/06/08 07:06

-目标
设计高性能,伸缩性好,响应性好,而且健壮的应用。

当线程发出一个同步设备I/O请求的时候,它会被临时挂起,直到设备完成I/O请求为止。但,出于性能原因,我们希望线程不被阻塞,可以始终进行有用的工作。

为了不让线程闲下来,我们需让各个线程就它们正在执行的操作相互通信。这种机制被称为I/O完成端口,它可以帮助我们创建高性能且伸缩性好的应用程序。通过使用I/O完成端口,可以让线程在读取设备和写入设备时,不必等待设备的响应,从而显著地提高吞吐量。

I/O完成端口最初被设计用来处理设备I/O,但多年来,Microsoft已架构出越来越多能非常适合I/O完成端口模型的操作系统设施。如作业内核对象。

本章介绍的I/O完成端口与设备I/O有关,但注意的是,I/O完成端口也可以和设备I/O完全无关。可以把I/O完成端口认为是一种有无数中用途的线程间通信机制。

-打开和关闭设备
1.设备,这里可认为是能够与之通信的任何东西。
2.各种设备及其常见用途

设备 常见用途 文件 永久存储任何数据 目录 属性和文件压缩的设置 逻辑磁盘驱动器 格式化驱动器 物理磁盘驱动器 访问分区表 串口 通过电话线传输数据 并口 将数据传输到打印机 邮件槽 一对多数据传输,通常是通过网络传到另一台运行windows的机器上 命名管道 一对一数据传输,通常是通过网络传到另一台运行windows的机器 匿名管道 单机上的一对一数据传输 套接字 报文或数据流的传输,通常是通过网络传到任何支持套接字的机器上 控制台 文本窗口的屏幕缓存

本章将讨论应用程序如何与这些设备进行通信而不必等待设备响应。
windows尽可能地对开发人员隐藏设备之间的差异。

打开一个设备并和它通信时,许多windows函数允许我们以相同方式来从设备读取数据和向设备写入数据。虽然,这些函数能让我们在读写数据的时候不必关心设备的类型,但各个设备间存在着差异。各个设备间存在的许多细微差别,不一一介绍。重点介绍下文件。
3.为执行任何类型的I/O,须先打开想要操作的设备并得到一个句柄。
用来打开各种设备的函数

设备 用来打开设备的函数 文件 CreateFile(pszName为路径名或UNC路径名) 目录 CreateFile(pszName为路径名或UNC路径名),如调用CreateFile时指定FILE_FLAG_BACKUP_SEMANTICS标志,那么windows允许我们打开一个目录。打开目录使我们能够改变目录的属性(如正常,隐藏,等等)和它的时间戳 逻辑磁盘驱动器 CreateFile(pszName为”\.\x:”),如果指定的字符串是”\.\x:”的形式,那么windows允许我们打开一个逻辑磁盘驱动器,其中x是驱动器的盘符。打开驱动器使我们能够格式化驱动器或检测驱动器媒介的大小。 物理磁盘驱动器 CreateFile(pszName为”\.\PHYSICALDRIVEx”)。如果指定的字符串是”\.\PHYSICALDRIVEx”的形式,那么windows允许我们打开一个物理磁盘驱动器,其中的x是物理驱动器号。如,为了读写用户的第一个物理驱动器的扇区,我们应指定”\.\PHYSICALDRIVE0”,打开物理驱动器使我们能直接访问硬盘的分区表。打开物理驱动器有潜在的危险,错误地写入设备可能会导致操作系统的文件系统无法访问磁盘的内容。 串口 CreateFile(pszName为”COMx”) 并口 CreateFile(pszName为”LPTx”) 邮件槽服务器 CreateMailslot(pszName为”\.\mailslot\mailslotname”) 邮件槽客户端 CreateFile(pszName为”\servername\mailslot\mailslotname”) 命名管道服务器 CreateNamedPipe(pszName为”\.\pipe\pipename”) 命名管道客户端 CreateFile(pszName为”\servername\pipe\pipename”) 匿名管道 CreatePipe用来打开服务器和客户端 套接字 Socket,accept或AcceptEx 控制台 CreateConsoleScreenBuffer或GetStdHandle

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

4.
当完成对设备的操作后,须将其关闭。

// 对大多数设备BOOL CloseHandle(HANDLE hObject);// 对套接字int closesocket(SOCKET s);

5.查询设备类型

DWORD GetFileType(HANDLE hDevice);

返回值:
FILE_TYPE_UNKNOWN 未知
FILE_TYPE_DISK 磁盘文件
FILE_TYPE_CHAR 字符文件,一般是一个并口设备或控制台
FILE_TYPE_PIPE 指定的文件是一个命名管道或匿名管道

-细看CreateFile
可以用来创建和打开磁盘文件,也可以打开许多其他设备

HANDLE CreateFile(PCTSTR pszName,DWORD dwDesiredAccess,DWORD dwShareMode,PSECURITY_ATTRIBUTES psa,DWORD dwCreationDisposition,DWORD dwFlagsAndAttributes,HANDLE hFileTemplate);

1.
pszName,既表示设备的类型,也表示该类设备的某个实例。
2.
dwDesiredAccess,指定想以何种方式来和设备进行数据传输
通用标志:
0 不允许对设备执行读取和写入
GENERIC_READ 读
GENERIC_WRITE 写
GENERIC_READ | GENERIC_WRITE 读&写
3.
dwShareMode,指定设备共享特权。
当我们仍然打开一个设备的时候,该参数可以控制其他CreateFile调用,能够以何种方式来打开设备。

值 含义 0 独占。如设备已打开,调用失败。如调用成功,未关闭此设备前,后续对同一设备CreateFile失败。 FILE_SHARE_READ 共享读取。如设备已被独占或写打开,调用失败。如调用成功,未关闭此设备前,后续对同一设备,要求写或独占时,会失败。 FILE_SHARE_WRITE 共享写入。如设备已被独占或读打开,调用失败。如调用成功,未关闭此设备前,后续对同一设备,要求独占或读时,会失败 FILE_SHARE_READ FILE_SHARE_WRITE FILE_SHARE_DELETE …

打开设备:
打开时要求的一个访问权限。
指定一个限制后续函数调用,所允许指定的访问权限。

突破文件名长度限制:
打开一个文件时,传入的路径名最长不能超过MAX_PATH。
CreateFileW+”\?\”作为路径名前缀,CreateFileW内部会把前缀去除,此时允许传入的路径名长度可超过32000个Unicode字符。此时,须用完整路径名,且路径中每个独立的组成部分仍然不能超过MAX_PATH个字符。

4.
psa,指向一个SECURITY_ATTRIBUTES结构,可以用来指定安全信息以及我们是否希望CreateFile返回的句柄能够被继承。只有在具备安全性的文件系统创建文件时,才会用到结构内部的安全描述符。其他情况下,该安全描述符被忽略。
传NULL时,表示用默认的安全设定来创建文件,且返回的句柄是不可继承的。

5.
dwCreationDisposition

值 含义 CREATE_NEW 创建新文件,如同名文件已存在,调用失败。 CREATE_ALWAYS 创建新文件,如同名文件已存在,覆盖。 OPEN_EXISTING 打开文件,如不存在,调用失败 OPEN_ALWAYS 打开文件,如不存在,创建后打开。 TRUNCATE_EXISTING 打开一个已有的文件+将文件大小截断为0字节。如不存在,调用失败。

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

6.
dwFlagsAndAttributes
6.1.允许设置一些标志来微调与设备间的通信
a.FILE_FLAG_NO_BUFFERING
系统访问磁盘时,
文件–高速缓存–内存

此标志设置时,文件系统的设备驱动程序会将文件数据直接写入我们提供的缓存中,需遵循以下规则:
访问文件时,使用的偏移量必须正好是磁盘卷的扇区大小的整数倍。
读取、写入的字节数必须正好是扇区大小的整数倍。
必须确保缓存在进程地址空间中的起始地址正好是扇区大小的整数倍。

高速缓存机制需要一些支持,文件特别大时,可能无法分配高速缓存机制所需的内部数据结构,而导致打开失败,故,访问非常大的文件,必须用FILE_FLAG_NO_BUFFERING。

b.FILE_FLAG_SEQUENTIAL_SCAN
承诺对文件顺序访问,由于顺序访问,从文件读取数据时,实际读取到高速缓存的数据量会超过要求的,提高顺序访问效率。
c.FILE_FLAG_RANDOM_ACCESS
承诺随机访问,此时,没必要在高速缓存中存放额外数据,提高内存使用效率。

b,c在使用了高速缓存下,才有意义。

d.FILE_FLAG_WRITE_THROUGH
指定时,系统将所有对文件的修改直接写入到磁盘中,系统仍然会在内部的缓存中保存文件数据。
对网络服务器上的文件,使用此标志打开文件,之后,对文件写操作,会在数据已被写入服务器的磁盘后,才返回。

e.FILE_FLAG_DELETE_ON_CLOSE
可以让系统在文件的所有句柄都关闭后,删除文件。
f.FILE_FLAG_BACKUP_SEMANTICS
备份、恢复权限打开文件
f.FILE_FLAG_OVERLAPPED
告诉系统我们想要以异步方式来访问设备。
同步I/O举例:
从文件读取数据时,我们得线程会被挂起,等待要读取的信息。一旦读取完毕,线程重新得到控制权并执行。

设备I/O比大多数其他操作慢。
异步I/O举例:
从文件读取数据时,立即返回。操作系统在自己的线程中替我们完成I/O操作。当操作系统完成我们要求的I/O操作时,会通知我们。

要创建高性能,伸缩性好,响应好且健壮的应用,异步I/O是关键。

6.2.设置文件的属性
要求:正在创建新文件+hFileTemplat为NULL时,文件属性有效。
a.FILE_ATTRIBUTE_ARCHIHVE
存档文件。
b.FILE_ATTRIBUTE_ENCRYPTED
加密文件。
c.FILE_ATTRIBUTE_HIDDEN
隐藏文件。
d.FILE_ATTRIBUTE_NORMAL
单独使用有效。
e.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED
内容索引服务不会对文件索引。
f.FILE_ATTRIBUTE_READONLY
只读文件。
g.FILE_ATTRIBUTE_SYSTEM
系统文件。
h.FILE_ATTRIBUTE_TEMPORARY
临时文件。
临时文件,系统会将其数据尽量保存在内存中,不断写入,直到系统再也无法继续
将数据保存在内存时,才被迫将其数据写入硬盘。

7.hFileTemplate
可以是一个已经打开的文件的句柄,也可是NULL。
如果hFileTemplate标识一个文件句柄,那么CreateFile会忽略dwFlagsAndAttributes参数,使用hFileTemplate所标识的文件的属性。

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

-使用文件设备
文件大小用64位值表示
在32位系统中处理64位值,使我们使用文件时会不太方便

1.取得文件大小

// 返回文件的逻辑大小BOOL GetFileSizeEx(HANDLE hFile,// 64位有符号数PLARGE_INTEGER pliFileSize);typedef union _LARGE_INTEGER{struct{// 32位无符号DWORD LowPart;// 32位有符号LONG HighPart;};// 64位有符号LONGLONG QuadPart;}LARGE_INTEGER, *PLARGE_INTEGER;// 64位无符号typedef union _ULARGE_INTEGER{struct{DWORD LowPart;DWORD HighPart;};ULONGLONG QuadPart;}ULARGE_INTEGER, *PULARGE_INTEGER;// 也可用来取得文件大小// 返回文件的物理大小// 返回值是低32位DWORD GetCompressedFileSize(// 文件名PCTSTR pszFileName,// 高32位,此时也是无符号PDWORD pdwFileSizeHigh);

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

每个文件内核对象都有自己的文件指针。

BYTE pb[10];DWORD dwNumBytes;HANDLE hFile1 = CreateFile(TEXT("MyFile.dat"), ...);HANDLE hFile2;DuplicateHandle(GetCurrentProcess(),hFile1,GetCurrentProcess(),&hFile2,0,FALSE,DUPLICATE_SAME_ACCESS);// 复制内核对象句柄,使内核对象使用计数加一。// 两个句柄指向同一个内核对象。// 文件指针一个内核对象只有一个。// 从文件位置0-9读取10个字节ReadFile(hFile1, pb, 10, &dwNumBytes, NULL);// 由于上一个调用使文件指针此时位于10 // 从文件位置10-19读取10个字节ReadFile(hFile2, pb, 10, &dwNumBytes, NULL);// 随机访问文件时,改变文件指针指向位置BOOL SetFilePointerEx(HANDLE hFile,// 要移动字节数// 可正,可负LARGE_INTEGER liDistanceToMove,// 返回更新后的文件指针的新值PLARGE_INTEGER pliNewFilePointer,// 指定移动文件指针时的起始位置// FILE_BEGIN   从文件头开始,此时liDistanceToMove被解释为一个无符号64位值// FILE_CURRENT 从当前指针位置// FILE_END     从文件结束位置(文件对象的文件指针被设为文件的逻辑大小加上liDistanceToMove)DWORD dwMoveMethod);

SetFilePointerEx注意点:
2.1.将文件指针的值设为超过文件当前的大小是正当操作。
除非在该位置向文件写入数据或调用SetEndOfFile,否则,这样做不会增加文件在磁盘上的实际大小。
2.2.如SetFilePointerEx操作的区大小的整数倍。
2.3.Windows没有提供一个GetFilPointerEx函数。
可调用SetFilePointerEx将文件指针移动0个字节,通过这种方式来达到相同效果。

LARGE_INTEGER liCurrentPosition = {0};SetFilePointerEx(hFile,liCurrentPosition,&liCurrentPosition, FILE_CURRENT);

3.设置文件尾
通常,关闭文件时,系统会负责设置文件尾。
有时,可能想强制使文件变得更小或更大。
这时,可用如下函数:

// 会根据文件对象的文件指针当前所在的位置来截断文件的大小或增大文件的大小// 用调用时文件指针位置作为文件结束位置(最后一个有效字节的下一字节处)BOOL SetEndOfFile(HANDLE hFile);// 举例:强制设置文件大小为1024。HANDLE hFile = CreateFile(...);LARGE_INTEGER liDistanceToMove;liDistanceToMove = 1024;SetFilePointerEx(hFile,liDistanceToMove,NULL,FILE_BEGIN);SetEndOfFile(hFile);CloseHandle(hFile);

-执行同步设备I/O
1.
本节讨论的Windows函数允许我们执行同步设备I/O。
设备可以是文件,也可以是邮件槽,管道,套接字,等等。无论使用何种类型设备,都用相同的函数来执行I/O操作。

BOOL ReadFile(HANDLE hFile,PVOID pvBuffer,DWORD nNumBytesToRead,PDWORD pdwNumBytes,OVERLAPPED* pOverlapped);BOOL WriteFlie(HANDLE hFile,CONST VOID *pvBuffer,DWORD nNumBytesToWrite,PDWORD pdwNumBytes,OVERLAPPED* pOverlapped);

3.1.hFile
标识想要访问的设备的句柄。打开设备时,不可指定FILE_FLAG_OVERLAPPED。否则,系统会认为我们想要与该设备执行异步I/O。
3.2.pvBuffer
指向一个缓冲,函数会把设备数据读取到该缓存,或把缓存中的数据写入到设备。
3.3.nNumBytesToRead
ReadFile要从设备读取的字节数
3.4.nNumBytesToWrite
WriteFile要向设备写入的字节数
3.5.pdwNumBytes
将成功从设备读取的字节数和成功向设备写入的字节数保存在这个参数中,并返回给调用者。
3.6.pOverlapped
执行同步I/O时,此参数应设为NULL。

2.将数据刷新至设备
一般,系统会对文件数据进行缓存。其他一些设备,如串口,邮件槽,管道,也会对数据进行缓存。如果我们想要强制系统将缓存数据写入到设备,那么可以调用FlushFileBuffers。

// 会强制将与hFile参数所标识的设备相关联的所有缓存数据写入设备。// 这里和上述读取,写入,相应的文件句柄得具有相应的权限。BOOL FlushFileBuffers(HANDLE hFlie);

3.同步I/O取消
用来进行同步I/O的函数很容易使用,但它们会阻塞住来自同一个线程(即发出I/O请求的线程)的任何其他操作。
用户在用鼠标和键盘进行输入的时候,窗口消息会被添加到相应的消息队列中,这个消息队列隶属于创建窗口的线程。如果执行CreateFile调用尚未返回,则,窗口消息无法得到处理。
应用停止响应最常见原因,就是要等待同步I/O操作完成而被阻塞。

现在,如果一个控制台应用程序因为同步I/O而停止响应,可按Ctrl+C,拿回控制权。

为了创建响应性好的应用程序,我们应尽可能执行异步I/O操作。
一般,这使我们能够在应用程序中使用较少的线程。
另外,如果I/O操作是以异步方式进行的,那么要向用户提供取消操作的功能通常会很简单。

有些Windows API(如CreateFlie)没有提供任何方法来进行异步调用,如果能有一个API供我们调用来强制线程取消同步I/O操作并退出等待,则最好。

BOOL CancelSynchronousIo(HANDLE hThread);

hThread:由于等待同步I/O请求完成而被挂起的线程的句柄,这个句柄必须是用THREAD_TERMINATE访问权限创建的。我们在用CreateThread或_beginthreadex创建自己的线程时,返回的句柄具备THREAD_ALL_ACCESS,自然包括THREAD_TERMINATE。

如果用OpenThread来得到与当前线程标识符相对应得线程句柄,这是要传入THREAD_TERMINATE。

如果指定的线程由于等待同步I/O操作完成而被挂起,则CancelSynchronousIo会将被挂起的线程唤醒,线程试图执行的操作将会失败。这是错误码为ERROR_OPERATION_ABORTED。
如果调用CancelSynchronousIo时,指定的线程不是因为要等待设备响应而被挂起,则,返回FALSE。

可能出现某个驱动程序不支持取消的情况。这时,无论如何CancelSynchronousIo还是会返回TRUE,这是因为函数已经找到了一个请求并将其标记为正在取消。将请求真正地取消是驱动程序的责任。

-异步设备I/O基础
1.与计算机执行的大多数其他操作相比,设备I/O是其中最慢,最不可预测的操作之一。
CPU从文件中或跨网络读取数据的速度,CPU向文件或跨网络写入数据的速度,相比它执行算术运算和绘制屏幕的速度要慢很多。但是,使用异步设备I/O使我们能更好地使用资源并创建出更高效的应用程序。

2.异步I/O举例
一个线程向设备发出异步I/O请求,这个请求被传给设备驱动处理,后者完成实际的I/O操作。当驱动程序在等待设备响应时,线程继续运行。
到了某一时刻,设备驱动完成了对队列中的I/O请求的处理,这时它须通知应用数据已发送,数据已收到,或发生了错误。

3.以异步方式来访问设备
1.调用CreateFile,在dwFlagsAndAttributes中指定FILE_FLAG_OVERLAPPED来打开设备。

2.ReadFile和WriteFile。

-OVERLAPPED结构
执行异步设备I/O时,须在pOverlapped中传入一个已初始化的OVERLAPPED结构。

typedef struct _OVERLAPPED{DWORD Internal;// 4DWORD InternalHigh;// 5DWORD Offset;// 1DWORD OffsetHigh;// 2HANDLE hEvent;// 3} OVERLAPPED, *LPOVERLAPPED;

1,2,3须在调用ReadFile和WriteFile之前初始化。
4,5由驱动程序来设置,当I/O操作完成时,我们可检查它们的值。

1.Offset和OffsetHigh
构成一个64位的偏移量,表示访问文件的时候,应从哪里开始进行I/O操作。

我们发出同步I/O请求的时候,系统知道应从文件指针指向的位置开始访问。操作完成后,系统会自动更新文件指针。
执行异步I/O的时候,系统会忽略文件指针。所有异步I/O请求必须在OVERLAPPED结构中指定起始偏移量。

非文件设备会忽略Offset和OffsetHigh,此时须将两个成员均初始化为0。

2.hEvent
接收I/O完成通知的一种方式使用。

3.Internal
保存已处理的I/O请求的错误码。
一旦我们发出一个异步I/O请求,设备驱动程序会立即将Internal设为STATUS_PENDING,表示操作尚未开始。

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

异步I/O请求完成时,我们会收到一个OVERLAPPED结构的地址,它就是我们发出请求时使用的那个。在OVERLAPPED结构中传入更多的上下文信息,在很多时候是有用的。可以创建一个派生自OVERLAPPED结构的C++类,应用收到OVERLAPPED结构的地址时,再转型为对应的C++类来访问传递的附加信息。

-异步设备I/O的注意事项
1.设备驱动不必以先入先出的方式来处理队列中的I/O请求。
如果不按顺利来执行I/O请求能提高性能,那么设备驱动一般都会这样做。
2.
当我们试图将一个异步I/O请求添加到队列中时,设备驱动可能会选择以同步方式来处理请求。
当从文件中读取数据时,系统检查我们想要的数据是否已在系统的缓存中,如在,系统不会将我们的I/O请求添加到设备驱动程序的队列中,而会将高速缓存的数据复制到我们的缓存中,来完成这个I/O操作。

驱动程序总是会以同步方式来执行某些操作。
如果请求的I/O操作是以同步方式执行的,那么ReadFile和WriteFile返回非0值。
如果请求的I/O操作是以异步方式执行的,或调用ReadFile或WriteFile时,发生了错误,那么这两个函数会返回FALSE,须调用GetLastError检查。
ERROR_IO_PENDING,I/O请求已被添加到队列,尚未处理。
其它值,I/O请求无法被添加到设备驱动程序的队列中。
举例:
ERROR_INVALID_USER_BUFFER或ERROR_NOT_ENOUGH_MEMORY每个设备驱动会在非分页缓冲池中维护一个固定大小的列表来管理待处理的I/O请求,如列表已满,返回上述两个错误码之一。
ERROR_NOT_ENOUGH_QUOTA某些设备要求将我们的数据缓存所在的存储器页面锁定,这样当I/O在等待处理时,数据不会被换出内存。但是,系统对一个进程能够锁定的存储器页面数量做了限制。

3.异步I/O请求完成前,不能移动或销毁在发出I/O请求时所使用的数据缓存和OVERLAPPED结构。
当系统将I/O请求加入设备驱动程序的队列中时,会将数据缓存的地址和OVERLAPPED结构的地址传给驱动程序。
当设备驱动程序准备好处理我们添加到对队列中的请求时,会传输pvBuffer地址所引用的数据,并访问pOverlapped参数指向的OVERLAPPED结构的文件偏移量和其它成员。

须为每个异步I/O请求,分配并初始化一个不同的OVERLAPPED结构。

-取消队列中的设备I/O请求
有时,想在设备驱动程序对一个已经加入队列的设备I/O进行处理前,将其取消。
1.

// 取消调用线程针对hFile标识的文件产生的所有尚未完成的I/O请求BOOL CancelIo(HANDLE hFile);

2.
设备句柄关闭时,此设备句柄发出的已添加到队列的I/O请求会被取消。
3.
线程终止时,此线程发出的已添加到队列的I/O请求会被取消。(发出请求的设备关联到I/O完成端口时,请求不会被取消)
4.

BOOL CancelIoEx(HANDLE hFile, LPOVERLAPPED pOverlapped);

被取消的I/O请求会返回错误码ERROR_OPERATION_ABORTED。

-接收I/O请求完成通知
触发设备内核对象
触发事件内核对象
使用可提醒I/O
使用I/O完成端口

1.触发设备内核对象
ReadFile和WriteFile在将I/O请求添加到队列之前,会先将设备内核对象设为未触发状态。设备驱动程序完成了请求后,驱动程序会将设备内核对象设为触发状态。

HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);BYTE bBuffer[100];OVERLAPPED o = { 0 };// 从文件的第346个字节开始读取o.Offset = 345;BOOL bReadDone = ReadFile(hFile, bBuffer, 100, NULL, &o);DWORD dwError = GetLastError();if(!bReadDone && (dwError == ERROR_IO_PENDING)){    WaitForSingleObject(hFile, INFINITE);    bReadDone = TRUE;}if(bReadDone){//}else{//}

缺点:
无法处理同一设备的多个异步I/O的完成通知。

2.触发事件内核对象
OVERLAPPED结构的最后一个成员hEvent来标识一个事件内核对象。
须通过CreateEvent创建这个事件对象。
当一个异步I/O请求完成时,设备驱动会检查OVERLAPPED的hEvent是否为NULL。如果hEvent不为NULL,那么设备驱动会调用SetEvent来触发事件。

HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);BYTE bReadBuffer[10];OVERLAPPED oRead = { 0 };oRead.Offset = 0;oRead.hEvent = CreateEvent(...);ReadFile(hFile, bReadBuffer, 10, 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:    break;case 1:    break;}

3.可提醒I/O
可提醒I/O非常糟糕,应该避免使用。但是,为了使可提醒I/O能正常工作,Microsoft在操作系统中添加了一些基础设施,这些基础设施非常有用。
这里,请将注意力集中在基础设施上,而不要纠缠在与I/O有关的方面。

系统创建一个线程时,会同时创建一个与线程相关联的队列,这个队列被称为异步过程调用队列。当发出一个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);VOID WINAPI CompletionRoutine(DWORD dwError,DWORD dwNumBytes,OVERLAPPED *po);

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

一个可提醒I/O完成时,设备驱动程序不会试图去触发一个事件对象。

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

hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);ReadFileEx(hFile, ...);WriteFileEx(hFile, ...);ReadFileEx(hFile, ...);SomeFunc();

假设执行SomeFunc需一些时间,系统在SomeFunc返回前就完成了所有三个操作。线程执行SomeFunc时,设备驱动程序正在将已完成的I/O一项一项地添加到线程的APC队列中。

APC队列由系统内部维护,系统会以任意的顺序来执行添加到队列的I/O请求,最后发出的I/O请求可能最先完成。添加到线程的APC队列中的每一项包含一个回调函数的地址,及一个要传给该回调函数的值。

当I/O请求完成时,系统会将它们添加到线程的APC队列中,为了对线程APC队列中的项进行处理,线程必须将自己置为可提醒状态。这不过意味这线程在执行的过程中已经到达了一个点,在这个点上它能够处理被中断的情况,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,//  须用MWMO_ALERTABLEDWORD dwFlags);

当调用刚才的6个函数之一并将线程置为可提醒状态时,系统会首先检查线程的APC队列。如果队列中至少有一项,那么系统不会让线程进入睡眠状态。会依次处理完,线程APC队列的每一项,(执行此项对应的函数调用)。处理完毕后,可提醒函数的调用才返回。线程才被挂起而进入睡眠状态。
当线程被挂起时,如果我们正等待的那个(或那些)内核对象被触发,或线程的APC队列中出现了一项,那么线程将会被唤醒。因为,线程处于可提醒状态,所以一旦APC队列中出现一项,系统就会唤醒我们的线程,处理线程APC队列中出现的项,处理完毕后,线程不会再次进行睡眠来等待内核对象被触发,而是从被挂起出,继续执行。
这6个函数的返回值,可表示返回的原因。如返回WAIT_IO_COMPLETION,则,线程继续执行的原因是线程至少处理了APC队列的一项。

可提醒I/O不足:
3.1.回调函数
回调函数的参数不包含足够的上下文信息。
3.2.线程问题
发出I/O请求的线程必须同时对APC队列项的回调函数进行处理。

-可提醒I/O基础设施
1.windows提供了一个函数,允许我们手动地将一项添加到APC队列中。

DWORD QueueUserAPC(// 一个指向APC函数的指针PAPCFUNC pfnAPC,// 要将该项添加到那个线程的队列中。// 如为另一进程的线程,pfnAPC也须基于对应进程HANDLE hThread,// 传给回调函数值ULONG_PTR dwData);VOID WINAPI APCFunc(ULONG_PTR dwParam);

强制线程退出等待状态

VOID WINAPI APCFunc(ULONG_PTR dwParam){}UINT WINAPI ThreadFunc(PVOID pvParam){HANDLE hEvent = (HANDLE)pvParam;DWORD dw = WaitForSingleObjectEx(hEvent, INFINITE, TRUE);if(dw == WAIT_OBJECT_0){//}else if(dw == WAIT_IO_COMPLETION){return 0;}...return 0;}void main(){HANDLE hEvent = CreateEvent(...);HANDLE hThread = (HANDLE)_beginthreadex(NULL,0,ThreadFunc,(PVOID)hEvent,0,NULL );...QueueUserAPC(APCFunc, hThread, NULL);WaitForSingleObject(hThread, INFINITE);CloseHandle(hThread);CloseHandle(hEvent);}

-I/O完成端口
windows的设计目标是一个安全的,健壮的操作系统。
我们能采用以下两种模型来架构一个服务应用程序:
串行模型:一个线程等待一个客户发请求,请求到达时,线程被唤醒对客户请求进行处理。
并发模型:一个线程等待一个客户请求,创建一个新线程来处理请求。

使用并发模型的服务应用程序是在windows中实现的,windows开发组注意到应用程序的性能不如预期的高。处理许多客户请求意味这系统中会有许多的线程并发执行。会导致,windows内核在各可运行的线程之间进行上下文切换发费了太多时间,以至于各线程都没多少CPU时间来完成任务了。为了将windows打造成一个出色的服务器环境,microsoft设计了I/O完成端口内核对象。

1.创建I/O完成端口
I/O完成端口背后的理论是,并发运行的线程数量须有一个上限。
一旦可运行线程数量大于可用的CPU数量,系统就须发时间执行线程上下文切换。
并发模型的另一个缺点是,需要为每隔客户请求创建一个新的线程。创建新线程也是一种开销。
如果在应用程序初始化的时候创建一个线程池,并让线程池中的线程在应用程序运行期间一直保持可用状态,那么服务应用程序的性能就能提高。
I/O完成端口设计初衷就是和线程池配合使用。

HANDLE CreateIoCompletionPort(// 设备句柄HANDLE hFile,// 一个已存在I/O完成端口句柄HANDLE hExistingCompletionPort,// 完成键ULONG_PTR CompletionKey,// 同一时间最多可用多少个线程处于可运行状态// 0时,使用默认值,主机CPU数量。DWORD dwNumberOfConcurrentThreads);

创建一个I/O完成端口+将一个设备与I/O完成端口关联。
I/O完成端口是一个创建时不需要指定安全属性的内核对象。

2.我们创建一个I/O完成端口时,系统内核实际上会创建5个不同的数据结构。
2.1.第一个数据结构是一个设备列表,表示与该端口相关联的一个或多个设备。

#define CK_FILE 1HANDLE hFile = CreateFile(...);// 创建I/O完成端口// 创建时,把一个设备加入到了I/O完成端口的设备列表,完成键为CK_FILE// 此I/O完成端口最多运行两个线程并发执行HANDLE hCompletionPort = CreateIoCompletionPort(hFile, NULL, CK_FILE, 2);

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

发出一个在完成时,不要被添加到队列中的I/O请求,我们须在OVERLAPPED结构的hEvent成员中保存一个有效的事件句柄,并将它与1按位或起来。

Overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);Overlapped.hEvent = (HANDLE)((DWORD_PTR)Overlapped.hEvent | 1);ReadFile(..., &Overlapped);....CloseHandle((HANDLE)((DWORD_PTR)Overlapped.hEvent & ~1));

2.3.I/O完成端口周边架构
当我们的服务应用程序初始化时候,应调用CreateNewCompletionPort之类的函数来创建I/O完成端口。
应用接着应创建一个线程池来处理客户请求。线程池中线程数,一般取主机的CPU数量并将其乘以2。
线程池中的所有线程应执行同一个函数。一般,这个线程函数会先进行一些初始化工作,然后进入一个循环,当服务进程被告知要停止时,这个循环也应该终止。在循环内,线程将自己切换到睡眠状态,来等待设备I/O请求完成并进入完成端口。

// 将调用线程切换到睡眠状态,直到指定的完成端口的I/O完成队列中出现一项,或者等待的事件以及超出了指定的时间。BOOL GetQueuedCompletionStatus(// 线程要监视的I/O完成端口HANDLE hCompletionPort,PDWORD pdwNumberOfBytesTransferred,PULONG_PTR pCompletionKey,OVERLAPPED **ppOverlapped,DWORD dwMilliseconds);

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

DWORD dwNumBytes;ULONG_PTR CompletionKey;OVERLAPPED *pOverlapped;BOOL bOk = GetQueuedCompletionStatus(hIOCP,&dwNumBytes,&CompletionKey,&pOverlapped,1000);DWORD dwError = GetLastError();// 端口的I/O完成队列中某一项触发了函数返回if(bOk){// }// 返回,但属于失败返回else{// 可用pOverlapped查看错误码if(pOverlapped){}else{// 可查看最后系统设置的错误码}}

移除I/O完成队列中的各项是以先入先出方式进行的。唤醒那些调用了GetQueuedCompletionStatus的线程是以后入先出的方式来进行的。
通过使用这种后入先出算法,系统可将那些未被调度的线程的内存资源(如栈空间)换出到磁盘,并将它们从处理器的高速缓存中清除。

// 同时取得多个I/O请求的结果,而不必让许多线程等待完成端口BOOL GetQueuedCompletionStatusEx(HANDLE hCompletionPort,// LPOVERLAPPED_ENTRY pCompletionPortEntries,// 数组容量ULONG ulCount,// 实际放入个数PULONG pulNumEntriesRemoved,// 超时时间DWORD dwMilliseconds,// 可提醒标志// FALSE 直到有已完成I/O请求被放入接收缓存才返回// TRUE  如果当前没有已完成I/O请求,挂起,处于可提醒状态BOOL bAlertable);typedef struct _OVERLAPPED_ENTRY{ULONG_PTR lpCompletionKey;LPOVERLAPPED lpOverlapped;// 无意义ULONG_PTR Internal;DWORD dwNumberOfBytesTransferref;}OVERLAPPED_ENTRY, *LPOVERLAPPED_ENTRY;

如果一个设备有完成端口与之相关联,那么,当我们向它发送一个异步I/O请求时,windows会将结果添加到完成端口的队列中,即使异步请求是以同步方式完成时,也会这样。
调用SetFileCompletionNotificationModes并传入FILE_SKIP_COMPLETION_PORT_ON_SUCCESS,可以告诉windows不要将以同步方式完成的异步请求添加到与设备关联的完成端口中。

-I/O完成端口如何管理线程池
当I/O完成端口唤醒一个线程的时候,会将该线程的线程标识符保存在与完成端口相关联的第4个数据结构中,也就是已释放线程列表。

这使得完成端口能记得哪些线程已被唤醒,并监视它们的执行情况。如果一个已释放的线程调用的任何函数将该线程切换到了等待状态,那么完成端口会检测到这一情况,此时它会更新内部的数据结构,将该线程的线程标识符从已释放线程列表中移除,并将其添加到已暂停线程列表。

完成端口的目标是根据在创建完成端口时指定的并发线程数量,将尽可能多的线程保持在已释放线程列表中。如果一个已释放线程由于任何原因而进入等待状态,那么已释放线程列表会缩减,完成端口就可释放另一个正在等待的线程。如果一个已暂停的线程被唤醒,那么它会离开已暂停线程列表并重新进入已释放线程列表。这意味着,此时已释放线程列表中的线程数量将大于最大允许的并发线程数量。

一旦一个线程调用了GetQueuedCompletionStatus,该线程会被“指派”给指定的完成端口。只有当指派给完成端口的正在运行的线程数量小于它最大允许的并发线程数量时,完成端口才会从线程池中唤醒线程。
可用以下三种方式来结束线程/完成端口的指派:
让线程退出。
线程调用GetQueuedCompletionStatus,并传入另一个不同的I/O完成端口的句柄。
销毁线程当前被指派的I/O完成端口。

I/O完成端口体系结构假定可运行线程的数量只会在很短的一段时间内高于最大允许的线程数量,一旦线程进入下次循环并调用GetQueuedCompletionStatus,可运行线程的数量就会迅速下降。

-线程池中有多少线程
1.当服务应用程序初始化时,想要创建一定数量的线程。这样后续就不必经常创建和销毁线程。
2.要设置一个最大线程数量。创建太多线程会浪费系统资源,即便这些资源中的大多数可以被换出内存。

大多数服务使用启发式算法来对它们的线程池进行管理。

LONG g_nThreadsMin;LONG g_nThreadsMax;LONG g_nThreadsCrnt;LONG g_nThreadsBusy;DWORD WINAPI ThreadPoolFunc(PVOID pv){    InterlockedIncrement(&g_nThreadsCrnt);    InterlockedIncrement(&g_nThreadsBusy);    for(BOOL bStayInPool = TRUE; bStayInPool;)    {        InterlockedDecrement(&m_nThreadsBusy);        BOOL bOk = GetQueuedCompletionStatus(...);        DWORD dwIOError = GetLastError();        int nThreadsBusy = InterlockedIncrement(&m_nThreadsBusy);        if(nThreadsBusy == m_nThreadsCrnt)        {            if(nThreadsBusy < m_nThreadsMax)            {                if(GetCPUUsage() < 75)                {                    CloseHandle(_beginthreadex(...));                }            }        }        if(!bOK && (dwIOError == WAIT_TIMEOUT))        {            bStayInPool = FALSE;        }        if(bOk || po)        {            if(GetCPUUsage() > 90)            {                if(g_nThreadsCrnt > g_nThreadsMin)                {                    bStayInPool = FALSE;                }            }        }    }    InterlockedDecrement(&g_nThreadsBusy);    InterlockedDecrement(&g_nThreadsCurrent);    return 0;}

-模拟已完成的I/O请求
I/O完成端口,不仅可用于设备I/O,也可用于线程间通信。

// 将一个已完成的I/O通知追加到I/O完成端口的队列中。BOOL PostQueuedCompletionStatus(HANDLE hCompletionPort,DWORD dwNumBytes,ULONG_PTR CompletionKey,OVERLAPPED *pOverlapped);

调用GetQueuedCompletionStatus的线程,在I/O完成端口有完成项时,会唤醒它的等待队列中的一个线程。
调用CloseHandle并传入一个完成端口的句柄时,系统将正等待GetQueuedCompletionStatus返回的线程唤醒,返回FALSE给它们,错误码为:ERROR_INVALID_HANDLE。

阅读全文
2 0