本文主要说明在Windows下操作文件的高级方法,比如直接读写磁盘,文件的异步操作,而文件普通的读写方式在网上可以找到一大堆资料,在这也就不再进行专门的说明。
判断文件是否存在
在Windows中并没有专门提供判断文件是否存在的API,替代的解决方案是使用函数GetFileAttributes,传入一个路径,如果文件不存在,函数会返回INVALID_FILE_ATTRIBUTES,这个时候一般就可以认为文件不存在。更严格一点的,可以在返回INVALID_FILE_ATTRIBUTES之后调用GetLastError函数,判断返回值是否为ERROR_FILE_NOT_FOUND或者ERROR_PATH_NOT_FOUND(这个值适用于判断目录)
下面是它的实例代码
BOOL IsFileExist(LPCTSTR pFilePath)
{
DWORD dwRet = GetFileAttributes(pFilePath);
if(INVALID_FILE_ATTRIBUTES == dwRet)
{
dwRet = GetLastError();
if (ERROR_FILE_NOT_FOUND == dwRet || ERROR_PATH_NOT_FOUND == dwRet)
{
return FALSE;
}
}
return TRUE;
}
文件查找和目录遍历
这个操作主要使用到了下面几个API函数:
1. FindFirstFile:建立一个指定搜索条件的搜索句柄,函数原型如下:
HANDLE FindFirstFile(
LPCTSTR lpFileName,
LPWIN32_FIND_DATA lpFindFileData
);
第一个参数是一个搜索起始位置路劲的字符串,但是这个字符串的格式为“路径+特定文件的通配符”这样它会以这个路径作为起始路径,依次查找到目录中文件名符合通配符的文件,比如”c:\*.“会返回c盘下的所有文件,而”c:\”直接返回错误,”c:\a.txt”会返回c盘中以a开头的txt文件
2. FindNextFile:搜索符合条件的下一项,在循环中调用它的话,它会依次返回符合FindFirstFile要求的文件信息和所有子目录新消息
3. FindClose:关闭搜索句柄
FindFirstFile和FindNextFile返回的文件信息结构为WIN32_FIND_DATA,它的定义如下:
typedef struct _WIN32_FIND_DATA {
DWORD dwFileAttributes; //文件属性
FILETIME ftCreationTime; //创建时间
FILETIME ftLastAccessTime; //最后访问时间
FILETIME ftLastWriteTime; //最后修改时间
DWORD nFileSizeHigh;
DWORD nFileSizeLow; //这两个值是一个64位的文件大小的高32位和低32位
DWORD dwOID;
TCHAR cFileName[MAX_PATH]; //文件名称
} WIN32_FIND_DATA;
一般在遍历的时候首先判断文件属性,如果为FILE_ATTRIBUTE_DIRECTORY(是个目录),并且文件名称不为”.”,”..”则递归调用遍历函数遍历它的子目录,但是一定要记得进行文件路径的拼接,如果不为目录,这个时候一般就是普通文件,这个时候可以选择进行打印(遍历文件目录)或者比较文件名称与需要查找的名称是否相同(查找文件)。下面是一个全盘搜索特定文件名的实例代码:
void FindFileByPath(LPCTSTR pszSearchEntry, LPCTSTR pszFileName)
{
WIN32_FIND_DATA fd = {0};
TCHAR szFilePath[MAX_PATH] = _T("");
StringCchCat(szFilePath, MAX_PATH, pszSearchEntry);
StringCchCat(szFilePath, MAX_PATH, _T("*.*"));
HANDLE hSearch = FindFirstFile(szFilePath, &fd);
if (INVALID_HANDLE_VALUE == hSearch)
{
return;
}
do
{
if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && _tcscmp(fd.cFileName, _T(".")) != 0 && _tcscmp(fd.cFileName, _T("..")) != 0)
{
TCHAR szSubDir[MAX_PATH] = _T("");
StringCchCat(szSubDir, MAX_PATH, pszSearchEntry);
StringCchCat(szSubDir, MAX_PATH, fd.cFileName);
StringCchCat(szSubDir, MAX_PATH, _T("\\"));
FindFileByPath(szSubDir, pszFileName);
}else
{
if (_tcscmp(fd.cFileName, pszFileName) == 0)
{
TCHAR szFullPath[MAX_PATH] = _T("");
StringCchCat(szFullPath, MAX_PATH, pszSearchEntry);
StringCchCat(szFullPath, MAX_PATH, _T("\\"));
StringCchCat(szFullPath, MAX_PATH, fd.cFileName);
printf("full path:%ws\n", szFullPath);
return;
}
}
ZeroMemory(&fd, sizeof(fd));
} while (FindNextFile(hSearch, &fd));
}
void FindFile(LPCTSTR pFileName)
{
TCHAR szVolumn[MAX_PATH] = _T("");
GetLogicalDriveStrings(MAX_PATH, szVolumn);
LPCTSTR pVolumnName = szVolumn;
while (_tcscmp(pVolumnName, _T("")) != 0)
{
FindFileByPath(pVolumnName, pFileName);
//偏移到下一个盘符的字符串位置
size_t nLen = 0;
StringCchLength(pVolumnName, MAX_PATH, &nLen);
nLen++;
pVolumnName += nLen;
}
}
由于这段代码会遍历整个磁盘,查找所有具有相同文件名称的文件,所以当某个逻辑分区的文件结构比较复杂的时候,可能执行效果比较慢。
这段代码出现了两个函数,第一个函数是真正遍历文件的函数,由于FindFirst函数需要传入一个入口点,所以在需要进行全盘遍历的时候提供了另外一个函数来获取所有磁盘的逻辑分区名。
获取所有逻辑分区名调用函数GetLogicalDriveStrings,这个函数会返回一个含有所有分区名称的字符串,每个分区名称之间以”\0”分割,所以在获取所有名称的时候需要自己进行字符串指针的偏移操作
在遍历的时候为了要遍历所有文件及目录搜索的统配符应该匹配所有文件名称。另外FindFirst也会返回一个文件信息的结构,这个结构是当前目录中符合条件的第一个文件信息,在遍历的时候不要忘记也取一下它返回的文件信息。最后当文件为目录的时候需要判断它是否为当前目录或者当前目录的父目录,也就是是否为”.”和”..”,这段代码有一点不足就是不支持通配符,必须输入文件名的全称
目录变更监视
一般像notepad++等文本编辑器都会提供一个功能,就是在它们打开了一个文本之后,如果文本被其他程序更改,那么它们会提示用户是否需要重新载入,这个功能的实现需要对文件进行监控,windows中提供了一套API用于监控目录变更
使用函数FindFirstChangeNotification创建一个监控句柄,该函数原型如下:
HANDLE FindFirstChangeNotification(
LPCTSTR lpPathName,
BOOL bWatchSubtree,
DWORD dwNotifyFilter);
第一个参数是一个目录的字符串,表示将要监控哪个目录,注意这里必须穿入一个目录,不能穿文件路径
第二个参数是一个bool类型,表示是否监控目录中的整个目录树
第三个参数是监控的时间类型,如果要监控目录中的文件的改动,可以使用FILE_NOTIFY_CHANGE_LAST_WRITE 标记,该标记会监控文件的最后一次写入,其他类型请查阅MSDN
创建监控句柄后使用Wait函数循环等待监控句柄,如果目录中发生对应的事件,wait函数返回,这个时候可以对比上次目录结构得出哪个文件被修改,做相应的处理后调用FindNextChangeNotification函数传入监控句柄,继续监控下一次变更。
最后当我们不需要进行监控的时候调用FindCloseChangeNotification关闭监控句柄
void WatchDirectoryChange(LPCTSTR lpDir)
{
HANDLE hChangNotify = FindFirstChangeNotification(lpDir, FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE );
if (hChangNotify == INVALID_HANDLE_VALUE)
{
printf("FindFirstChangeNotification function faild!\n");
return ExitProcess(GetLastError());
}
while (TRUE)
{
printf("wait for change notify.......\n");
if(WAIT_OBJECT_0 == WaitForSingleObject(hChangNotify, INFINITE))
{
printf("some file be changed in this directory\n");
}
FindNextChangeNotification(hChangNotify);
}
FindCloseChangeNotification(hChangNotify);
}
如果嫌这个方法比较麻烦的话,为了实现这个功能,Windows专门提供了一个函数ReadDirectoryChangesW,就跟他的名字一样他只能用于UNICODE平台,这个函数不存在ANSI版本,所以在ANSI版本时需要进行字符串的转化操作。
函数原型如下:
BOOL WINAPI ReadDirectoryChangesW(
__in HANDLE hDirectory, //需要监控的目录的句柄,这个句柄可以用CreateFile打开
__out LPVOID lpBuffer, //函数返回信息的缓冲
__in DWORD nBufferLength, //缓冲区的长度
__in BOOL bWatchSubtree, //是否监控它的子目录
__in DWORD dwNotifyFilter, //监控的事件
__out_opt LPDWORD lpBytesReturned, //实际返回数据长度
__inout_opt LPOVERLAPPED lpOverlapped, //异步调用时的OVERLAPPED结构
__in_opt LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine //异步调用时的APC函数
);
这个函数它的原理就类似于上面的三个函数,如果是同步操作,当需要监控的目录发生指定的事件时函数返回,并将监控得到的信息填充到结构体中,它会将数据以FILE_NOTIFY_INFORMATION结构的形式返回。该结构的定义如下:
typedef struct _FILE_NOTIFY_INFORMATION {
DWORD NextEntryOffset;
DWORD Action;
DWORD FileNameLength;
WCHAR FileName[1];
} FILE_NOTIFY_INFORMATION, *PFILE_NOTIFY_INFORMATION;
这个结构体中存储文件名称的成员为FileName,这个成员只是起到一个变量名称标识的作用,在存储文件名称时用到了越界访问的方式,所以定义缓冲的大小一定要大于这个结构,让其有足够的空间容纳FileName这个字符串。结构体中的Action表示当前发生了何种操作,具体的类型可以参考MSDN,它的意思根据字面的单词很容易理解
下面是使用它的具体代码:
void WatchFileChange(LPCTSTR lpFilePath)
{
DWORD cbBytes;
char notify[1024];
HANDLE dirHandle = CreateFile(lpFilePath,GENERIC_READ | GENERIC_WRITE | FILE_LIST_DIRECTORY,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
NULL);
if(dirHandle == INVALID_HANDLE_VALUE) //若网络重定向或目标文件系统不支持该操作,函数失败,同时调用GetLastError()返回ERROR_INVALID_FUNCTION
{
cout<<"error"+GetLastError()<<endl;
}
memset(notify,0,strlen(notify));
FILE_NOTIFY_INFORMATION *pnotify = (FILE_NOTIFY_INFORMATION*)notify;
cout<<"start...."<<endl;
while(true)
{
if(ReadDirectoryChangesW(dirHandle,¬ify,1024,true,
FILE_NOTIFY_CHANGE_FILE_NAME |
FILE_NOTIFY_CHANGE_DIR_NAME
| FILE_NOTIFY_CHANGE_SIZE,
&cbBytes,NULL,NULL))
{
//设置类型过滤器,监听文件创建、更改、删除、重命名等
switch(pnotify->Action)
{
case FILE_ACTION_ADDED:
_tprintf(_T("add file: %s\n"), pnotify->FileName);
break;
case FILE_ACTION_MODIFIED:
_tprintf(_T("modify file:%s\n"), pnotify->FileName);
break;
case FILE_ACTION_REMOVED:
_tprintf(_T("file removed %s\n"), pnotify->FileName);
break;
case FILE_ACTION_RENAMED_OLD_NAME:
_tprintf(_T("file renamed:%s\n"), pnotify->FileName);
break;
default:
cout<<"unknow command!"<<endl;
}
}
}
CloseHandle(dirHandle);
}
这段代码很容易理解,但是需要注意几点:
1. 之前说过的分配的缓冲一定要大于FILE_NOTIFY_INFORMATION 结构
2. 这个函数也是用来监控目录的,所以这里要传入一个目录路径,不能传入文件路径
3. 在使用CreateFile来打开目录的时候这个函数要求传入的文件句柄必须要以FILE_LIST_DIRECTORY标识打开,否则在调用的时候会报“参数错误”这个错
文件映射
Windows中,文件映射是文件内容到进程的虚拟地址空间的映射,这个映射称之为File Mapping,文件内容的拷贝就是文件视图(File View),从内存管理的角度来看,文件映射只是将磁盘的真实地址通过页表映射到进程的虚拟地址空间中,读写这段虚拟地址空间其实就是在读写磁盘。而文件视图就是将文件中的内容整个读到内存中,并将这段虚拟地址空间与真实物理内存对应。最终在关闭整个文件映射的时候如果存在文件视图,操作系统会将视图中的内容写会到磁盘,其实也就是简单的进行了下物理内存到磁盘的页面交换,从内存管理的角度来看,文件映射其实就是操作系统将磁盘上的数据与物理内存之间的页面交换,操作系统在二者之间来回倒腾数据而已
文件映射本身是一个内核对象,操作系统在内核中维护了一个相关的数据结构,这个结构中记录了被映射到虚拟地址空间中的起始地址和被映射的数据的大小。
由于内核对象的数据结构是在内核中被维护,而内核被所有进程共享,所以从理论上将不同的进程是可以共享同一个内核对象的,虽然它们的对象句柄会在不同进程中呈现不同的值,但是在内核中,却是指向同一个结构,那么虽然不同进程的文件映射对象不同,但是通过寻址得到的物理内存肯定是同一个,所以这就提供了另一种进程间共享内存的方法——文件映射。
创建文件映射主要使用函数CreateFileMapping,这个函数第一个参数是一个文件句柄,这个句柄可以是一个真实存在在磁盘上的文件,这样创建的文件映射最终就是将磁盘中的数据映射到进程的虚拟地址空间,也可以传入一个INVALID_HANDLE_VALUE,这个时候也会返回成功,传入INVALID_HANDLE_VALUE一般是用来在进程间共享内存的。注意:这个函数只是创建了一个内核对象并返回它的句柄,并没有进行内存映射的相关操作。同时由于它第一个句柄参数可以填INVALID_HANDLE_VALUE,在使用CreateFile函数后一定要注意校验,不然可能看到CreateFileMapping函数返回的是一个有效句柄,但是并没有成功创建这个文件的映射
然后调用MappingViewOfFile函数,将对应文件与一段进程的虚拟地址空间关联并将文件映射到内存,也就是将磁盘文件中的数据交换到物理内存中
当我们不使用这块真实内存的时候,调用UnMapViewOfFile将内存中的数据交换到磁盘,最终使用文件映射完毕后,调用CloseHandle关闭所有句柄
使用文件映射一般有几个好处:
1. 针对文件来说,文件映射本质上是磁盘到物理内存之间的页面交换,由操作系统的内存管理机制统一调度,效率比一般的文件读写要高,而且在使用完毕后,操作系统会自动的将内存中的数据写到磁盘中,不用手动的更新文件
2. 针对不同进程来说,使用文件映射来共享内存本质上是在使用同样一块内存,相比于管道油槽等方式传输数据来说显得更为高效
下面通过几个例子来说明在这两种情况下使用文件映射
void GetFileNameByHandle(HANDLE hFile)
{
HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (INVALID_HANDLE_VALUE == hMapping)
{
_tprintf(_T("create file mapping error\n"));
return;
}
LPVOID lpMappingMemeory = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 1);
if (NULL == lpMappingMemeory)
{
_tprintf(_T("MapViewOfFile error\n"));
return;
}
TCHAR szFileName[MAX_PATH] = _T("");
if(0 == GetMappedFileName(GetCurrentProcess(), lpMappingMemeory, szFileName, MAX_PATH))
{
_tprintf(_T("GetMappedFileName error\n"));
return;
}
TCHAR szTemp[MAX_PATH] = _T("");
GetLogicalDriveStrings(MAX_PATH, szTemp);
TCHAR szDriver[4] = _T(" :");
LPCTSTR p = szTemp;
while (*p != _T('\0'))
{
*szDriver = *p;
TCHAR szName[MAX_PATH] = _T("");
QueryDosDevice(szDriver, szName, MAX_PATH);
size_t nPathLen = 0;
StringCchLength(szName, MAX_PATH, &nPathLen);
if(CSTR_EQUAL == CompareString(LOCALE_USER_DEFAULT, NORM_IGNORECASE, szName, nPathLen, szFileName, nPathLen))
{
TCHAR szFullPath[MAX_PATH] = _T("");
StringCchCopy(szFullPath, MAX_PATH, p);
//在这使用文件带卷名的字符串首地址 + 卷名长度 + 1(+1是为了偏移到卷名后面的"\"的下一个字符,因为这个盘符中自己带了"/"字符)
StringCchCat(szFullPath, MAX_PATH, szFileName + nPathLen + 1);
_tprintf(_T("文件全路径:%s"), szFullPath);
break;
}
size_t dwLen = 0;
StringCchLength(p, MAX_PATH, &dwLen);
p = p + dwLen + 1;
}
UnmapViewOfFile(lpMappingMemeory);
CloseHandle(hMapping);
return;
}
该函数利用文件映射的方式,通过一个文件的句柄获取它的绝对路径。
该函数首先根据文件句柄创建一个文件映射并调用GetMappedFileName获取文件的全路径,但是获取到的是类似于“\Device\HarddiskVolume6\Program\FileDemo\FileMapping\FileMapping.cpp”这样的卷名加上文件的相对路径,而不是我们常见的类似于C D E这样的盘符名称,所以为了获取对应的盘符,使用的方式是利用GetLogicalDriverString函数来获取系统所有逻辑卷的盘符,然后调用QueryDosDevice函数将盘符转化为卷名,再与之前获取到的路径中的卷名进行比较,在这使用了一个技巧,就是首先获取卷名对应的长度,然后调用比较函数时传入卷名的长度让其只比较卷名对应的字符,如果相同,就找到了卷名对应的盘符名称,最后将卷名与在卷中的相对路径进行拼接就得到了它的文件全路径。
下面来看一个使用文件映射在不同进程间共享内存的例子
//Process A
#define BUFF_SIZE 1024
int _tmain(int argc, _TCHAR* argv[])
{
TCHAR szHandleName[] = _T("Global\\ShareMemMapping");
HANDLE hMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, BUFF_SIZE, szHandleName);
if (INVALID_HANDLE_VALUE == hMapping)
{
printf("create file mapping error\n");
return GetLastError();
}
LPVOID pMem = MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, BUFF_SIZE);
if (NULL == pMem)
{
printf("MapViewOfFile Error\n");
return GetLastError();
}
ZeroMemory(pMem, BUFF_SIZE);
TCHAR pszData[] = _T("this is written by process A");
CopyMemory(pMem, pszData, sizeof(pszData));
_tsystem(_T("PAUSE"));
UnmapViewOfFile(pMem);
CloseHandle(hMapping);
return 0;
}
#define BUFF_SIZE 1024
int _tmain(int argc, _TCHAR* argv[])
{
TCHAR szHandleName[] = _T("Global\\ShareMemMapping");
HANDLE hMapping = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, szHandleName);
if (INVALID_HANDLE_VALUE == hMapping)
{
printf("OpenFileMapping");
return GetLastError();
}
LPCTSTR pMem = (LPCTSTR)MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, BUFF_SIZE);
if (NULL == pMem)
{
printf("MapViewOfFile Error\n");
return GetLastError();
}
printf("read date: %ws\n", pMem);
_tsystem(_T("PAUSE"));
UnmapViewOfFile(pMem);
CloseHandle(hMapping);
return 0;
}
在上面的例子中,进程A做了如下工作:
1. 创建一个命名的文件映射对象
2. 构建文件映射的视口,并写入一段内存
3. 等待
4. 关闭相关句柄
在进程B中做了如下工作:
1. 打开之前A创建的文件映射对象
2. 构建文件映射的视口,读取内存
3. 关闭相关句柄
在使用文件映射共享内存时需要注意:
1. 使用命名对象的时候,对象前面必须要加上“Global//”表示该对象是一个全局的对象
2. 不同进程在使用文件映射共享内存时调用函数MapViewOfFile填写内存的起始偏移,视口大小必须完全一样
这个例子中只是简单的一个进程写,另一个进程读,如果想要两个进程同时读写共享内存,可以使用Event等方式进行同步。
直接读写磁盘扇区
CreateFile可以打开许多设备,一般来说,它可以打开所有的字符设备,向串口,管道,油槽等等,在编写某些硬件的驱动程序时如果将其以字符设备的方式来操作,那么理论上在应用层是可以用CreateFile打开这个硬件设备的句柄,并操作它的,这里介绍下如何使用CreateFile来直接读取物理磁盘。
读写物理磁盘只需要改变一下CreateFile中代表文件名称的第一个参数,将这个参数改为\.\PhysicalDrive0,后面的数字代表的是第几块物理硬盘,如果有多块硬盘,后面还可以是1、2等等
注意这是在直接读写物理磁盘,当你不了解文件系统的时候,不要随意往里面写数据,以免造成磁盘损坏
下面是一个简单的例子
DWORD dwSectorsPerCluster = 0;
DWORD dwBytesPerSector = 0;
DWORD dwNumberOfFreeClusters = 0;
DWORD dwTotalNumberOfClusters = 0;
TCHAR pDiskName[] = _T("\\\\.\\PhysicalDrive0");
//get disk info
if(GetDiskFreeSpace(_T("c:\\"), &dwSectorsPerCluster, &dwBytesPerSector, &dwNumberOfFreeClusters, &dwTotalNumberOfClusters))
{
printf("磁盘信息:\n");
LARGE_INTEGER size_disk = {0};
size_disk.QuadPart = (LONGLONG)dwTotalNumberOfClusters * (LONGLONG)dwSectorsPerCluster * (LONGLONG)dwBytesPerSector;
printf("\t总大小 %dG", size_disk.QuadPart / (1024 * 1024 * 1024));
printf("\t簇总数%d, 簇中扇区总数:%d, 扇区大小:%d\n", dwTotalNumberOfClusters, dwSectorsPerCluster, dwBytesPerSector);
}
else
{
dwBytesPerSector = 512;
}
HANDLE hDisk = CreateFile(pDiskName,GENERIC_READ,FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,NULL,OPEN_EXISTING,0,NULL);
if(hDisk == INVALID_HANDLE_VALUE)
{
printf("create file error\n");
return GetLastError();
}
char* pMem = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBytesPerSector * 8);
DWORD dwRead = 0;
if(!ReadFile(hDisk, pMem, dwBytesPerSector * 8, &dwRead, NULL))
{
printf("read file error\n");
return GetLastError();
}
for(int i = 0; i < dwBytesPerSector * 8; i++)
{
if(i % 16 == 0 && i != 0)
{
printf("\n");
}
printf("0x%02x ", pMem[i]);
}
CloseHandle(hDisk);
上面的例子调用了GetDiskFreeSpace函数获取了逻辑卷的相关信息,它需要传入一个盘符,表示要获取哪个盘的数据,它会通过输出参数返回多个逻辑卷的信息,它们分别是:每个簇有多少个扇区,每个扇区的大小,有多少个空闲的簇,卷中簇的个数。根据这些信息就可以计算出逻辑卷的大小哦,在计算的时候由于磁盘空间一定是大于4G的,所以在这要用64位整数保存。
知道了扇区大小后,直接调用文件操作函数,读取8个扇区的数据,然后输出。
文件的异步操作
在常规文件读写方式中,是严格串行化的,只有当读写操作完全完成时才会返回,由于磁盘读写相对于CPU的运行效率来说实在是太慢的,这就造成了程序长时间处理等待状态,这种读写方式称之为阻塞方式,早期的磁盘在进行读写时是需要CPU来控制,这样CPU必须来配合慢速的硬盘,造成了效率低下,于是硬件工程师在在磁盘中加入了一个控制设备,专门用来控制磁盘的读写,这个设备被称之为DMA,由于DMA的存在,使得CPU从漫长的磁盘操作中解放出来,一般在进行磁盘读写时,CPU主要向DMA发出一个读写命令,然后就继续执行后面的工作,当读写完成后DMA向CPU发出完成的指令,这个时候CPU会停下手上的工作,来处理这个通知,程序此时会陷入中断,直到CPU完成对应的操作。由于DMA的出现使得CPU从慢速的磁盘操作中解放出来,但是在同步的读写方式中,CPU发出磁盘的读写指令后什么都不做,一直等待磁盘的读写玩成,使CPU长时间陷入等待状态,浪费了宝贵的CPU的资源。所以为了程序效率,在读写磁盘时一般使用异步的方式,在发出读写命令后立即返回,然后执行后面的操作,这样就在一定程度上利用了闲置的CPU资源。
重叠IO
在Windows中默认使用同步的方式进行读写操作,如果要使用异步的方式,在创建文件句柄的时候,需要在CreateFile函数的dwFlagsAndAttributes参数中加上FILE_FLAG_OVERLAPPED标识,然后可以设置一个完成函数,并在对应线程中调用waitex函数或者使用SleepEx函数使线程陷入可警告状态,当读写操作完成时会将完成函数插入线程的APC队列,当线程进入可警告状态的时候会调用APC函数,这样就可以知道读写操作已经完成。这是一种方式,还可以使用一个OVERLAPPED结构,并给这个结构中填上一个事件对象,在需要进行同步的地方等待这个事件对象,在磁盘操作完成的时候会将其设置为有信号,上面的两种方式都利用的Windows提供的重叠IO模型
不管使用哪种方式,在进行文件的异步操作时都需要自己维护并偏移文件指针。在同步的方式时Windows是完成之后返回,它一次只会写入一条数据到磁盘,而且它也知道具体写入了多少数据,这时候系统帮助我们完成了文件指针的偏移,但是在进行异步操作的时候可能会同时有多条数据写入,并且系统不知道具体会成功写入多少数据,所以它不可能帮我们进行文件指针的偏移,这个时候就需要自己进行偏移操作
完成函数
使用完成函数主要需要如下步骤:
1. 调用CreateFile在dwFlagsAndAttributes参数中加上FILE_FLAG_OVERLAPPED标识表示我们需要使用异步的方式来进行磁盘操作
2. 准备一个完成函数,函数的原型为:VOID CALLBACK FileIOCompletionRoutine(DWORD dwErrorCode,DWORD dwNumberOfBytesTransfered,LPOVERLAPPED lpOverlapped);函数的最后一个参数是一个OVERLAPPED结构,该结构的定义如下:
typedef struct _OVERLAPPED {
ULONG_PTR Internal;
ULONG_PTR InternalHigh;
union {
struct {
DWORD Offset;
DWORD OffsetHigh;
};
PVOID Pointer;
};
HANDLE hEvent;
} OVERLAPPED, *LPOVERLAPPED;
这个结构中有一个共用体,其实这个共用体都可以用来操作文件指针,如果用其中的结构体,那么需要分别给其中的高32位和低32位赋值,如果使用指针,这个时候指针变量不指向任何内存,这个指针变量仅仅是作为一个变量名罢了,使用时也是将其作为正常变量来使用,虽然它是一个指针占4个字节,但是由于是一个共用体,它后面还有4个字节的剩余空间可以使用,所以使用它来存储文件指针的偏移没有任何问题。
3. 调用ReadFileEx或者WriteFileEx函数(ReadFile WriteFile不支持完成函数的方式)并将完成函数作为最后一个参数传入
4. 调用WaitEx族的等待函数或者SleepEx函数使线程陷入可警告状态,这个时候会执行完成函数
下面是一个演示的例子
LARGE_INTEGER g_FilePointer = {0}; //全局的文件指针
struct ST_EXT_OVERLAPPED
{
OVERLAPPED m_ol; //后面的代码在使用的时候后
HANDLE m_hFile; //操作的文件句柄
LPVOID m_pData; //操作的内存
DWORD m_dwLen; //操作的数据长度
};
VOID CALLBACK FileIOCompletionRoutine(DWORD dwErrorCode,DWORD dwNumberOfBytesTransfered,LPOVERLAPPED lpOverlapped)
{
ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)lpOverlapped;
printf("线程[%04x]完成写入操作\n", GetCurrentThreadId());
HeapFree(GetProcessHeap(), 0, pExOl->m_pData);
HeapFree(GetProcessHeap(), 0, pExOl);
pExOl = NULL;
}
DWORD WriteThreadProc(LPVOID lpParameter)
{
HANDLE hFile = *(HANDLE*)(lpParameter);
ST_EXT_OVERLAPPED* pExOl = NULL;
TCHAR szBuf[256] = _T("");
StringCchPrintf(szBuf, 256, _T("这是一条模拟日志写入信息,由线程[%04x]写入\r\n"), GetCurrentThreadId());
size_t dwLen = 0;
StringCchLength(szBuf, 256, &dwLen);
dwLen += 1; //保存字符串结尾的\0
for (int i = 0; i < 100; i++)
{
pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED));
pExOl->m_dwLen = dwLen * sizeof(TCHAR);
pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLen * sizeof(TCHAR));
StringCchCopy((TCHAR*)pExOl->m_pData, 256, szBuf);
pExOl->m_hFile = hFile;
//使用锁无关的方式进行同步操作
*((LONGLONG*)&pExOl->m_ol.Pointer) = InterlockedCompareExchange64(&g_FilePointer.QuadPart, g_FilePointer.QuadPart + pExOl->m_dwLen, g_FilePointer.QuadPart);
WriteFileEx(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, (OVERLAPPED*)&pExOl->m_ol, FileIOCompletionRoutine);
//do something
if(WAIT_IO_COMPLETION == SleepEx(INFINITE, TRUE))
{
}
}
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
HANDLE hFile = CreateFile(_T("log.txt"), GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);//让其支持异步操作
if (hFile == INVALID_HANDLE_VALUE)
{
printf("CreateFile error\n");
return GetLastError();
}
ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED));
pExOl->m_hFile = hFile;
pExOl->m_dwLen = sizeof(WORD);
pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WORD));
*((WORD*)pExOl->m_pData) = MAKEWORD(0xff,0xfe);
//文件指针的偏移
pExOl->m_ol.Offset = g_FilePointer.LowPart;
pExOl->m_ol.OffsetHigh = g_FilePointer.HighPart;
g_FilePointer.QuadPart += pExOl->m_dwLen;
WriteFileEx(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, (LPOVERLAPPED)&pExOl->m_ol, FileIOCompletionRoutine);
HANDLE hThreads[20] = {NULL};
for (int i = 0; i < 20; i++) //创建20个写线程
{
hThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteThreadProc, &hFile, 0, NULL);
}
while(WAIT_IO_COMPLETION == WaitForMultipleObjectsEx(20, hThreads, TRUE, INFINITE, TRUE)) //函数返回WAIT_IO_COMPLETION 表示执行了完成函数
{
printf("有一个读写操作完成\n");
}
for (int i = 0; i < 20; i++)
{
CloseHandle(hThreads[i]);
}
CloseHandle(hFile);
_tsystem(_T("PAUSE"));
return 0;
}
在上面的例子中,我们首先向文件中写入0xff, 0xfe这两个值,在Windows中存储Unicode字符串的文件都是以0xff 0xfe开头,所以在写入Unicode字符串之前需要写入这两个值
然后创建了20个线程,每个线程负责往文件中写入100条数据。线程先创建了一个包含OVERLAPPED结构的数据类型,然后再使用InterlockedCompareExchange64同步文件指针,这句话的意思是,向将高速缓存中的数据与内存中的数据进行比较,如果二者的值相同,那么久更改全局的文件指针,否则就不进行变化。实际上在Intel架构的机器上存在大量的高速缓存,为了效率,有的时候会将一些数据放置到高速缓存中,这样造成高速缓存中一份,内存中也有一份,有的时候在进行值得更改时它只会改变内存中的值,而高速缓存中的值不会更新,在调用这个函数的时候第一个参数传入的是一个指针,取值操作会强制CPU到内存中进行访问,这样这句话实质上是比较高速缓存与内存中的值是否一致,如果不一致,那么说明它被其他的线程进行过修改,将新的文件指针进行了替换,那么这个时候不需要进行任何操作,在之前写入文件的末尾进行追加即可,如果没有发生修改,那么其他线程可能会在当前位置写入,本线程也在当前位置写的话会造成覆盖,所以往后偏移文件指针,使其他线程使用新偏移的位置,本线程使用当前的位置,这样就不会发生覆盖
在完成历程中完成清理内存的任务。每个WriteFileEx都对应着内存的分配,完成后都会调用这个完成历程清理对应的内存,这样就不会造成内存泄露。
最后在主线程中等待子线程的完成,然后关闭句柄并结束进程
事件模型
事件模型与之前的完成历程相似,只是它不需要设置完成函数,需要在OVERLAPPED结构中设置一个事件,当IO操作完成时会将这个事件设置为有信号,然后在需要进行同步的位置等待这个事件即可
下面是它的具体的例子
LARGE_INTEGER g_FilePointer = {0}; //全局的文件指针
struct ST_EXT_OVERLAPPED
{
OVERLAPPED m_ol; //后面的代码在使用的时候后
HANDLE m_hFile; //操作的文件句柄
LPVOID m_pData; //操作的内存
DWORD m_dwLen; //操作的数据长度
};
DWORD WriteThreadProc(LPVOID lpParameter)
{
HANDLE hFile = *(HANDLE*)(lpParameter);
ST_EXT_OVERLAPPED* pExOl = NULL;
TCHAR szBuf[256] = _T("");
StringCchPrintf(szBuf, 256, _T("这是一条模拟日志写入信息,由线程[%04x]写入\r\n"), GetCurrentThreadId());
size_t dwLen = 0;
StringCchLength(szBuf, 256, &dwLen);
dwLen += 1; //保存字符串结尾的\0
for (int i = 0; i < 100; i++)
{
pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED));
pExOl->m_dwLen = dwLen * sizeof(TCHAR);
pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLen * sizeof(TCHAR));
StringCchCopy((TCHAR*)pExOl->m_pData, 256, szBuf);
pExOl->m_hFile = hFile;
pExOl->m_ol.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
//使用锁无关的方式进行同步操作
*((LONGLONG*)&pExOl->m_ol.Pointer) = InterlockedCompareExchange64(&g_FilePointer.QuadPart, g_FilePointer.QuadPart + pExOl->m_dwLen, g_FilePointer.QuadPart);
DWORD dwWritten = 0;
WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (OVERLAPPED*)&pExOl->m_ol);
//do something
if(WAIT_OBJECT_0 == WaitForSingleObject(pExOl->m_ol.hEvent, INFINITE))
{
printf("线程[%04x],写入操作完成一次,继续等待写入.....\n", GetCurrentThreadId());
HeapFree(GetProcessHeap(), 0, pExOl->m_pData);
HeapFree(GetProcessHeap(), 0, pExOl);
}
}
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
HANDLE hFile = CreateFile(_T("log.txt"), GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);//让其支持异步操作
if (hFile == INVALID_HANDLE_VALUE)
{
printf("CreateFile error\n");
return GetLastError();
}
ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED));
pExOl->m_hFile = hFile;
pExOl->m_dwLen = sizeof(WORD);
pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WORD));
*((WORD*)pExOl->m_pData) = MAKEWORD(0xff,0xfe);
pExOl->m_ol.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
//文件指针的偏移
pExOl->m_ol.Offset = g_FilePointer.LowPart;
pExOl->m_ol.OffsetHigh = g_FilePointer.HighPart;
g_FilePointer.QuadPart += pExOl->m_dwLen;
DWORD dwWritten = 0;
WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (LPOVERLAPPED)&pExOl->m_ol);
HANDLE hThreads[20] = {NULL};
//等待当前写入完成
if (WAIT_OBJECT_0 == WaitForSingleObject(pExOl->m_ol.hEvent, INFINITE))
{
printf("写入头部操作完成\n");
HeapFree(GetProcessHeap(), 0, pExOl->m_pData);
HeapFree(GetProcessHeap(), 0, pExOl);
}
for (int i = 0; i < 20; i++) //创建20个写线程
{
hThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteThreadProc, &hFile, 0, NULL);
}
WaitForMultipleObjects(20, hThreads, TRUE, INFINITE);
for (int i = 0; i < 20; i++)
{
CloseHandle(hThreads[i]);
}
CloseHandle(hFile);
_tsystem(_T("PAUSE"));
return 0;
}
上面的例子与之前的完成历程的例子基本上一样,只是在OVERLAPPED结构中加入EVENT对象,并且没有完成历程,内存的清理工作需要在本线程中进行清理
完成端口
上述重叠IO在一定程度上解决的线程陷入等待的问题,但是从上面的代码上来看,仍然需要在本线程中进行等待操作,也就是说,如果在IO函数返回后进行某项操作,但是这项操作完成后而IO操作并没有完成,那么仍然要陷入等待,现在有一个想法,就是同步操作不在本线程中完成,另外开辟一个线程,将所有的等待操作都放到新线程中,而本线程就不必进行等待,同步线程只需要在操作完成的时候启动执行,这样几乎就不存在CPU等待IO设备的问题。主要的问题是,怎么向新线程传递同步对象,就像上面的例子来说,等待IO操作完成就是为了清理内存而已,这个时候如果创建新线程进行等待的话,总共有2000个写入操作,为了清理每块内存,需要定义一个2000O包含VERLAPPED结构的数组,然后当所有线程启动后将数组指针传入,如果为每个如果动态添加新的写入线程,那就必须修改数组大小。这给编程造成了很大的麻烦,为了解决这个问题,VC中引入了完成端口模型
本质上完成端口利用了线程池机制并结合了重叠IO的优势,在Windows下这种IO模型是最高效的一种。
完成端口首先创建对应数量的线程的线程池,然后将相关的文件句柄与完成端口对象绑定,并传入一个OVERLAPPED结构的指针,然后进行等待,一旦有IO操作完成,就会启动完成端口中的线程,完成后续的操作。
完成端口的使用一般经过下面几个步骤:
1. 调用CreateIoCompletionPort创建完成端口对象,并制定最大并发线程数(一般制定CPU核数或者核数的两倍)
2. 创建用于完成端口的线程,一般大于等于最大并发数
3. 调用函数CreateIoCompletionPort,将文件句柄与完成端口绑定
4. 在IO操作中传入一个OVERLAPPED结构
5. 在完成端口的线程中调用GetQueuedCompletionStatus进行等待,当有IO操作完成时函数会返回,对应的线程就可以启动执行
函数CreateIoCompletionPort原型如下
HANDLE WINAPI CreateIoCompletionPort(
__in HANDLE FileHandle,
__in_opt HANDLE ExistingCompletionPort,
__in ULONG_PTR CompletionKey,
__in DWORD NumberOfConcurrentThreads
);
第一个参数是文件句柄,第二参数是完成端口句柄,第三个参数是一个完成的标识。一般给NULL,第四个是最大线程数。一般在操作的时候如果是创建完成端口句柄,那么只需要指定最大并发线程数,如果是将文件句柄和完成端口对象进行绑定,只需要提供前连个参数。在下面的例子中可以很清楚的看到它的用法
下面是一个使用完成端口的例子:
LARGE_INTEGER g_FilePointer = {0}; //全局的文件指针
struct ST_EXT_OVERLAPPED
{
OVERLAPPED m_ol; //后面的代码在使用的时候后
HANDLE m_hFile; //操作的文件句柄
LPVOID m_pData; //操作的内存
DWORD m_dwLen; //操作的数据长度
BOOL bExit;
};
DWORD WriteThreadProc(LPVOID lpParameter)
{
HANDLE hFile = *(HANDLE*)(lpParameter);
ST_EXT_OVERLAPPED* pExOl = NULL;
TCHAR szBuf[256] = _T("");
StringCchPrintf(szBuf, 256, _T("这是一条模拟日志写入信息,由线程[%04x]写入\r\n"), GetCurrentThreadId());
size_t dwLen = 0;
StringCchLength(szBuf, 256, &dwLen);
dwLen += 1; //保存字符串结尾的\0
for (int i = 0; i < 100; i++)
{
pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED));
pExOl->m_dwLen = dwLen * sizeof(TCHAR);
pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLen * sizeof(TCHAR));
StringCchCopy((TCHAR*)pExOl->m_pData, 256, szBuf);
pExOl->m_hFile = hFile;
pExOl->bExit = FALSE;
//使用锁无关的方式进行同步操作
*((LONGLONG*)&pExOl->m_ol.Pointer) = InterlockedCompareExchange64(&g_FilePointer.QuadPart, g_FilePointer.QuadPart + pExOl->m_dwLen, g_FilePointer.QuadPart);
DWORD dwWritten = 0;
WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (OVERLAPPED*)&pExOl->m_ol);
}
return 0;
}
DWORD IocpThreadProc(LPVOID lpParameter)
{
HANDLE hIocp = *(HANDLE*)lpParameter;
DWORD dwBytesTransfered = 0;
DWORD dwFlags = 0;
LPOVERLAPPED pOl = NULL;
while (TRUE)
{
ST_EXT_OVERLAPPED* pExOl = NULL;
BOOL bRet = GetQueuedCompletionStatus(hIocp, 0, 0, &pOl, INFINITE);//MSDN上说如果完成端口队列为空,那么函数会返回FLASE,并且pOl为NUULL, 所以在这进行判断,如果为FLASE,就不往下执行,否则程序会崩溃
if (!bRet)
{
continue;
}
pExOl = (ST_EXT_OVERLAPPED*)pOl;
if (pExOl->bExit)
{
printf("收到退出消息,IOCP线程[%04x]退出", GetCurrentThreadId());
HeapFree(GetProcessHeap(), 0, pExOl);
return 0;
}
printf("有一个线程的写入操作完成\n");
HeapFree(GetProcessHeap(), 0, pExOl->m_pData);
HeapFree(GetProcessHeap(), 0, pExOl);
}
}
int _tmain(int argc, _TCHAR* argv[])
{
HANDLE hFile = CreateFile(_T("log.txt"), GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);//让其支持异步操作
if (hFile == INVALID_HANDLE_VALUE)
{
printf("CreateFile error\n");
return GetLastError();
}
//创建IOCP内核对象并制定最大并发线程数
SYSTEM_INFO si = {0};
GetSystemInfo(&si);
HANDLE hIocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 2 * si.dwNumberOfProcessors);
//创建IOCP线程
HANDLE* hIocpThreads = (HANDLE*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 2 * si.dwNumberOfProcessors * sizeof(HANDLE));
for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++)
{
hIocpThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)IocpThreadProc, &hIocp, 0, NULL);
}
//将文件句柄与IOCP句柄绑定
CreateIoCompletionPort(hFile, hIocp, NULL, 0);
ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED));
pExOl->m_hFile = hFile;
pExOl->m_dwLen = sizeof(WORD);
pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WORD));
*((WORD*)pExOl->m_pData) = MAKEWORD(0xff,0xfe);
pExOl->bExit = FALSE;
//文件指针的偏移
pExOl->m_ol.Offset = g_FilePointer.LowPart;
pExOl->m_ol.OffsetHigh = g_FilePointer.HighPart;
g_FilePointer.QuadPart += pExOl->m_dwLen;
DWORD dwWritten = 0;
WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (LPOVERLAPPED)&pExOl->m_ol);
HANDLE hThreads[20] = {NULL};
for (int i = 0; i < 20; i++) //创建20个写线程
{
hThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteThreadProc, &hFile, 0, NULL);
}
//等待写入线程的完成
WaitForMultipleObjects(20, hThreads, TRUE, INFINITE);
for (int i = 0; i < 20; i++)
{
CloseHandle(hThreads[i]);
}
//关闭IOCP线程
for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++)
{
ST_EXT_OVERLAPPED* pExitMsg = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED));
pExitMsg->bExit = TRUE;
PostQueuedCompletionStatus(hIocp, 0, 0, &pExitMsg->m_ol);
}
//关闭IOCP线程句柄
for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++)
{
CloseHandle(hIocpThreads[i]);
}
CloseHandle(hFile);
_tsystem(_T("PAUSE"));
return 0;
}