Windows系统编程(四):IO同步异步
来源:互联网 发布:无尽战区mac可以玩吗 编辑:程序博客网 时间:2024/06/08 13:53
作者:yurunsun@gmail.com 新浪微博@孙雨润 新浪博客 CSDN博客日期:2012年11月8日
1. 打开设备:CreateFile
CreateFile
是操作I/O最重要的函数,除了创建和打开磁盘文件,它同样可以打开许多其他设备。
HANDLE WINAPI CreateFile( LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
1.1 lpFileName
既表示设备的类型,也表示该类设备的某个实例。
1.2 dwDesiredAccess
用来指定以何种方式和设备进行数据传输。
NULL
不希望从设备读写数据,只是想改变设备的配置GENERIC_READ
对设备只读访问GENERIC_WRITE
对设备只写访问GENERIC_READ | GENERIC_WRITE
对设备读写(最常用)
1.3 dwShareMode
用来指定设备的共享特权。在尚未调用CloseHandle
之前,我们可以使用这个参数控制其他的CreateFile
调用。
NULL
要求独占对设备的访问。如果设备已经打开,CreateFile
调用会失败;如果成功打开,后续CreateFile
会失败(以下类似)FILE_SHARE_READ
要求其他内核对象不得修改设备的数据FILE_SHARE_WRITE
要求其他内核对象不得读取该设备的数据FILE_SHARE_READ
|FILE_SHARE_WRITE
不关心其他设备读写FILE_SHARE_DELETE
对文件进行操作时,不关心文件是否被逻辑删除或者移动。在OS内部会先将文件标记为待删除,然后当该文件所有已打开的句柄都被关闭时再将其真正删除
1.4 dwCreationDisposition
打开设备时此参数意义重大:
CREATE_NEW
创建一个新文件,如果存在同名则调用失败CREATE_ALWAYS
如果存在同名则覆盖原文件OPEN_EXISTING
如果文件/设备不存在则调用失败OPEN_ALWAYS
如果文件不存在则创建新文件TRUNCATE_EXISTING
打开一个已有文件并将文件大小截断为0字节,如果文件不存在则调用失败
1.5 dwFlagsAndAttributes
用来微调与设备之间的通信和属性,大多数是优化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
临时文件
2. 使用文件设备
2.1 取得文件大小
返回文件的逻辑大小:
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。
2.2 设置文件指针
每个文件内核对象有自己的文件指针:
BYTE pb[10];DWORD dwNumBytes;HANDLE hFile = CreateFile(_T("file.dat"), ...); // Point to 0ReadFile(hFile, pb, 10, &dwNumBytes, NULL); // Reads bytes 0-9ReadFile(hFile, pb, 10, &dwNumBytes, NULL); // Reads bytes 10-19HANDLE hFile2 = CreateFile(_T("file.dat"), ...); // Point to 0ReadFile(hFile2, pb, 10, &dwNumBytes, NULL); // Reads bytes 0-9HANDLE 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);
3. 同步IO操作
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
3.1 强制将数据从buffer刷进设备
BOOL WINAPI FlushFileBuffers(HANDLE hFile);
3.2 取消同步IO
Vista之后的OS提供下面API用户终止指定线程的同步IO请求:
BOOL WINAPI CancelSynchronousIo(HANDLE hThread);
hThread
句柄在创建时一定包含了THREAD_TERMINATE
访问权限- 如果
hThread
线程并不处于因为等待IO响应而Pending的状态,函数返回FALSE
- 取消IO请求取决于对应system layer实现的那个驱动程序,如果驱动不支持取消,函数调用还是返回
TRUE
,因为它已经完成请求任务
4. 异步IO操作
4.1 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
用来保存已传输的字节数。
4.2 异步IO的注意事项
驱动设备程序不一定以先入先出方式处理队列中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结构被释放。
4.3 取消队列中IO请求
OS提供了多种方式:
CancelIo
来取消由给定句柄标识的线程,所添加到队列中的所有IO请求,除了IOCPBOOL CancelIo(HANDLE hFile);
关闭设备句柄来取消已添加到队列中所有IO请求,无论由哪个线程添加
- 线程终止时OS会自动取消该线程发出的所有IO请求,除了IOCP
CancelIoEx
来取消指定文件句柄的指定IO请求BOOL CancelIoEx(HANDLE hFile, LPOVERLAPPED pOverlapped);
5. 异步IO完成通知
5.1 触发设备内核对象
一个很自然的想法是利用上一节线程中提到的设备内核对象,调用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的设计意图,但是展示了一些重要的概念,是对异步操作的一个总结。
5.2 触发事件内核对象
5.2.1 引入原因
设备内核对象不能处理多个IO请求:例如我们要从文件读取10个字节再写入10个字节,任何一个操作完成都会触发设备内核对象,而无法区分是读操作还是写操作完成。
5.2.2 事件内核对象
首先通过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
。
5.2.3 示例
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 completecase 1: break; // write complete}这段代码一样没有实用价值,无法体现异步的作用。
5.3 Alertable IO
5.3.1 Alertable IO
系统创建一个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
5.3.2 OS执行流程
以下边代码为例:
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
。
5.3.3 Alertable IO的优劣
缺点
- 回调函数:回调函数很难保存某个问题有关的上下文,导致可能需要大量全局变量/成员变量。
- 线程问题:发出IO请求的线程必须是执行回调函数的线程,无法调度
优点
能够手动添加回调函数到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并使其退出。
5.4 IOCP
首先考虑两种服务器模型:
串行:一个thread等待一个client request,当请求到达时线程被唤醒处理client request
并行:一个thread等待一个client request,当请求到达时创建一个新的thread来处理请求,同时进入下一次循环等待新的client
显而易见后者的高并发更受欢迎,但问题是这种模型使所有thread都处于runnable而非pending状态,OS浪费大量时间进行thread切换,因此Windows引入了IOCP来解决这个问题。
IOCP的两个理论基础:
- 并行thread数量必须有一个上限,因为一旦runnable thread > CPU数量,OS就必须进行thread切换而影响CPU cycle
- 使用线程池来避免为每个client创建新thread的开销,线程池内thread数量一般为CPU数量*2
5.4.1 创建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不关心。
5.4.2 IOCP在OS中的数据结构(DS)
DS1:设备列表:
hDevicedwCompletionKeyhDevice
->dwCompletionKey
的mapAssociateDeviceWithCompletionPort
时添加,设备Handle关闭时删除DS2:IO完成队列:
dwBytesTransferreddwCompletionKeypOverlappeddwErrorIO完成或`PostQueuedCompletionStatus时添加,IOCP从等待队列中删除时删除
DS3:等待线程队列(后进先出)
dwThreadIdthread调用
GetQueuedCompletionStatus
时添加,IO完成队列不空且此时runnable threads数小于dwNumberOfConcurrentThreads
则删除,并转移到DS4。DS4:已释放线程列表
dwThreadId从DS4转移过来,或暂停的thread被唤醒时添加;thread再次调用
GetQueuedCompletionStatus
时转移到DS3,或pending自己时转移到DS5DS5:已暂停线程列表
dwThreadId从DS4转移过来时添加;pending自己的thread被唤醒时转移回DS4
5.4.3 IOCP的工作流程
假设双核CPU
- 创建IOCP时指定并发线程数上限:2
- 创建线程池,拥有thread A B C D
- 每个thread初始化时调用
GetQueuedCompletionStatus
进入DS3 - 如果3个client的request已经完成,插入IO完成队列。然后DS3中的2个thread A B会被唤醒,转到DS4,执行2个client的回调函数;另两个C D继续睡眠
- thread A处理完继续调用
GetQueuedCompletionStatus
试图休眠进入DS3,这时OS发现IO完成队列中还有第三个client的request的回调任务,会再次被唤醒。 - 在执行回调过程中的runable thread,即在DS4中的thread(例如thread B)如果调用了
Sleep/WaitFor*
等API使自己pending,则会转移到DS5;OS立即检测到一个runnable thread将自己暂停,为了保证以2个thread为上限前提下满负荷运行,出现下一个完成端口的回调任务时OS即刻从DS3中唤醒thread C进行处理。 - 此时thread B恢复runnable 状态,导致DS4中数量变成3,超过了并发线程数上限2。IOCP系统允许在短时间内出现这种情况,对应的策略是:在runnable threads <= 2之前不会再唤醒任何thread
5.4.3 实例代码
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一起打造一个简洁高并发的服务器框架。
- 如果这篇文章对您有帮助,请到CSDN博客留言;
- 转载请注明:来自雨润的技术博客 http://blog.csdn.net/sunyurun
- Windows系统编程(四):IO同步异步
- 《Windows核心编程》之“同步IO和异步IO”
- Windows核心编程笔记(十)同步IO 与 异步IO
- win32串口同步编程、异步编程(重叠IO)
- 《Windows核心编程系列》十谈谈同步设备IO与异步设备IO之异步IO
- 《Windows核心编程系列》十谈谈同步设备IO与异步设备IO之异步IO
- 《Windows核心编程系列》九谈谈同步设备IO与异步设备IO之同步设备IO
- 《Windows核心编程系列》九谈谈同步设备IO与异步设备IO之同步设备IO
- windows 核心编程之 10 同步设备IO与异步设备IO
- Windows异步IO四种方式
- 同步IO,异步IO
- 同步IO 异步IO
- 同步异步IO
- IO同步与异步
- IO同步与异步
- IO同步异步!
- 同步IO和异步IO
- 同步IO与异步IO
- uva 10004 双着色
- 快速排序
- 海量数据搜索算法优化-
- Android 文件打开的intent
- Excel Sheet Row Numbers
- Windows系统编程(四):IO同步异步
- 今日总结11_10
- [自扫盲]skype、IP电话、VOIP、网络电话、互联网电话、IP拨号
- 【phpcms-v9】获取当前栏目下周点击量最高的三篇带缩略图的文章
- 织梦自动增加数值标签
- WordPress文章中添加上一篇、下一篇链接专题研究
- Android通过webservice连接SQLServer 详细教程(数据库+服务器+客户端)
- hdu 1540 区间合并
- perl应用:从NCBI提供的信息中获取需要的序列(下)use Scalar