在windows系统下面存在三种形式的异步文件操作:
1 OVERLAPPED IO
2 APC
3 IO完成端口
overlapped IO技术要求操作系统在适当的时机传输数据,并且在传输完成的时候通知你。这一技术使得程序在进行IO的时候仍然能够继续处理事务。
HANDLE WINAPI CreateFile( _In_ LPCTSTR lpFileName, _In_ DWORD dwDesiredAccess, _In_ DWORD dwShareMode, _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes, _In_ DWORD dwCreationDisposition, _In_ DWORD dwFlagsAndAttributes, _In_opt_ HANDLE hTemplateFile );
首先看一下CreateFile函数原型,为了使得文件操作能够支持overlapped IO操作,需要给CreateFile函数调用的第六个参数设置上FILE_FLAG_OVERLAP。当然也可以利用这个函数进行传统的同步文件操作,不过不能和FILE_FLAG_OVERLAP一起使用。overlapped IO能够在同一时间读或者写文件的不同部分,这也就是说,文件没有当前位置这个概念。因为每一次读写操作都必须提供开始读取的位置。当你在设置了FILE_FLAG_OVERLAP属性的文件上同时执行读写操作的时候,实际上执行结果的顺序不能保证。
BOOL WINAPI ReadFile( _In_ HANDLE hFile, _Out_ LPVOID lpBuffer, _In_ DWORD nNumberOfBytesToRead, _Out_opt_ LPDWORD lpNumberOfBytesRead, _Inout_opt_ LPOVERLAPPED lpOverlapped );文件的读操作是由上述函数完成的(写操作和读操作类似),这里只介绍最关键的参数LPOVERLAPPED。在这个结构体参数里面设置有一个成员变量设置开始读的位置,并且还有一个成员变量是一个事件句柄。我们的程序可以在这个时间句柄上面等待IO完成通知,当然,如果LPOVERLAPPED参数为NULL的时候,也可以利用文件句柄作为等待——文件句柄也是内核分发对象,不过使用文件句柄作为等待对象的时候,当读和写同时出现的时候,可能导致分不清到底是读完成还是写完成——实际操作不能保证顺序。同样的,由于整个OVERLAPPED结构体存在的时间可能比读写操作要长,所以最好是在堆里面分配。在等待结束之后,你也可以利用GetOverlappedResult函数来得到等待的结果。
由于overlapped IO存在一些限制,第一、是WaitForMultipleObjects函数等待的句柄对象存在上限;第二是,你必须根据哪一个句柄被激发而计算如何反应——利用一个分派表格和WaitForMultipleObjects的句柄数组结合起来。这两个问题都可以利用APC机制来解决。APC利用ReadFileEx函数来代替原来的ReadFile函数进行读,(写操作也类似)。ReadFileEx函数允许你指定一个额外的参数,这个参数是一个回调函数地址,当一个overlapped IO完成的时候,系统会掉用这个回调函数。当然,系统并不会贸然,打断你的线程,只有当你调用SleepEx函数或者在Waitxxx函数调用的时候传递的Alertable为TRUE的时候,系统才会打断现有的线程的运行,并且提交APC。在ReadFileEx函数当中的LPOVERLAPPED参数的事件句柄成员变量可以作为其他的用途,同时在回调函数里面激活线程调用Waitxxx函数的原因,就可以让线程继续运行了。
APC的缺点是,有些操作实际上立刻就可以返回——比如很小的文件读操作,然而,APC必须等待调用Waitxxx函数或者SleepEx进入Alertable状态的时候才能提交,这样反而使得小块的IO操作变得很慢,并且只能是调用CreateFile的线程才能够使用回调函数。IO完成端口有助于改善这些问题。IO完成端口允许你将启动overlapped请求的线程和提供服务的线程拆伙。为了使用IO完成端口,程序需要启动一堆线程,这些线程都在IO完成端口上等待——这些等待线程组成IO完成端口的线程池。每次出现overlapped IO操作的时候,可以利用相应的函数将这个文件句柄和IO完成端口句柄连接起来。每一次文件操作成功都会使得系统发送一个IO完成包到IO完成端口,这些操作发生在系统内部,对应用程序是透明的。为了回应这个IO完成包,IO端口激活一个在等待中的线程,如果目前没有线程正在等待,那么完成端口不会产生新的线程——这样可以减少不必要的线程上下文切换。同样的,激活的线程处理完相应的IO完成包之后不会退出,而是继续在IO完成端口上面等待。
HANDLE WINAPI CreateIoCompletionPort( _In_ HANDLE FileHandle, _In_opt_ HANDLE ExistingCompletionPort, _In_ ULONG_PTR CompletionKey, _In_ DWORD NumberOfConcurrentThreads );上面的函数可以用于生成一个IO完成端口,当第二个参数为NULL的时候,这个函数会生成一个默认的IO完成端口,如果第一个参数不为INVALID_HANDLE的时候,会将这个文件句柄和IO完成句柄关联起来。第三个参数会被当做一些额外的信息传递给等待的线程,而第四个参数限制等待线程的个数。在IO端口上等待的IO完成包是先进先出的,而反过来等待的线程缺失先进后出的,这样可以保证最近访问的线程没有被换页出去。
BOOL GetQueuedCompletionStatus( HANDLE CompletionPort, LPDWORD lpNumberOfBytes, PULONG_PTR lpCompletionKey, LPOVERLAPPED *lpOverlapped, DWORD dwMilliseconds );上面的函数可以用于等待IO完成端口,其中lpCompletionKey就是创建IO完成端口的第三个参数。最后一个参数是一个超时时限,当时限超过的时候,函数会返回FALSE,而lpOverlapped为NULL。
在编程的时候,还会有另外一种情况,当写操作在排队而并没有立刻完成,下一个写操作将覆盖上一个写操作的时候,就需要阻止线程被激活——也就是要阻止前一个写操作的IO完成包。利用这种技术,只需要将传递给文件操作的OVERLAPPED成员变量的事件句柄最低位设置成1就可以了。
Linux下面的EPOLL机制类似于windows下面的IO完成端口,EPOLL机制也不会存在文件句柄上限的限制,另外IO效率也不会随着文件描述符的增长而降低。EPOLL包括两种模式,第一种是LT(也就是水平激发)这个模式可以用在阻塞或者非阻塞情况下,设置了这种属性,系统回忆中通知直到所有的数据都被处理,第二种是ET(边缘激发),所有的操作只有一次就绪通知,在用户设置这个文件为未就绪之前都不会得到就绪通知。
EPOLL的实现首先调用epoll_create函数生成一个EPOLL文件标识符,其中参数是告诉内核最多可以有多少个文件标志服呗监听,不过需要注意的是,创建的EPOLL文件标识符本身就占用一个文件标识符数目。然后调用epoll_ctl函数注册事件,很类似上面的IO完成端口和文件句柄关联操作——不过在与EPOLL文件标识符关联之前需要调用系统文件操作将文件标识符设置为非阻塞。第一个参数是上面生成的EPOLL文件标识符,第二个操作表示文件关联操作主要有三个EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL。第三个参数是一个epoll_event结构,如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
最后调用epoll_wait函数来等待EPOLL操作完成,这个函数的第一个参数是相关的EPOLL文件标识符,而第二个参数是需要返回的epoll_event结构,第三个参数是可以等待的最大的时间个数,这个个数不能大于创建时候传递的参数,最后一个参数是等待超时的时间。