第六章:Overlapped I/O,在你身后变戏法
1.overlapped I/O 是 Win32 的一项技术,你可以要求操作系统为你传送数据,并且在传送完毕时通知你。这项技术使你的程序在I/O 进行过程中仍然能够继续处理事务。事实上,操作系统内部正是以线程来完成 overlapped I/O。
2.Win32文件操作函数
(1)CreateFile()可以用来打开文件、串行口和并行口、Named pipes、Console。
HANDLE CreateFile(
LPCTSTR lpFileName, // 指向文件名称
DWORD dwDesiredAccess, // 存取模式(读或写)
DWORD dwShareMode, // 共享模式(share mode)
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 指向安全属性结构
DWORD dwCreationDisposition, // 如何产生
DWORD dwFlagsAndAttributes, // 文件属性
HANDLE hTemplateFile // 一个临时文件,将拥有全部的属性拷贝
);
注:overlapped I/O性质:可以在同一时间读写文件的许多部分;多个overlapped请求,执行次序无法保证;overlapped I/O的基本型式是以ReadFile()和WriteFile()完成的。
(2)ReadFile()
BOOL ReadFile(
HANDLE hFile, // 欲读之文件
LPVOID lpBuffer, // 接收数据之缓冲区
DWORD nNumberOfBytesToRead, // 欲读取的字节个数
LPDWORD lpNumberOfBytesRead, // 实际读取的字节个数的地址
LPOVERLAPPED lpOverlapped // 指针,指向 overlapped info
);
(3)WriteFile()
BOOL WriteFile(
HANDLE hFile, // 欲写之文件
LPCVOID lpBuffer, // 储存数据之缓冲区
DWORD nNumberOfBytesToWrite, // 欲写入的字节个数
LPDWORD lpNumberOfBytesWritten, // 实际写入的字节个数的地址
LPOVERLAPPED lpOverlapped // 指针,指向 overlapped info
);
如果CreateFile() 的第6个参数被指定为FILE_FLAG_ OVERLAPPED,就必须在上述的 lpOverlapped 参数中提供一个指针,指向一个 OVERLAPPED 结构。
(4)OVERLAPPED结构
typedef struct _OVERLAPPED {
DWORD Internal; //通常保留。当GetOverlappedResult传回False,并且GetLastError并非传回ERROR_IO_PENDING,则内含一个视系统而定的状态。
DWORD InternalHigh; //通常被保留,当GetOverlappedResult传回True,则内含“被传输数据的长度”
DWORD Offset; //读、写偏移位置,从文件头开始算起。若目标设备不支持文件位置,忽略
DWORD OffsetHigh; //64位文件偏移中较高32位。若目标设备不支持文件位置,忽略
HANDLE hEvent; //manual reset event,overlapped I/O完成时被激发。ReadFileEX,WriteFileEX忽略这个栏位,彼时被用来传递一个用户自定义的指针。
} OVERLAPPED, *LPOVERLAPPED;
OVERLAPPED 结构执行两个重要的功能。第一,它像一把钥匙,用以识别每一个目前正在进行的 overlapped 操作。第二,它在你和系统之间提供了一个共享区域,参数可以在该区域中双向传递。
通常overlapped结构存放在heap中。
3.被激发的File Handles
(1)异步IO的步骤:CreateFile指定FILE_FLAG_OVERLAPPED;设立一个OVERLAPPED结构,调用ReadFile、WriteFile带上这个参数。
(2)文件handle是一个核心对象,一旦操作完毕即被激发。
(3)GetOverlappedResult()
BOOL GetOverlappedResult(
HANDLE hFile, //文件设备的handle
LPOVERLAPPED lpOverlapped, //一个指针,指向overlapped结构
LPDWORD lpNumberOfBytesTransferred, //一个指针,指向DWORD,保存真正被传输的字节数。
BOOL bWait //是否要等待操作完成,TRUE表示等待。
);
(4)虽然你要求一个overlapped 操作,但它并不一定就是 overlapped!如果数据已经被放进 cache中,或如果操作系统认为它可以很快速地取得那份数据,那么文件操作就会在ReadFile() 返回之前完成,而 ReadFile() 将传回 TRUE。
(5)一个文件操作为 overlapped,而操作系统把“操作请求”放到队列中等待执行, ReadFile() 和、WriteFile()都会传回 FALSE 以示失败。这个行为并不是很直观, 你必须调用GetLastError() 并确定它传回 ERROR_IO_PENDING,那意味着“overlappedI/O 请求”被放进队列之中等待执行。GetLastError() 也可能传回其他的值,例如 ERROR_HANDLE_EOF,那就真正代表一个错误了。
4.被激发的event对象
(1)所使用的 event 对象必须是手动重置(manual-reset)而非自动重置(auto-reset)。
(2)IOBYEVENT例子。
5.异步过程调用(Asynchronous Procedure Calls,APCs)
(1)使用overlapped I/O与event搭配的两个问题:
<1>WaitForMultipleObjects最多等待64个对象。
<2>必须不断的根据“哪一个handle被激发”而计算如何反应。
(2)使用Ex版的ReadFile和WriteFile,可以使用异步过程调用机制。只有当线程处于alertable状态时,APCs才会被调用。当线程因为以下5个函数而处于等待状态,且线程的“alertable”标记被设为TRUE,则线程处于alertable状态:
SleepEx()
WaitForSingleObjectEx()
WaitForMultipleObjectEx()
MsgWaitForMultipleObjectsEx()
SignalObjectAndWait();
(3)用于 overlapped I/O 的 APCs 是一种所谓的 user mode APCs。WindowsNT 另有一种所谓的 kernel mode APCs。Kernel mode APCs 也会像 usermode APCs 一样被保存起来,但一个 kernel mode APC 一定会在下一个timeslice 被调用,不管线程当时正在做什么。 Kernel mode APCs 用来处理系统机能,不在应用程序的控制之中。
(4)提供的 I/O completion routine 应该有这样的型式:
VOID WINAPI FileIOCompletionRoutine(
DWORD dwErrorCode, //0表示操作完成,ERROR_HANDLE_EOF表示操作已经到了文件尾端。
DWORD dwNumberOfBytesTransferred,//真正被传输的数据字节数
LPOVERLAPPED lpOverlapped//指向overlapped结构,此结构由开启overlapped I/O操作的函数提供
);
(5)使用 APCs 时,OVERLAPPED 结构中的 hEvent 栏位不需要用来放置一个 event handle。Win32 文件上说此时 hEvent 栏位可以由程序员自由运用。那么最大的用途就是:首先配置一个结构,描述数据来自哪里,或是要对数据进行一些什么操作,然后将 hEvent 栏位设定指向该结构
(6)在C++ 中产生一个I/O Completion Routines:储存一个指针,指向用户自定义数据(一个对象),然后经由此指针调用一个 C++ 成员函数。由于 static 成员函数是类的一部分,你还是可以调用 private 成员函数。
6.对文件进行overlapped I/O的缺点
(1)似乎 Windows NT 是以“I/O 请求”的大小来决定要不要将此请求先记录下来。所以对于数据量小的操作,overlapped I/O的效率反而更低。
(2)解决办法:以少量的线程负责所有的硬盘 I/O,然后把这些线程的I/O 请求,保持在一个队列之中。这种效率比较高。
(3)有两种情况,overlapped I/O 总是同步执行,甚至即使 FILE_FLAG_NO_BUFFERING 已经指定。第一种情况是你进行一个写入操作而造成文件的扩展。第二种情况是你读写一个压缩文件。
7.I/O Completion Ports
(1)APCs的缺点:最大的问题就是,有好几个 I/O APIs 并不支持 APCs,如listen() 和 WaitCommEvent() 便是两个例子。APCs 的另一个问题是,只有发出“overlapped 请求”的那个线程才能够提供 callback 函数,然而在一个“scalable”(译注)系统中,最好任何线程都能够服务 events。
(2)产生一个I/O Completion Port
HANDLE CreateIoCompletionPort(
HANDLE FileHandle, //文件或设备的handle,若为INVALID_HANDLE_VALUE,则产生一个没有和任何handle关联的port。
HANDLE ExistingCompletionPort, //若此栏位被指定,则FileHandle被加到此port上。指定Null产生一个新的port。
DWORD CompletionKey, //用户自定义的一个数值,将被交给提供服务的线程。此值和FileHanlde有关联。
DWORD NumberOfConcurrentThreads//与此I/O completion port 有关联的线程个数。
);
(3)与一个文件handle产生关联
再次使用CreateIoCompletionPort接口。
(4)在一个I/O Completion Port上等待
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort, //将在其上等待completion port。
LPDWORD lpNumberOfBytesTransferred, //指向DWORD,收到“被传输的数据字节数”。
LPDWORD lpCompletionKey, //指向DWORD,该DWORD将收到由CreateIoCompletionPort定义的key。
LPOVERLAPPED *lpOverlapped, //overlapped结构指针的地址。
DWORD dwMilliseconds //等待的最长时间,时间终了,lpOverlapped被设为NULL,函数传回FALSE。
);
在completion port上等待的线程是以先进后出的次序提供服务。
(5)避免Completion Packets
设定一个 OVERLAPPED 结构,内含一个合法的手动重置(manual-reset)event 对象,放在 hEvent 栏位。然后把该 handle 的最低位设为 1。
overlap.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
overlap.hEvent = (HANDLE)((DWORD)overlap.hEvent | 0x1);
WriteFile(hFile, buffer, 128,& dwBytesWritten, &overlap);
8.对Sockets使用Overlapped I/O
分析ECHO例子,多实践socket IOCP。