人, 既无虎狼之爪牙,亦无狮象之力量,却能擒狼缚虎,驯狮猎象,无他,唯智慧耳。
全部博文(167)
分类: WINDOWS
2012-04-10 20:52:53
一、概述
Windows的优势之一是它支持大量的设备类型,这里我们将设备笼统的定义为能够与之通信的任何东西,常见的一些设备及打开方式有:
设备 | 打开设备的函数 |
文件 | CreateFile() |
目录 | 同上 |
逻辑磁盘驱动器 | 同上 |
物理磁盘驱动器 | 同上 |
串口 | 同上 |
并口 | 同上 |
邮件槽服务器 | CreateMailslot() |
邮件槽客户端 | CreateFile() |
命名管道服务器 | CreateNamedPipe() |
命名管道客户端 | CreateFile() |
匿名管道 | CreatePipe() |
套接字 | Socket() |
控制台 | CreateConsoleScreenBuffer() CreateStdHandle() |
当然,这里只是简单地列出了各个设备打开的API,具体用法参数可以查阅MSDN或者SDK。当线程与设备发生通信时,即线程发出设备I/O请求时,一般来说有两种方式:
同步设备I/O请求:字如其名,线程发出设备I/O请求后,它会被临时挂起,直到设备完成I/O请求为止,显然这种方式严重损坏性能;
异步设备I/O请求:线程发出设备I/O请求后,不会被临时挂起而是继续执行其他任务,而设备同时继续处理I/O请求,设备处理完成后发送信号给线程,线程接着处理I/O结果。异步设备I/O可以避免线程挂起导致的CPU大量闲置,提高了利用率和吞吐量。
与设备I/O的通信交互是Windows编程中的重点,涉及到诸多的线程的同步方式,尤其是I/O完成端口的引入,使得不管是否与设备I/O关联,都提供了一种有无数种用途的绝佳的线程间通信机制。囿于学力,很多部分的理解都很有限,把自己掌握的整理出来,以便日后再学习吧。这篇笔记的结构大致如下:
Ø 打开和关闭设备
Ø 使用文件设备
Ø 异步设备I/O基础
Ø 接收I/O请求完成通知
二、打开和关闭设备
为了执行任何类型的设备I/O,我们必须先打开想要操作的设备并得到一个句柄,随后我们可以讲该句柄传给许多函数来与设备进行通信。这里同样用这个设备句柄来唯一标识我们的设备。
HANDLE CreateFile(
PCTSTR pszName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
PSECURITY_ATTRIBUTES psa,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hFileTemplate
);
从之前的表中也可以看出,CreateFile是打开许多设备句柄的关键函数,所以我们对它的参数做一个具体的说明。
l pszName:既可以标识设备的类型,也可以表示该类设备的某个实例。
l dwDesiredAccess:用来指定我们相用什么样的方式来和设备进行数据传输,常见的有四个标志,它们是
0——表示我们不希望对设备读出或写入任何数据,一般用来只想改变设备的配置(比如修改文件的时间戳)
GENERIC_READ——允许对设备进行只读访问
GENERIC_WRITE——允许对设备进行只写访问,比如备份软件或者将数据发送到打印机
GENERIC_READ | GENERIC_WRITE——允许对设备进行读写操作
l dwShareMode:用来指定设备共享特权(device-sharing privilege),但我们打开一个设备但是尚未调用CloseHandle()关闭时,该参数可以控制其他的CreateFile()调用以何种方式打开设备。常见参数有
0——要求独占对设备的访问
FILE_SHARE_READ——只允许共享设备读取方式
FILE_SHARE_WRITE——只允许共享设备写入方式
FILE_SHARE_READ | FILE_SHARE_WRITE——不解释
FILE_SHARE_DELETE——对文件操作时我们不关心文件是否被逻辑删除或者被移动,先将文件打上待删除标记,只有当该文件打开的所有句柄都被关闭的时候再将其真正删除
l Psa指向一个内核对象都具备的安全属性结构,里面可以指定安全信息以及我们是否希望CreateFile返回的句柄能被继承。通常我们传入NULL,这表示用默认的安全设定来创建文件,并且返回的句柄是不可继承的。
l dwCreationDisposition:用来表示用CreateFile打开文件时如果碰到存在的同名文件等情况如何处理,如
CREATE_NEW——若存在同名文件,则调用失败
CREATE_ALWAYS——若存在同名文件,则覆盖
OPEN_EXISTING——若打开的文件或者设备不存咋则调用失败
OPEN_ALWAYS——若打开文件不存在则只直接创建一个
当调用CreateFile打开文件之外的其他设备时,必须将OPEN_EXISTING传给dwCreationDisposition参数
l dwFlagsAndAttributes:允许我们设置一些标志来微调与设备之间的通信;其次我们还可以通过一些属性参数来设置文件属性。比如常见的告诉缓存标志:
FILE_FLAG_NO_BUFFERING告诉高速缓存管理器我们不希望它对任何数据进行缓存,,我们会自己对数据进行缓存;标志FILE_FLAG_DELETE_ON_CLOSE可以让文件系统在文件的所有句柄都被关闭后删除该文件(比如程序运行用到的临时文件,结束后删除,更加隐蔽),标志FLIE_FLAG_BACKUP_SEMANTICS用于备份和恢复软件,不要求文件的全部管理员权限,还有一个重要的标志FILE_FLAG_OVERLAPPED告诉系统我们想要以异步方式来访问设备,默认是同步I/O访问请求。我们重点来介绍一下文件属性参数,可以看到常见的Windows文件属性都有涉及
FILE_ATTRIBUTE_ARCHIVE——存档文件,默认自动设置
FILE_ATTRIBUTE_ENCRYPTED——加密文件
FILE_ATTRIBUTE_HIDDEN——隐藏文件
FILE_ATTRIBUTE_NORMAL——没有其他属性,只有单独使用时才有效
FILE_ATTRIBUTE_SYSTEM——系统文件
FILE_ATTRIBUTE_READONLY——只读文件
OK,可以看到右键文件属性的所有可能这里基本都有涉及,尽管没有向Linux那样提供了丰富的命令行选项,但是Windows在API参数中也为我们提供了尽可能多的功能选项。
三、使用文件设备
文件的使用非常普遍,因此重点来讨论下与文件设备有关的问题。首先我们看看如何取得文件的大小。
BOOL GetFileSizeEx(
HANDLE hFile,
PLARGE_INTEGER pliFileSize //联合类型指针
);
hFile当然就是目标文件的句柄了,pliFileSize是一个LARGE_INTEGER联合类型的地址,这个联合体允许我们以一个64位的有符号数的形式来引用一个64位有符号数,或者以两个32位值的形式来引用一个64位有符号数。下面是大概的定义:
Typedef union _LARGE_INTEGER {
Struct {
DWORD LowPart; //Low 32-bit unsigned value
LONG HighPart; //High 32-bit signed value
};
LONGLONG QuadPart; //Full 64-bit signed value
} LARGE_INTEGER, *PLARGE_INTEGER;
我们调用GetFileSizeEx可以得到文件的逻辑大小,使用GetCompressedFileSize可以得到文件压缩后的物理大小。
如果要对一个文件读写,其实就是向文件发送一个I/O请求,当然,这里的道理其实不仅适用于文件,对于设备同样适用,如邮件槽、管道、套接字等等。我们通常使用如下函数发送I/O请求,
BOOL ReadFile(
HANDLE hFile,
PVOID pvBuffer,
DWORD nNumBytesToRead,
PDWORD pdwNumBytes,
OVERLAPPED* pOverlapped
);
BOOL WriteFile(
HANDLE hFile,
CONST VOID *pvBuffer,
DWORD nNumBytesToWrite,
PDWORD pdwNumBytes,
OVERLAPPED* pOverlapped
);
执行同步I/O时pOverlapped设为NULL,只有在异步I/O时才有意义。
每当调用CreateFile时系统会创建一个文件内核对象来管理对文件的操作,在这个内核对象中维护这一个文件指针,它指向一个64位的偏移量,表示应该在哪里执行下一次同步读取或写入操作,我们称之为文件指针,初始化为0。需要注意的是,调用CreateFile打开同一个文件会得到多个文件内核对象,内部维护的文件指针彼此独立。我们可以使用SetFilePointerEx来设置文件指针的位置
BOOL SetFilePointerEx(
HANDLE hFile,
LARGE_INTEGER liDistanceToMove,
PLAGER_INTEGER pliNewFilePointer,
DWORD dwMoveMethod
);
hFile表示我们想要修改哪个文件内核对象的文件指针。liDistanceToMove告诉系统我们想要移动把指针在dwMoveMethod指定的位置移动多少个字节。使用负数可以将文件指针向后移动。dwMoveMehtod告诉系统如何解释liDistanceToMove,比如常用的参数值为
FILE_BEGIN——从文件头开始计算liDistanceToMove,之和为当前的文件指针
FILE_CURRENT——从当前的文件指针位置开始计算
FILE_END——在文件末尾开始计算
我们给出一个小例子
//本程序用来实验联系Windows关于文件读写操作的API
//命令行启动,参数作为写入文件的内容
#include
#include
#include
#include
using namespace std;
int main(int argc, char *argv[])
{
//操作一个文件之前必须先调用CreateFile()函数以指定的方式打开一个文件
//访问方式:GENERIC_READ |GENERIC_WRITE
//共享模式:当文件被打开时,其他程序可以对该文进进行的操作,表示不允许同时对任何操作
//创建方式:当文件存在或者不存在时的处理策略——CREATE_ALWAYS, CREATE_NEW, OPEN_ALWAYS, OPEN_EXISTING
//新创建的文件属性:ARCHIVE(存档),HIDDEN, SYSTEM, READONLY
//模板文件:没有则设为NULL
HANDLE hFile;
hFile = CreateFile(L"C:\\Documents and Settings\\admin\\桌面\\FileAPITest.txt", GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_HIDDEN, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
cout<<"CreateFile error !"<
return -1;
}
//调用SetFilePointer()函数凋整文件指针位置,移动到文件指定位置:FILE_BEGIN, FILE_CURRENT, FILE_END
//第二/三个参数指定在指定文件指针处移动到位置大小
if (SetFilePointer(hFile, 0, NULL, FILE_END) == -1)
{
cout<<"SetFilePointer error !"<
return -2;
}
//利用WriteFile()函数将命令行的参数内容写入到指定的文件中
DWORD dwByteWritten; //实际写入的字节数
if (WriteFile(hFile, argv[1], strlen(argv[1]), &dwByteWritten, NULL) == FALSE)
{
cout<<"WriteFile error !"<
return -3;
}
cout<<"写入数据成功!"<
//使用完毕后释放文件句柄
CloseHandle(hFile);
return 0;
}
/*PS:
其他的相关函数:
写入函数——ReadFile()
复制函数——CopeFile()
移动函数——MoveFile()
删除函数——DeleteFile(LPCTSTR lpFileName);
本函数用来生成了一个隐藏文件
*/
四、异步设备I/O基础
对于异步设备而言,线程访问I/O设备大致步骤如下:
1. 线程调用CreateFile(...,FILE_FLAG_OVERLAPPED,...)以异步方式打开一个文件设备,调用ReadFile/WriteFile发送异步I/O请求;
2. 设备驱动将I/O请求加入等待队列,线程继续执行;
3. 设备执行完I/O请求返回给设备驱动程序,设备驱动发送信号给线程
4. 线程继续执行I/O处理
线程将I/O请求加入设备等待队列的方法我们已经明白了,但是设备驱动如何告知线程I/O请求处理完毕呢?比着急,我们先来学习一下异步设备I/O的基本知识。
我们已经知道,如果想异步访问一个I/O设备,必须首先用CreateFile()传入FILE_FLAG_OVERLAPPED参数打开一个设备句柄,然后调用ReadFile/WriteFile将I/O请求加入设备驱动程序的队列中。这里涉及到一个OVERLAPPED结构体,到底是怎么一回事呢?
Typedef struct _OVERLAPPED {
DWORD Internal; // [out] Error Code
DWORD InternalHigh; // [out] Number of bytes transferred
DWORD Offset; // [in] Low 32-bit file offset
DWORD OffsetHigh; // [in] High 32-bit file offset
HANDLE hEvent; // [in] Event handle or data
} OVERLAPPED, *LPOVERLAPPED;
这里标注[out]的是在设备处理完I/O请求后添加的,不需要我们负责;我们需要初始化标注为[in]的三个参数。两个32位偏移量共同组成文件的64位偏移量,用来指定文件的文件指针位置,对于非文件设备Offset和OffsetHigh必须初始化为0,因为非文件设备不需要文件指针。
使用异步设备I/O时我们有以下需要注意的问题:
1. 设备驱动程序不必以先入先出的方式来处理队列中的I/O请求
2. 如果以同步方式执行ReadFile/WriteFile,成功则返回非零值,失败返回FALSE;如果以异步方式执行,执行成功也会返回FALSE,利用GerLastError可以查看错误代码ERROR_IO_PENDING则表示异步I/O请求已被成功加入了队列,会在晚些时候执行。
3. 在异步I/O请求完成前,一定不能移动或是销毁在发出I/O请求时所使用的数据缓存和OVERLAPPED结构。
4. 我们必须为每一个I/O请求分配并初始化一个不同的OVERLAPPED结构
五、接收I/O请求完成通知
这部分内容主要回到设备驱动如何告知线程I/O处理完的问题。Windows提供了四种方法,由易到难是
触发设备内核对象;
触发事件内核对象;
可提醒I/O;
I/O完成端口。
应该说,I/O完成端口属于目前最完美的技术,但是对于当前自己的程序而言不需要,所以只需要使用简单的设备内核对象或者事件内核对象就可以实现设备I/O请求的送达。
l 触发设备内核对象
由于文件本身即是一种内核对象,触发规则为:有待处理的I/O请求时为未触发状态,I/O请求处理完成时为触发状态。因而我们只需要使用WaitForSingleObject(hFile, INFINITE)一个语句即可,当然这个语句要放到一定代码之后,估计I/O请求处理完毕之后再进入检查文件是否触发。
我们给出一个示例代码
HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);
BYTE bBuffer[100];
OVERLAPPED ol = {0};
ol.Offset = 345;
BOOL bReadDone = ReadFile(hFile, bBuffer, 100, NULL, &ol);
DWORD dwError = GetLastError();
If (!bReadDone && (dwError == ERROR_IO_PENDING) ) {
WaitForSingleObject(hFile, INFINITE);
bReadDone = TRUE;
}
l 触发事件内核对象
只需要将文件内核对象改为事件内核对象就可以了,这里需要注意的是此时OVERLAPPED结构的最后一个成员hEvent需要和我们的事件内核对象绑定起来,我们给出一个方法来同时执行多个异步设备I/O操作并使用同一个设备对象。
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 = 0;
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: //Read completed
breadk;
Case 1: //Write completed
Break;
}