作者:[email protected] 新浪微博@孙雨润 新浪博客 CSDN博客日期:2012年11月8日
CreateFile
是操作I/O最重要的函数,除了创建和打开磁盘文件,它同样可以打开许多其他设备。
HANDLE WINAPI CreateFile(
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
既表示设备的类型,也表示该类设备的某个实例。
用来指定以何种方式和设备进行数据传输。
NULL
不希望从设备读写数据,只是想改变设备的配置GENERIC_READ
对设备只读访问GENERIC_WRITE
对设备只写访问GENERIC_READ | GENERIC_WRITE
对设备读写(最常用)用来指定设备的共享特权。在尚未调用CloseHandle
之前,我们可以使用这个参数控制其他的CreateFile
调用。
NULL
要求独占对设备的访问。如果设备已经打开,CreateFile
调用会失败;如果成功打开,后续CreateFile
会失败(以下类似)FILE_SHARE_READ
要求其他内核对象不得修改设备的数据FILE_SHARE_WRITE
要求其他内核对象不得读取该设备的数据FILE_SHARE_READ
| FILE_SHARE_WRITE
不关心其他设备读写FILE_SHARE_DELETE
对文件进行操作时,不关心文件是否被逻辑删除或者移动。在OS内部会先将文件标记为待删除,然后当该文件所有已打开的句柄都被关闭时再将其真正删除打开设备时此参数意义重大:
CREATE_NEW
创建一个新文件,如果存在同名则调用失败CREATE_ALWAYS
如果存在同名则覆盖原文件OPEN_EXISTING
如果文件/设备不存在则调用失败OPEN_ALWAYS
如果文件不存在则创建新文件TRUNCATE_EXISTING
打开一个已有文件并将文件大小截断为0字节,如果文件不存在则调用失败用来微调与设备之间的通信和属性,大多数是优化OS缓存算法提高效率。
缓存相关
FILE_FLAG_NO_BUFFERING
在访问文件时不要使用任何数据缓存。通常不使用此flag以提高性能;打开此flag可以提高内存的使用效率。非特殊场合不要打开!FILE_FLAG_SEQUENTIAL_SCAN
当使用缓存时,告诉OS我们将顺序访问文件FILE_FLAG_RANDOM_ACCESS
当使用缓存时,告诉OS我们不保证顺序访问文件FILE_FLAG_WRITE_THROUGH
禁止对文件写入操作进行缓存,而是将修改直接写入磁盘,能减少数据丢失的可能杂项
FILE_FLAG_DELETE_ON_CLOSE
让文件系统在此文件所有句柄都关闭后删除该文件,通常作为临时文件与FILE_ATTRIBUTE_TEMPORARY
一起使用
FILE_FLAG_BACKUP_SEMANTICS
用于备份和恢复软件,跳过文件安全性检查,但是需要调用者的access token具备对文件/目录进行备份/恢复的权限
FILE_FLAG_POSIX_SEMANTICS
按照POSIX要求,查找/打开文件时区分大小写
FILE_FLAG_OVERLAPPED
以异步方式访问设备
文件属性
FILE_ATTRIBUTE_ARCHIVE
创建时自动设置,表示是一个存档文件FILE_ATTRIBUTE_ENCRYPTED
文件经过加密FILE_ATTRIBUTE_HIDDEN
隐藏文件FILE_ATTRIBUTE_NORMAL
仅在单独使用时表示此文件没有其他属性FILE_ATTRIBUTE_READONLY
只读FILE_ATTRIBUTE_SYSTEM
系统文件FILE_ATTRIBUTE_TEMPORARY
临时文件返回文件的逻辑大小:
BOOL WINAPI GetFileSizeEx(HANDLE hFile, PLARGE_INTEGER lpFileSize);
返回文件的物理大小:
DWORD WINAPI GetCompressedFileSize(LPCTSTR lpFileName, LPDWORD lpFileSizeHigh);
ULARGE_INTEGER ulFileSize;
ulFileSize.LowPart = GetCompressedFileSize(_T("filename.dat"), &ulFileSize.HighPart);
例如100KB文件压缩后占85K,前者返回100K后者返回85K。
每个文件内核对象有自己的文件指针:
BYTE pb[10];
DWORD dwNumBytes;
HANDLE hFile = CreateFile(_T("file.dat"), ...); // Point to 0
ReadFile(hFile, pb, 10, &dwNumBytes, NULL); // Reads bytes 0-9
ReadFile(hFile, pb, 10, &dwNumBytes, NULL); // Reads bytes 10-19
HANDLE hFile2 = CreateFile(_T("file.dat"), ...); // Point to 0
ReadFile(hFile2, pb, 10, &dwNumBytes, NULL); // Reads bytes 0-9
HANDLE hFile3;
DuplicateHandle(GetCurrentProcess(), hFile2, GetCurrentProcess(), &hFile3);
ReadFile(hFile3, pb, 10, &dwNumBytes, NULL); // Reads bytes 10-19
随机访问文件
BOOL WINAPI SetFilePointerEx(
HANDLE hFile,
LARGE_INTEGER liDistanceToMove,
PLARGE_INTEGER lpNewFilePointer,
DWORD dwMoveMethod
);
设置文件尾:强制使文件尾变得更小或更大:
BOOL WINAPI SetEndOfFile(HANDLE hFile);
BOOL WINAPI ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
);
BOOL WINAPI WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
【Note】hFile
在创建时一定不要指定FILE_FLAG_OVERLAPPED
,否则该句柄会执行异步IO;同理如果想执行异步IO,一定记得创建时指定FILE_FLAG_OVERLAPPED
BOOL WINAPI FlushFileBuffers(HANDLE hFile);
Vista之后的OS提供下面API用户终止指定线程的同步IO请求:
BOOL WINAPI CancelSynchronousIo(HANDLE hThread);
hThread
句柄在创建时一定包含了THREAD_TERMINATE
访问权限hThread
线程并不处于因为等待IO响应而Pending的状态,函数返回FALSE
TRUE
,因为它已经完成请求任务OVERLAPPED
结构typedef struct _OVERLAPPED {
ULONG_PTR Internal; // [out] Error code
ULONG_PTR InternalHigh; // [out] Number of bytes transferred
union {
struct {
DWORD Offset; // [in] Low 32-bit file offset
DWORD OffsetHigh; // [in] High 32-bit file offset
};
PVOID Pointer;
};
HANDLE hEvent; // Event handle or data
} OVERLAPPED, *LPOVERLAPPED;
Offset, OffsetHigh, hEvent
必须在调用ReadFile/WriteFile
之前完成初始化,Internal, InternalHigh
由驱动程序设置。
Offset, OffsetHigh
这两个成员构成一个64位偏移量,表示IO操作的起点。之所以要求在OVERLAPPED
中指定起点是因为对于多次异步调用,OS无法确定第二次之后的起点,这与同步IO不同。非文件设备会忽略Offset, OffsetHigh
,调用时必须将其初始化为0,否则IO请求会失败。
hEvent
用来接收IO完成通知的4种方法之一会用到hEvent
,一般在其中保存一个C++对象地址,后续会进一步介绍。
Internal
用来保存已处理的IO请求的错误码,初始为STATUS_PENDING
,下面宏可以检查IO是否完成:
#define HasOverlappedIoCompleted(pOverlapped) \
((pOverlapped)->Internal != STATUS_PENDING)
InternalHigh
用来保存已传输的字节数。
驱动设备程序不一定以先入先出方式处理队列中IO请求,例如:
OVERLAPPED o1 = {0};
OVERLAPPED o2 = {0};
BYTE bBuffer[100];
ReadFile(hFile, bBuffer, 100, NULL, &o1};
WriteFile(hFile, bBuffer, 100, NULL, &o1};
驱动程序可能先Write后Read
使用正确方式检查错误:
大多数WindowsAPI返回FALSE
表示失败,但ReadFile
和WriteFile
不同。当我们试图将一个异步IO请求添加到队列中时,驱动程序可能会选择以同步方式处理请求,例如当我们想要的数据已经在OS的缓存中。如果被以同步方式执行,则ReadFile/WriteFile
会返回非零值,如果被以异步方式执行,或执行中发生错误,则返回FALSE
,这是要根据GetLastError
是否为ERROR_IO_PENDING
来确定是否成功加入队列。
在异步IO请求完成之前一定不能移动或销毁在发出请求时使用的数据缓存和OVERLAPPED
结构
OS将IO请求加入驱动设备程序队列中时会传入数据缓存和OVERLAPPED结构的地址。如下代码就是错误的:
VOID ReadData(HANDLE hFile) {
OVERLAPPED o = {0};
BYTE b[100];
ReadFile(hFile, b, 100, NULL &o);
}
问题在于当异步IO请求被加入队列之后函数返回,栈上缓存和OVERLAPPED结构被释放。
OS提供了多种方式:
CancelIo
来取消由给定句柄标识的线程,所添加到队列中的所有IO请求,除了IOCP
BOOL CancelIo(HANDLE hFile);
关闭设备句柄来取消已添加到队列中所有IO请求,无论由哪个线程添加
CancelIoEx
来取消指定文件句柄的指定IO请求
BOOL CancelIoEx(HANDLE hFile, LPOVERLAPPED pOverlapped);
一个很自然的想法是利用上一节线程中提到的设备内核对象,调用WaitForSingleObject
:
HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);
BYTE bBuffer[100];
OVERLAPPED o = {0};
o.Offset = 345;
BOOL bReadDone = ReadFile(hFile, bBuffer, 100, NULL, &o);
DWORD dwError = GetLastError();
if (!bReadDone && (dwError == ERROR_IO_PENDING)) {
// IO正在以异步方式执行,等待完成
WaitForSingleObject(hFile, INFINITE);
bReadDone = TRUE;
}
if (bReadDone) {
// 读出oInternal, oInternalHigh, bBuffer
} else {
// 读出dwError
}
这段代码枉费了异步IO的设计意图,但是展示了一些重要的概念,是对异步操作的一个总结。
设备内核对象不能处理多个IO请求:例如我们要从文件读取10个字节再写入10个字节,任何一个操作完成都会触发设备内核对象,而无法区分是读操作还是写操作完成。
首先通过CreateEvent
创建事件对象并赋给OVERLAPPED
的hEvent
,这样驱动程序在完成异步IO后会调用SetEvent
触发事件。当然5.1中的设备内核对象同样会触发,但是不要去等待。为了略微提高性能,可以禁用5.1的设备内核对象触发:
UCHAR flag = FILE_SKIP_SET_EVENT_ON_HANDLE;
BOOL SetFileCompletionNotificationModes(HANDLE FileHandle, UCHAR Flags);
现在如果想要同时执行多个异步设备IO请求,先要为每个请求创建不同的事件对象,并初始化每个请求的OVERLAPPED
结构的hEvent
成员,再调用ReadFile/WriteFile
,在需要同步的地方调用WaitForMultipleObjects
。
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; // read complete
case 1: break; // write complete
}
这段代码一样没有实用价值,无法体现异步的作用。
系统创建一个thread时会同时创建一个thread相关的队列,称为异步过程调用(APC)队列。发出IO请求时让驱动程序在APC队列添加一项回调函数:
BOOL ReadFileEx(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPOVERLAPPED lpOverlapped,
LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
BOOL WriteFileEx(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPOVERLAPPED lpOverlapped,
LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
typedef VOID (WINAPI *LPOVERLAPPED_COMPLETION_ROUTINE)(
DWORD dwErrorCode,
DWORD dwNumberOfBytesTransfered,
LPOVERLAPPED lpOverlapped);
与ReadFile/WriteFile
的不同点是:表示已传输字节数的输出参数移到了回调函数中,当然这就要求提供一个回调函数lpCompletionRoutine
以下边代码为例:
hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);
ReadFileEx(hFile, ...);
WriteFileEx(hFile, ...);
ReadFileEx(hFile, ...);
SomeFunc();
假设SomeFunc()执行需要一段时间,返回前OS就完成了3个异步IO。驱动则正在讲已完成的IO一个个添加到线程的APC队列中,注意添加顺序与调用顺序无关。而thread必须将自己置为可唤醒状态,才能出发APC队列中的函数回调,以下6个API可以将自身置为可唤醒:
DWORD SleepEx(DWORD dwMilliseconds,BOOL bAlertable);
DWORD WaitForSingleObjectEx(HANDLE hHandle, DWORD dwMilliseconds, BOOL bAlertable);
DWORD WaitForMultipleObjectsEx(
DWORD nCount,
CONST HANDLE *lpHandles,
BOOL bWaitAll,
DWORD dwMilliseconds,
BOOL bAlertable);
DWORD SignalObjectAndWait(
HANDLE hObjectToSignal,
HANDLE hObjectToWaitOn,
DWORD dwMilliseconds,
BOOL bAlertable);
BOOL GetQueuedCompletionStatusEx(
HANDLE CompletionPort,
lpCompletionPortEntries,
ULONG ulCount,
PULONG ulNumEntriesRemoved,
DWORD dwMilliseconds,
BOOL fAlertable
);
MsgWaitForMultipleObjectsEx(
DWORD nCount,
CONST HANDLE *pHandles,
DWORD dwMilliseconds,
DWORD dwWakeMask,
DWORD dwFlags);
前5个函数的bAlertable
设置为TRUE
;MsgWaitForMultipleObjectsEx
则使用MWMO_ALERTABLE
让thread进入可提醒状态。Sleep/WaitForSingleObject/WaitForMultipleObjects
在内部调用了*Ex
的对应版本,并总将bAlertable
置为FALSE
。
缺点
优点
能够手动添加回调函数到APC队列:
DWORD QueueUserAPC(PAPCFUNC pfnAPC, HANDLE hThread, ULONG_PTR dwData);
typedef void ( __stdcall *PAPCFUNC )(DWORD_PTR dwParam);
使用hThread
允许跨线程、跨进程;跨进程时pfnAPC
必须在hThread
所处的地址空间中,此API可以用于非常高效的线程/进程间通信。
能够强制让线程退出Pending状态:
加入某线程处于WaitForSingleObject
等待内核对象被触发,则QueueUserAPC
能够干净地唤醒此thread并使其退出。
首先考虑两种服务器模型:
串行:一个thread等待一个client request,当请求到达时线程被唤醒处理client request
并行:一个thread等待一个client request,当请求到达时创建一个新的thread来处理请求,同时进入下一次循环等待新的client
显而易见后者的高并发更受欢迎,但问题是这种模型使所有thread都处于runnable而非pending状态,OS浪费大量时间进行thread切换,因此Windows引入了IOCP来解决这个问题。
IOCP的两个理论基础:
HANDLE CreateIoCompletionPort(
__in HANDLE FileHandle,
__in_opt HANDLE ExistingCompletionPort,
__in ULONG_PTR CompletionKey,
__in DWORD NumberOfConcurrentThreads
);
从可读性角度,一般将此API拆分成两步:
创建IOCP
HANDLE CreateNewCompletionPort(DWORD dwNumberOfConcurrentThreads)
{
return CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, dwNumberOfConcurrentThreads);
}
这个函数唯一参数dwNumberOfConcurrentThreads
告诉IOCP并行thread数量上限,传0则IOCP会使用CPU数量作为默认值。
绑定设备
BOOL AssociateDeviceWithCompletionPort(HANDLE hCompletionPort, HANDLE hDevice, DWORD dwCompletionKey)
{
HANDLE h = CreateIoCompletionPort(hDevice, hCompletionPort, dwCompletionKey, 0);
return (h == hCompletionPort) ? TRUE : FALSE;
}
其中hCompletionPort
为刚创建的Handle, hDevice
为设备Handle, dwCompletionKey
为对我们有意义的完成key,OS不关心。
DS1:设备列表:hDevice
-> dwCompletionKey
的map
hDevice | dwCompletionKey |
AssociateDeviceWithCompletionPort
时添加,设备Handle关闭时删除
DS2:IO完成队列:
dwBytesTransferred | dwCompletionKey | pOverlapped | dwError |
IO完成或`PostQueuedCompletionStatus时添加,IOCP从等待队列中删除时删除
DS3:等待线程队列(后进先出)
dwThreadId |
thread调用GetQueuedCompletionStatus
时添加,IO完成队列不空且此时runnable threads数小于dwNumberOfConcurrentThreads
则删除,并转移到DS4。
DS4:已释放线程列表
dwThreadId |
从DS4转移过来,或暂停的thread被唤醒时添加;thread再次调用GetQueuedCompletionStatus
时转移到DS3,或pending自己时转移到DS5
DS5:已暂停线程列表
dwThreadId |
从DS4转移过来时添加;pending自己的thread被唤醒时转移回DS4
假设双核CPU
GetQueuedCompletionStatus
进入DS3GetQueuedCompletionStatus
试图休眠进入DS3,这时OS发现IO完成队列中还有第三个client的request的回调任务,会再次被唤醒。Sleep/WaitFor*
等API使自己pending,则会转移到DS5;OS立即检测到一个runnable thread将自己暂停,为了保证以2个thread为上限前提下满负荷运行,出现下一个完成端口的回调任务时OS即刻从DS3中唤醒thread C进行处理。class CIOCP {
public:
CIOCP(int nMaxConcurrency = -1) {
m_hIOCP = NULL;
if (nMaxConcurrency != -1)
(void) Create(nMaxConcurrency);
}
~CIOCP() {
if (m_hIOCP != NULL)
CloseHandle(m_hIOCP);
}
BOOL Close() {
BOOL bResult = CloseHandle(m_hIOCP);
m_hIOCP = NULL;
return(bResult);
}
BOOL Create(int nMaxConcurrency = 0) {
m_hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, nMaxConcurrency);
return m_hIOCP != NULL;
}
BOOL AssociateDevice(HANDLE hDevice, ULONG_PTR CompKey) {
return (CreateIoCompletionPort(hDevice, m_hIOCP, CompKey, 0) == m_hIOCP);
}
BOOL AssociateSocket(SOCKET hSocket, ULONG_PTR CompKey) {
return AssociateDevice((HANDLE) hSocket, CompKey);
}
BOOL PostStatus(ULONG_PTR CompKey, DWORD dwNumBytes = 0, OVERLAPPED* po = NULL) {
return PostQueuedCompletionStatus(m_hIOCP, dwNumBytes, CompKey, po);
}
BOOL GetStatus(ULONG_PTR* pCompKey, PDWORD pdwNumBytes, OVERLAPPED** ppo, DWORD dwMilliseconds = INFINITE) {
return GetQueuedCompletionStatus(m_hIOCP, pdwNumBytes, pCompKey, ppo, dwMilliseconds);
}
private:
HANDLE m_hIOCP;
};
这里仅仅列出了对IOCP的常用封装,后续章节将介绍如何使用IOCP与WinSock一起打造一个简洁高并发的服务器框架。