《Windows核心编程》第十章虽然题目中提到了同步设备,但是似乎整章作者都在宣扬异步设备I/O的好处!
同步设备I/O在用户发出I/O设备请求时是会阻塞线程,直到I/O设备完成任务的!虽然,这样做可能损害了程序的性能,但是由于操作简单还是有很多我这样的初学者这么做的(毕竟我们又不在乎什么性能!)
异步I/O设备ReadFile或者WriteFile函数立刻返回,线程可以继续运行。那么现在我们需要一种方式来通知运行中的线程:“我们的I/O请求已经完成了!可以对出已经对读取或者写入的数据进行进一步操作了!”这里进行通知的方法就是这一张要讨论的内容!
Windows中有众多的设备(文件相当于是硬盘设备),这些设别的内核对象统统用函数CreateFile创建,这个函数书中有详细介绍,这里必须提一下两个标值:FILE_FLAG_NO_BUFFERING和FILE_FLAG_OVERLAPPED
FILE_FLAG_NO_BUFFERING:表示在访问文件的时候使用高速缓存,直接在硬盘进行读写。按照书中的说法,使用高速缓存可能会浪费系统的内存有时候可能甚至会降低程序的效率。
使用了这个标志后我们对文件的存取就有了一定限制:访问文件是使用的偏移量、读写入的字节数和使用的缓存空间的起始地址,这三个东西必须是磁盘卷扇区大小的整数倍!!
Windows中的下面几个函数能够实现我们上面的要求!
下面这个函数能帮我们确定扇区的大小
BOOL GetDiskFreeSpace(
LPCTSTR lpRootPathName, // pointer to root path
LPDWORD lpSectorsPerCluster, // pointer to sectors per cluster
LPDWORD lpBytesPerSector, // pointer to bytes per sector
LPDWORD lpNumberOfFreeClusters,
// pointer to number of free clusters
LPDWORD lpTotalNumberOfClusters
// pointer to total number of clusters
);
VirtualAlloc能够在使分配的内存空间满足上面的要求,当然m_nBufferSize应该是磁盘卷扇区大小的整数倍
PVOID m_pvData=VirtualAlloc(NULL, m_nBuffSize, MEM_COMMIT, PAGE_READWRITE)
同时,我们还可能要修改文件的大小使他正好是磁盘扇区大小的整数倍!这就涉及到了文本文件的指针问题!
先介绍一个表示LONGLONG 的结构LARGE_INTEGER,该结构表示64位整型,下面大概是他的定义:
typedef union _LARGE_INTEGER {
struct {
DWORD LowPart; //低32位
LONG HighPart; //高32位
};
LONGLONG QuadPart;//long long类型
} LARGE_INTEGER;
这个结构,Windows中用来表示文件指针的位置,0表示文件开头,可见我们能处理的文件大小是2的64次方个字节,就是16EB大概是(2^24个TB)!
下面的函数返回文件大小,当然我们会主要用到第一个
//返回文件大小,这里是逻辑大小,就是一个文件解压缩后的大小
BOOL GetFileSizeEx(HANDLE hFile,PLARGE_INTEGER pliFileSize);
//这个返回物理大小,其中低32位是函数返回值,高32位在lpFileSizeHigh指向的位置
DWORD GetCompressedFileSize(LPCTSTR lpFileName,LPDWORD lpFileSizeHigh);
每一个文件内核都有一个独有的文件指针,如果我们顺序读取文件,那么这个指针是自动增加的直到文件尾,如果我们要改变文件的大小就要调用函数设定指针的文件尾
SetFilePointerEx函数能够移动指针的位置(向前,向后,直接到文件末尾,到文件开头!!),SetEndOfFile能够把当前指针的位置设定为文件尾,实现对文件的截断或者扩大!
BOOL SetFilePointerEx(
HANDLE hFile, // handle of file
LARGE_INTEGER lDistanceToMove, // number of bytes to move file pointer
PLARGE_INTEGER lpDistanceToMoveHigh,//移动后指针的位置
DWORD dwMoveMethod // how to move
);
BOOL SetEndOfFile(
HANDLE hFile // handle of file whose EOF is to be set
);
FILE_FLAG_OVERLAPPED表示我们在对文件进行读取或者写入的时候采用异步操作,这个东西是我们异步I/O操作的开始!!
我们使用下面两个函数进行设备读写
BOOL ReadFile(
HANDLE hFile, // handle of file to read
LPVOID lpBuffer, // pointer to buffer that receives data
DWORD nNumberOfBytesToRead, // number of bytes to read
LPDWORD lpNumberOfBytesRead, // pointer to number of bytes read
LPOVERLAPPED lpOverlapped // pointer to structure for data
);
BOOL WriteFile( HANDLE hFile, // handle to file to write to
LPCVOID lpBuffer, // pointer to data to write to file
DWORD nNumberOfBytesToWrite, // number of bytes to write
LPDWORD lpNumberOfBytesWritten, // pointer to number of bytes written
LPOVERLAPPED lpOverlapped // pointer to structure for overlapped I/O);
);
但我们是使用异步I/O时,我们必须传递一个结构OVERLAPPED的指针,这个结构和我们异步调用的过程息息相关!!
typedef struct _OVERLAPPED { // o
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
HANDLE hEvent;
} OVERLAPPED;
这个结构后面三个变量需要我们手动的初始化!
其中,Offset和OffsetHigh表示文件设备的指针(也可以理解为这次异步操作开始的地方,都初始化为0表示我们从文件的开头开始操作)。在异步I/O中我们是忽略内核对象中的文件指针的。
hEvent用来通知异步操作何时完成,异步操作完成时这个对象会被触发!
第一个和第二个对象有设备的驱动程序来设定,Internal表示错误码,InternalHigh表示传输了多少个字节!!
通过上面的介绍我们实际上已经可以使用异步的I/O操作了!
我们可以同过FILE_FLAG_OVERLAPPED创建一个设备对象,初始化一个OVERLAPPED结构,为其中的hEvent分配一个事件对象,然后用readfile和writefile进行I/O请求!然后我们的请求线程可以先干别的,当请求完成以后,我们的一个线程可以通过hEvent获得这个消息,进行下一步处理!!
不过显然怎样这样的异步调用方式略显简单,书中一共提出了四中方式对异步设备消息进行通知,其中后两种分别称为可提醒I/O和异步I/O完成端口,特别是最后一种方式功能相当强大!!下面,是我们记录的一些概念!
异步过程调用队列(APC队列):每当我们创建一个线程时,我们对都创建这样一个队列!每当我们的一个异步请求完成时,我们可以向这个队列中添加一项并填写对应的回调函数(完成函数)告诉程序有空时再来处理这个队列中的请求。使用ReadFileEx和WriteFileEx进行读写请求时,一旦请求完成APC队列就会得到通知!
程序在可提醒状态时处理APC队列中的内容!所谓的可提醒状态就是程序没事干的时候,一共有6个函数能够产生可提醒状态:
SleepEx,WaitForSingleObjectEx,WaitForMultipleObjectEx。。。。。
当然APC的用处肯定很多,不是只能用在I/O上。
Windows提供了一个函数使我们能够手工添加APC队列
DWORD QueueUserAPC(
PAPCFUNC pfnAPC, // pointer to APC function
HANDLE hThread, // handle to the thread
DWORD dwData // argument for the APC function
);
其中第一个参数是APC的回调函数,这个回调函数是在一个本线程中执行的,不会创建新的线程!
VOID WINAPI APCFunc(ULONG_PTR dwParam)
这里需与要注意的是pfnAPC要和hThread在同一个进程的地址空间中
我们可以用APC来进行线程间的通信,甚至可以进行跨进程的线程间通信(比如使用ToolHelp得到线程句柄--)。这里我们的邮差是函数QueueUserAPC,接收方有一个邮箱就是这个线程的APC队列,一旦他收到消息就可以在回调函数中处理。但是,美中不足的是我们通信的内容只能是一个ULONG_PTR。
我们还可以使用QueueUserAPC来杀死进程中的其他线程,让整个进程完美退出!当然这里有一个前提是要杀死的线程正处在可提醒状态!
I/O完成端口
整章的内容其实是为了讲这个,用来管理线程池!但是,我好想用不上!!