背景
由于Windows自带的文件搜索的搜索速度比较慢,所以我们需要自己写一个文件搜索器来提高文件的扫描速度。
文件搜索需要用到两个Windows API,一个是FindFirstFile,另一个是FindNextFile函数。
首先我们先来看一下这两个函数的原型:
HANDLE WINAPI FindFirstFile(
_In_ LPCTSTR lpFileName,
_Out_ LPWIN32_FIND_DATA lpFindFileData
);
lpFileName要搜索的目录或者路径。需要注意的是后面还需要加上过滤条件,比如,我们想搜索C盘中所有的文件或者文件夹,这个参数就应该是:C:\*.*。其它的就没有什么需要注意的了!!!
lpFindFileData,这是一个输出参数,它包含了找到的文件或者目录的信息。
返回值如果函数执行成功,它返回的是一个搜索句柄,如果失败,返回 INVALID_HANDLE_VALUE。
typedef struct _WIN32_FIND_DATA {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD dwReserved0;
DWORD dwReserved1;
TCHAR cFileName[MAX_PATH];
TCHAR cAlternateFileName[14];
} WIN32_FIND_DATA, *PWIN32_FIND_DATA, *LPWIN32_FIND_DATA;
dwFileAttributes文件或目录的属性。
ftCreationTime文件或者目录创建的时间。
ftLastAccessTime对于一个文件,它记录了最后一次读、写或者执行的时间;对于一个目录,它记录了这个目录是什么时候被创建的。
ftLastWriteTime对于一个文件,它记录了最后一次写入,截断、=或者重写的时间;对于一个目录,它记录了这个目录是什么时候被创建的。
nFileSizeHigh文件大小的高阶段,以字节为单位。
nFileSizeLow文件大小的低阶段,以字节为单位。
dwReserved0如果dwFileAttributes 包含了FILE_ATTRIBUTE_REPARSE_POINT 属性, 该成员指定重新解析点标记,否则这个成员的值是未定义的,并且也不该使用。
dwReserved1被保留,用于将来使用。
cFileName[MAX_PATH]文件名字。
cAlternateFileName[14]该文件的后缀名。
BOOL WINAPI FindNextFile(
_In_ HANDLE hFindFile,
_Out_ LPWIN32_FIND_DATA lpFindFileData
);
hFindFile由 FindFirstFile函数返回的搜索句柄。
lpFindFileData这是一个输出参数,它包含了找到的文件或者目录的信息。
返回值如果函数执行成功,它返回的是一个非零值,并且lpFindFileData结构体被填充,否则返回零。
递归实现方式
我们的第一个版本是使用递归的方式进行
#include <Windows.h>
#include <string>
#include <stdio.h>
DWORD g_dwFindFileNumber = 0, g_dwSearchFileNumber = 0, g_dwSearchDirectoryNumber = 0;
std::wstring MakeStandardDir(std::wstring wstrDir)
{
if (wstrDir[wstrDir.size()-1] != '\\')
{
wstrDir += '\\';
}
return wstrDir;
}
void MyFindFile(std::wstring wstrBeginDir, std::wstring wstrFindFileName)
{
WIN32_FIND_DATA findData = { 0 };
HANDLE hFile = FindFirstFileW((MakeStandardDir(wstrBeginDir) + L"*.*").c_str(), &findData);
do
{
if (wcscmp(findData.cFileName, L".") == 0)
{
continue;
}
if (wcscmp(findData.cFileName, L"..") == 0)
{
continue;
}
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
//printf("%ls 是一个目录!\r\n", findData.cFileName);
MyFindFile((MakeStandardDir(wstrBeginDir) + findData.cFileName).c_str(), wstrFindFileName);
++g_dwSearchDirectoryNumber;
}
else
{
//printf("%ls 是一个文件!\r\n", findData.cFileName);
if (wcsstr(findData.cFileName, wstrFindFileName.c_str()) != nullptr)
{
printf("%ls%ls\r\n", wstrBeginDir.c_str(), findData.cFileName);
++g_dwSearchFileNumber;
++g_dwFindFileNumber;
}
++g_dwSearchFileNumber;
}
}
while (FindNextFileW(hFile, &findData));
}
int main()
{
std::wstring wstrBeginDir = L"C:";
std::wstring wstrFileName = L"ntdll";
DWORD dwPrevTime = GetTickCount();
MyFindFile(wstrBeginDir, wstrFileName);
DWORD dwTimeUse = (GetTickCount() - dwPrevTime)/1000;
printf("\r\n\r\n");
printf("共找到 %lu 个 %ls 相关的文件!\r\n", g_dwFindFileNumber, wstrFileName.c_str());
printf("共查找了 %lu 个文件夹, %lu 个文件!\r\n", g_dwSearchDirectoryNumber, g_dwSearchFileNumber);
printf("用时 %lu 秒\r\n", dwTimeUse);
system("pause");
return 0;
}
执行结果如下所示:
上面的程序需要注意以下几点:
- 条件判断(组合值)
应该是if(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY),而不该是if(findData.dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY),因为这里比较的是位,就不多做解释了。
- 过滤条件
在传入的第一个目录时,应该是“C:\\*.*”,而不应该只是“C:\\”,还需要加上后面的“*.*”的过滤条件。
- 遇到文件和文件夹这两种情况时候的应该做相应的处理
当遇到文件的时候,就使用模糊匹配,如果包含我们的搜索条件,那么就是我们要找的文件;
当遇到文件夹的时候,就需要进入文件夹(进入文件夹的时候还需要注意加上"*.*",这个过滤条件),从而扫描文件夹里面的所有文件或者文件夹,这就是递归的使用方法。
- 路径
对于文件夹名,它的后面是没有"\\",我们需要给它加上,这事是我们上面写的MakeStandardDir函数。
- 特殊的目录
无论是在Linux系统还是在Windows系统中,每个文件夹中都会有一个"."目录和一个".."目录,"."表示当前目录,".."表示上一级目录。
“多线程”实现方式
细心的朋友看到这个标题的时候肯定会想,多线程就多线程吧,为什么还要加上一个双引号呢?难道不是真正的多线程,而是一个伪多线程吗?…
其实它是一个真正的多线程,只不过启用的线程太多了而已。反正是不正常的多线程。实现的逻辑是这样的:
我们每遇到一个目录就开启一个线程来负责扫描本线程中的文件,这样看起来好像很完美呀!你看我开这么多的线程来同时扫描你一个小小的硬盘,那速度肯定也是没谁了!!!我们先不要下结论,先按照我们的思路把代码实现,然后运行一次看看结果再下结论吧!
#include <string>
#include <stdio.h>
#include <process.h>
#include <Windows.h>
HANDLE g_hExitEvent;
CRITICAL_SECTION g_cs;
long g_lFindFileNumber = 0, g_lSearchFileNumber = 0, g_lSearchDirectoryNumber = 0, g_lNeedSearchDirNum = 0;
std::wstring MakeStandardDir(std::wstring wstrDir)
{
if (wstrDir.back() != L'\\')
{
wstrDir += L'\\';
}
return wstrDir;
}
struct ThreadData
{
std::wstring wstrDirectoryName;
std::wstring wstrSearch;
std::wstring wstrFilter;
};
unsigned int __stdcall ThreadFindFile(void *lParam)
{
InterlockedAdd(&g_lNeedSearchDirNum, 1);
ThreadData *pData = (ThreadData *)lParam;
WIN32_FIND_DATA findData = { 0 };
HANDLE hFile = FindFirstFileW((MakeStandardDir(pData->wstrDirectoryName) + pData->wstrFilter).c_str(), &findData);
do
{
if (wcscmp(findData.cFileName, L".") == 0)
{
continue;
}
if (wcscmp(findData.cFileName, L"..") == 0)
{
continue;
}
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
ThreadData *pTempData = new ThreadData;
pTempData->wstrDirectoryName = MakeStandardDir(pData->wstrDirectoryName) + findData.cFileName;
pTempData->wstrFilter = pData->wstrFilter;
pTempData->wstrSearch = pData->wstrSearch;
EnterCriticalSection(&g_cs);
CloseHandle((HANDLE)_beginthreadex(nullptr, 0, ThreadFindFile, pTempData, 0, nullptr));
LeaveCriticalSection(&g_cs);
InterlockedAdd(&g_lSearchDirectoryNumber, 1);
}
else
{
if (wcsstr(findData.cFileName, pData->wstrSearch.c_str()) != nullptr)
{
EnterCriticalSection(&g_cs);
printf("%ls%ls\r\n", pData->wstrDirectoryName.c_str(), findData.cFileName);
LeaveCriticalSection(&g_cs);
InterlockedAdd(&g_lFindFileNumber, 1);
}
InterlockedAdd(&g_lSearchFileNumber, 1);
}
} while (FindNextFileW(hFile, &findData));
delete pData;
InterlockedAdd(&g_lNeedSearchDirNum, -1);
if (g_lNeedSearchDirNum == 0)
{
SetEvent(g_hExitEvent);
}
return 0;
}
int main()
{
InitializeCriticalSection(&g_cs);
g_hExitEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
ThreadData *pData = new ThreadData;
pData->wstrDirectoryName = L"C:";
pData->wstrFilter = L"*.*";
pData->wstrSearch = L"ntdll";
DWORD dwPrevTime = GetTickCount();
CloseHandle((HANDLE)_beginthreadex(nullptr, 0, ThreadFindFile, pData, 0, nullptr));
WaitForSingleObject(g_hExitEvent, INFINITE);
DWORD dwTimeUse = (GetTickCount() - dwPrevTime) / 1000;
printf("共找到 %lu 个 ntdll 相关的文件!\r\n", g_lFindFileNumber);
printf("共查找了 %lu 个文件夹, %lu 个文件!\r\n", g_lSearchDirectoryNumber, g_lSearchFileNumber);
printf("用时 %lu 秒\r\n", dwTimeUse);
DeleteCriticalSection(&g_cs);
system("pause");
return 0;
}
以上代码已经准备好,我们就开始测试吧!!!期待中~
看到这个结果是不是很惊讶!!!首先结果跟我们的递归方式扫描的结果不一样啊!!!怎么无缘无故少了4个???还有扫描时间竟然是216秒,是递归扫描用时34秒的6倍多,这还是多线程吗?简直就是没法玩了!!!
这是哪里出问题了呢?可是我们的逻辑也是经过谨慎的思考了呀。
先不要着急,我们再来运行一下看看结果:
这次运行结果怎么时间也短了,最重要的是貌似我们要找的结果好像也对了, 但是这并非是个正确的结果,因为查找到的文件个数和文件夹个数和递归扫描的结果不相等,所以这次的扫描也是不完整的。
下面我们来分析一下为什么会出现这样的结果:
首先,我们创建线程的时候,有可能会失败,这个我们没有考虑到,下面我们看一下这个错误结果。
//CloseHandle((HANDLE)_beginthreadex(nullptr, 0, ThreadFindFile, pTempData, 0, nullptr));
HANDLE hThread = (HANDLE)_beginthreadex(nullptr, 0, ThreadFindFile, pTempData, 0, nullptr);
if (hThread != NULL)
{
CloseHandle(hThread);
}
else
{
printf("创建线程失败,错误值为:%d\r\n", errno);
}
上面是代码修改的部分,当再次执行这个程序的时候,会打印出下面的结果:
错误码是12,当查询msdn的时候,会告诉我们这是没有足够内存的错误!!!
这个错误我们肯定是能够解决的,比如,通过一个循环,直到线程创建成功的时候才退出循环。不过我们不再对这个多线程版本进行修改了,因为从时间上来看,还不如单线程的递归方式扫描的快,所以再进行改进也就没什么意义!!!