同步和异步设备I/O

1.打开和关闭设备对象
      打开设备     CreateFile CreateMailslot CreateNamedPipe CreatePipe
      关闭设备      CloseHandle CloseSocket
      查看设备类型 GetFileType
 
各个设备的创建函数使用:  
File  
CreateFile (pszName is pathname or UNC pathname).
Directory
CreateFile (pszName is directory name or UNC directory name).
Logical disk drive
CreateFile (pszName is "\\.\x:").  
Physical disk drive
CreateFile (pszName is "\\.\PHYSICALDRIVEx").
Serial port
CreateFile (pszName is "COMx").
Parallel port
CreateFile (pszName is "LPTx").
Mailslot server
CreateMailslot (pszName is "\\.\mailslot\mailslotname").
Mailslot client
CreateFile (pszName is "\\servername\mailslot\mailslotname").
Named pipe server
CreateNamedPipe (pszName is "\\.\pipe\pipename").
Named pipe client
CreateFile (pszName is "\\servername\pipe\pipename").
Anonymous pipe
CreatePipe client and server.
Socket
socket, accept, or AcceptEx.
Console
CreateConsoleScreenBuffer or GetStdHandle.
 
2.文件设备 example:
   HANDLE hFile = CreateFile(...);
   BYTE pb[10];
   DWORD dwNumBytes;
   ReadFile(hFile1, pb, 10, &dwNumBytes, NULL);
   LARGE_INTEGER liDistanceToMove;
   liDistanceToMove.QuadPart = 1024;
   SetFilePointerEx(hFile, liDistanceToMove, NULL, FILE_BEGIN);
   SetEndOfFile(hFile);
   CloseHandle(hFile);
 
3.同步设备I/O
      ReadFile 仅当设备以GENERIC_READ 方式打开时才可调用ReadFile
      WriteFile 仅当设备以GENERIC_WRITE 方式打开时才可调用WriteFile
      FlushFileBuffers  强迫系统将缓冲区的数据写设备。
       CancelSynchronousIo 如果某个线程因等待I/O 操作而挂起太长时间, 可以调用该函数使线程从I/O 操作中返回。
 该线程必须符合以下两个条件中的一个:
           (1) 该线程是则CreateThread _beginThread 函数创建
           (2) 该线程则OpenThread 创建且具有THREAD_TERMINATE 权限
I/O 设备Cancel 操作最终由设备驱动实现。若设备驱动不支持I/O Cancel. 那么CancelSynchronouslo 永远返回True.
 
­
4.异步设备I/O基本概念
  Overlapped I/O的设计的目的:
       取代多线程功能,(多线程存在同步机制,错误处理,在成千上万个线程 I/O中,线程上下文切换是十分消耗CPU资源的).  Overlapped I/O模型是 OS为你传递数据,完成上下文切换,在处理完之后通知你。由程序中的处理,变为OS的处理。内部也是用线程处理的。
      (1) 异步请求要求用CreateFile FILE_FLAG_OVERLAPPED OVERLAPPED 结构体为参数打开设备,
      (2)OVERLAPPED( 重迭) 结构体
typedef struct _OVERLAPPED {
    DWORD   Internal;      [out] 通常被保留,当 GetOverlappedResult()传回False并且GatLastError()并非传回ERROR_IO_PENDINO时,该状态置为系统定的状态。
DWORD    InternalHigh; [out]  通常被保留,当 GetOverlappedResult()传回False时,为
                        被传输数据的长度。
DWORD    Offset;        [in] 指定文件的位置,从该位置传送数据,文件位置是相对文件开始
处的字节偏移量。调用 ReadFile或WriteFile函数之前调用进
程设置这个成员,读写命名管道及通信设备时调用进程忽略这
个成员;
DWORD    OffsetHigh;    [in] 指定开始传送数据的字节偏移量的高位字,读写命名管道及通
信设备时调用进程忽略这个成员;
HANDLE hEvent;         [in] 标识事件,数据传送完成时把它设为信号状态,调用ReadFile
                          WriteFile   ConnectNamedPipe   TransactNamedPipe 函数
前,调用进程设置这个成员. 相关函数 
CreateEvent  ResetEvent   GetOverlappedResult  
WaitForSingleObject   CWinThread   GetLastError   
} OVERLAPPED, *LPOVERLAPPED;
 
二个重要功能:
1. 标识每个正在 overlapped 的操作。
2. 程序和系统之间提供了共享区域。参数可以在区域内双向传递。
 
OVERLAPPED和数据缓冲区释放问题 :
在请求时,不能释放,只有在 I/O请求完成之后,才可以释放。如果发出多个overlapped请求,每个overlapped读写操作,都必须包含文件位置(socket),另外,如果有多个磁盘,I/O执行次序无法保证。(每个overlapped都是独立的请求操作)。
 
      HasOverlappedIoCompleted 宏用于判断I/O 请求是否执行完毕
 
      (3) 取消异步请求
         BOOL CancelIo(HANDLE hFile);
         BOOL CancelIoEx(HANDLE hFile, LPOVERLAPPED pOverlapped);.
­
5.获取I/O操作完毕通知 四种方法
 
      (1) 通过设备内核对象获取通知
          设备对象也是内核对象。在调用ReadFile WriteFile 时,设备对象会被置成Non-Signal 状态。当完成读、写操作后设备对象会被置成Signal 状态。 因此可以用WaitForSingleObject WaitForMultipleObject 获取I/O 操作完毕的通知。
  问题:不能区分那一个 overlapped操作,对同一个文件handle,系统有多个异步操作时(一边读文件头,一边写文件尾, 有一个完成,就会有信号,不能区分是那种操作。),为每个进行中的overlapped调用GetOverlappedResult是不好的作法。
      (2) 事件内核对象多个I/O 异步请求时,1不能实现
         OVERLAPPED 结构体中有一个成员是Event 对象的句柄。当I/O 操作完毕后 相应的OVERLAPPED 对象中Event 对象句柄所指向的对象也会被置成Signal 状态。 因此,当同时有多个 I/O 异步请求, 通过WaitForMultipleObject 等待每个I/O 操作中所对应的OVERLAPPED 对象的Event 句柄即可获得I/O 操作完毕的通知。
    事件内核对象在使用 WaitForMultipleObjects时,只能等待64个对象。需要另建两个数据组,并gOverlapped[nIndex].hEvent = ghEvents[nIndex]绑定起来。
BOOL GetOverlappedResult(  //处理情况
   HANDLE      hFile,
   OVERLAPPED* pOverlapped,
   PDWORD      pdwNumBytes,
   BOOL        bWait);
­
3) Alertable I/O(避免使用)
原因:
Callback functions 需要全局变量
Threading issues 同一个线程内,请求并处理IO完成通知
­
      线程创建时会关联一个队列 APC(asynchronous procedure call ),注意下面函数的使用
QueueUserAPC would be the only way to force the thread out of a wait state.
DWORD QueueUserAPC(  // manually queue an entry to a thread's APC queue
   PAPCFUNC  pfnAPC,
   HANDLE    hThread,
   ULONG_PTR dwData);
­
Alertable I/O的使用:
BOOL ReadFileEx(
   HANDLE      hFile,
   PVOID       pvBuffer,
   DWORD       nNumBytesToRead,
   OVERLAPPED* pOverlapped,
   LPOVERLAPPED_COMPLETION_ROUTINE pfnCompletionRoutine); //原型如下
VOID WINAPI CompletionRoutine(
   DWORD       dwError,
   DWORD       dwNumBytes,
   OVERLAPPED* po);
BOOL WriteFileEx(
   HANDLE      hFile,
   CONST VOID  *pvBuffer,
   DWORD       nNumBytesToWrite,
   OVERLAPPED* pOverlapped,
   LPOVERLAPPED_COMPLETION_ROUTINE pfnCompletionRoutine);
enter the alertable state:
DWORD SleepEx(
返回值 WAIT_IO_COMPLETION 线程不进入等待状态而Go on.
­
4)I/O Completion Ports(解决context switching 频繁
HANDLE CreateIoCompletionPort( //分成两个函数看:第一次创建完成端口,然后绑定Devices和完成端口
   HANDLE    hFile,
   HANDLE    hExistingCompletionPort,
   ULONG_PTR CompletionKey,
   DWORD     dwNumberOfConcurrentThreads);  //创建完成端口时,使用这个
数据结构使用:
devices queue:  I/O操作关联的设备
I/O completion queue:I/O请求完成的通知Entry放入
the waiting thread queue: 正在等待的线程id, 注意唤醒顺序是后进先出
released thread list: 唤醒的线程id
the paused thread list: 上面队列中的线程进入等待状态后移出后,进入这里(唤醒后会重新进入上面队列)
分析,假设创建完成端口时设定的最大线程数为2,然后创建有4个线程的线程池。当有3个I/O 请求完成通知到来时,完成端口唤醒2个线程来处理其中两个,如果在处理过程中有个线程调用了Sleep, WaitForSingleObject , WaitForMultipleObjects , SignalObjectAndWait 进入了等待状态,完成端口会把唤醒第三个线程来处理第三个请求,总之目标就是为了CPUS尽量饱和。最终等待状态的线程被唤醒,这时就有三个线程在运行,完成端口假设了三个线程很快会下降到2个。这也说明了为何要 创建更多的线程
­
BOOL GetQueuedCompletionStatus( //获得completion queue状态信息,堵塞式
   HANDLE       hCompletionPort,
   PDWORD       pdwNumberOfBytesTransferred, // [out]
   PULONG_PTR   pCompletionKey, // [out]
   OVERLAPPED** ppOverlapped, // [out]
   DWORD        dwMilliseconds); //如果进入等待状态,等待时间
­
BOOL PostQueuedCompletionStatus( //放一个完成端口通知Entry进入completion queue
   HANDLE      hCompletionPort,
   DWORD       dwNumBytes,
   ULONG_PTR   CompletionKey,
   OVERLAPPED* pOverlapped);
­
线程池中线程的数量:可以使用启发算法。
LONG g_nThreadsMin;    // Minimum number of threads in pool
LONG g_nThreadsMax;    // Maximum number of threads in pool
LONG g_nThreadsCrnt;   // Current number of threads in pool
LONG g_nThreadsBusy;   // Number of busy threads in pool
创建g_nThreadsMin个线程
 
 
Tip 1 : 使用WSASend/WSARecv来收发数据,而不是使用ReadFile/WriteFile
一句话,前者具有更好的性能
Tip 2:  理解IOCP的最大并发线程数和工作线程数
应该让工作线程数(调用GetQueuedCompletionStatus那些线程)大于等于在CreateIoCompletionPort 指定的NumberOfConcurrentThreads数。
标准做法是永远设置NumberOfConcurrentThreads=0
Tip 3: 利用GetQueuedCompletionStatus的completion key和overlapped structure参数在异步操作中来传递信息
通常completion key用来传递和handle/socket/session的信息
而overlapped structure用来传递每次异步I/O的一些信息,通常的做法是会定义一个structure来派生于OVERLAPPED
struct MY_IO_DATA : public OVERLAPPED
Tip 4: 理解IOCP的完成包的排队行为
从GetQueuedCompletionStatus得到完成包的次序可能跟调用WSASend/WSARecv的次序不一样。
微软唯一保证是如果调用WSASend/WSARecv得到SUCCESS或者IO_PENDING,就一定会有一个完成包出现在IOCP的队列上,不管这个socket是否关闭了。
如果关闭socket,那么之后的WSASend/WSARecv调用就一定返回失败的结果。
关于IOCP包可能次序错乱和解决方法,有一篇文章可以参考: http://www.codeproject.com/KB/IP/reusablesocketserver4.aspx
我的做法是避免多次调用WSARecv
Tip 5: IOCP的清除
最重要的一点是,在I/O完成之前,不要释放overlapped structure。可以用HasOverlappedIoCompleted来监测OV是否完成。
通常的做法是
1) 调用PostQueueCompletionStatus N次(N=工作线程数),来传递特殊的退出信息给所有的工作线程
2) 关闭所有的socket,如果很在意处理完未完成的数据包,需要使用一个计数器来跟踪异步I/O事件,直到计数器为0,才关闭相应的socket
3) 关闭completion port
 

你可能感兴趣的:(I/O)