首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
105 阅读
2
nvim番外之将配置的插件管理器更新为lazy
78 阅读
3
2018总结与2019规划
62 阅读
4
PDF标准详解(五)——图形状态
40 阅读
5
为 MariaDB 配置远程访问权限
33 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
linux
elisp
文本编辑器
Java
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
315
篇文章
累计收到
31
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
页面
归档
友情链接
关于
搜索到
147
篇与
的结果
2017-08-14
Vista 及后续版本的新线程池
在上一篇的博文中,说了下老版本的线程池,在Vista之后,微软重新设计了一套线程池机制,并引入一组新的线程池API,新版线程池相对于老版本的来说,它的可控性更高,它允许程序员自己定义线程池,并规定线程池中的线程数量和其他一些属性。线程池使用线程池的使用主要需要下面的四步:创建工作项提交工作项等待工作项完成清理工作项在前面说的四种线程池在使用上都是这4步,只是使用的API函数不同,每种线程池的每一步都有一个对应的API,总共有16个API普通线程池创建工作项的API为PTP_WORK WINAPI CreateThreadpoolWork( __in PTP_WORK_CALLBACK pfnwk, __inout_opt PVOID pv, __in_opt PTP_CALLBACK_ENVIRON pcbe );第一个参数是一个回调函数,当提交后,线程池中的线程会执行这个回调函数第二个参数是传递给回调函数的参数第三个参数是一个表示回调环境的结构,这个在后面会说回调函数的原型VOID CALLBACK WorkCallback( __inout PTP_CALLBACK_INSTANCE Instance, __inout_opt PVOID Context, __inout PTP_WORK Work );第一个参数用于表示线程池当前正在处理的一个工作项的实例,在后面会说它怎么用第二个参数是传给回调函数的参数的指针第三个参数是当前工作项的结构创建工作项完成之后调用SubmitThreadpoolWork将工作项提交到对应的线程池,由线程池中的线程处理这个工作项,该函数原型如下:VOID WINAPI SubmitThreadpoolWork( __inout PTP_WORK pwk );这个函数只有一个参数那就是工作项的指针,即我们想将哪个工作项提交。提交工作项之后,在需要同步的地方,调用函数WaitForThreadpoolWorkCallbacks,等待线程池中的工作项完成,该函数原型如下VOID WINAPI WaitForThreadpoolWorkCallbacks( __inout PTP_WORK pwk, __in BOOL fCancelPendingCallbacks );最后一个参数表示线程池是否需要执行未执行的工作项,注意它只能取消执行还没有开始执行的工作项,而不能取消已经有线程开始执行的工作项,最后调用函数CloseThreadpoolWork清理工作项,该函数的原型如下:VOID WINAPI CloseThreadpoolWork( __inout PTP_WORK pwk );就我个人的理解,TP_WORK应该保存的是一个工作项的信息,包含工作项的回调以及传递个回调函数的参数,每当提交一个工作项就是把这个结构放入到线程池的队列中,当线程池中有空闲线程的时候从队列中取出这个结构,将结构中的回调函数参数传递给回调函数,并调用它。我们可以重复提交同一个工作项多次,但是每个工作项一旦定义好了,那么传递给对应回调函数的参数应该是固定的,后期是没办法更改它的。它的等待函数调用时根据第二个参数,如果为TRUE则将线程池队列中的工作项清除,然后等待所有线程都为空闲状态时返回,而当参数为FALSE时,就不对队列中的工作项进行操作,并且一直等到线程池中的所有线程为空闲。下面是一个具体的使用例子:VOID CALLBACK MyWorkCallback( PTP_CALLBACK_INSTANCE Instance, PVOID Parameter, PTP_WORK Work ) { int nWaitTime = 4; printf("线程[%04x]将等待%ds\n", GetCurrentThreadId(), nWaitTime); Sleep(nWaitTime * 1000); printf("线程[%04x]执行完毕\n", GetCurrentThreadId()); } int _tmain(int argc, _TCHAR* argv[]) { PTP_WORK_CALLBACK workcallback = MyWorkCallback; PTP_WORK work = CreateThreadpoolWork(workcallback, NULL, NULL); //创建工作项 for (int i = 0; i < 4; i++) { SubmitThreadpoolWork(work); //提交工作项 } //等待线程池中的所有工作项完成 WaitForThreadpoolWorkCallbacks(work, FALSE); //关闭工作项 CloseThreadpoolWork(work); return 0; }定时器线程池定时器线程池中使用的对应的API分别为CreateThreadpoolTimer、SetThreadpoolTimer、WaitForThreadpoolTimerCallbacks和CloseThreadpoolTimer,这些函数的参数与之前的函数参数基本类似,区别比较大的是SetThreadpoolTimer,由于涉及到定时器,所以这里的参数稍微复杂一点VOID WINAPI SetThreadpoolTimer( __inout PTP_TIMER pti, __in_opt PFILETIME pftDueTime, __in DWORD msPeriod, __in_opt DWORD msWindowLength );第二个参数表示定时器触发的时间,它是一个64位的整数,如果为正数表示一个绝对的时间,表示从1960年到多少个100ns的时间后触发,如果为负数则表示从设置之时起经过多少时间后触发,单位为微秒(转化为秒是1000 * 1000)第三个参数每隔多长时间触发一次,如果只是想把这个定时器作为一次性的,和第四个参数没有用处,而如果想让线程池定期的触发它,这个值就是定期触发的间隔 时间,单位为毫秒第四个参数是用来给回调函数的执行时机增加一定的随机性,如果这个定时器是一个定期触发的定时器,那么这个值告诉线程池,可以在自定时器设置时间起,在(msPeriod - msWindowLength, mePeriod + msWindowsLong)这个区间之后的任意时间段触发另外我自己在编写测试代码的时候发现有的时候调用WaitForThreadpoolTimerCallbacks可能立即就返回了,后来我自己分析可能的原因是这个函数会在线程池队列中没有需要处理的工作项,并且线程池中线程为空闲的时候返回,当我使用定时器的时候,在等待时可能这个时候定时器上的时间未到,而线程池中又没有需要处理的定时器的工作项,所以它就返回了从而未达到等待的效果。下面是一个使用的具体例子,这个例子是《Windows核心编程》这本书中的例子,我觉得它里面有一个更改MessageBox显示信息的功能,所以将其修改了下作为例子int g_nWaitTime = 10; TCHAR g_szTitle[] = _T("提示"); #define ID_MSGBOX_STATIC_TEXT 0x0000ffff //MessageBox上内容部分的控件ID VOID CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_TIMER Timer) { HWND hWnd = FindWindow(NULL, g_szTitle); //找到MessageBox所对应的窗口句柄 if (NULL != hWnd) { TCHAR szText[1024] = _T(""); StringCchPrintf(szText, 1024, _T("您将有%ds的时间"), --g_nWaitTime); SetDlgItemText(hWnd, ID_MSGBOX_STATIC_TEXT, szText); //更改显示信息 } if (g_nWaitTime == 0) { ExitProcess(0); } } int _tmain(int argc, _TCHAR* argv[]) { //创建定时器历程 PTP_TIMER pTimer = CreateThreadpoolTimer(TimerCallback, NULL, NULL); //将定时器历程加入到线程池 ULARGE_INTEGER uDueTime = {0}; FILETIME FileDueTime = {0}; uDueTime.QuadPart = (LONGLONG) -(1 * 10 * 1000 * 1000); //时间为1s FileDueTime.dwHighDateTime = uDueTime.HighPart; FileDueTime.dwLowDateTime = uDueTime.LowPart; SetThreadpoolTimer(pTimer, &FileDueTime, 1000, 0); //每1s调用一次 WaitForThreadpoolTimerCallbacks(pTimer, FALSE); //此处调用等待函数会立即返回 TCHAR szText[] = _T("您将有10s的时间"); MessageBox(NULL, szText, g_szTitle, MB_OK); //关闭工作项 CloseThreadpoolTimer(pTimer); return 0; }同步对象线程池对这种线程池的使用主要调用这样几个函数: CreateThreadpoolWait、SetThreadpoolWait、WaitForThreadpoolWaitCallbacks、CloseThreadpoolWait ,这几个函数的使用与之前的普通线程池的使用类似,在这就不再进行说明直接给例子VOID CALLBACK WaitCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WAIT Wait, TP_WAIT_RESULT WaitResult) { if (WaitResult == WAIT_OBJECT_0) { printf("[%04x] wait the event\n", GetCurrentThreadId()); }else if (WaitResult == WAIT_TIMEOUT) { printf("[%04x] time out\n", GetCurrentThreadId()); } } int _tmain(int argc, _TCHAR* argv[]) { //创建等待线程池 PTP_WAIT pWait = CreateThreadpoolWait(WaitCallback, NULL, NULL); //创建事件 HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); //等待时间为1s FILETIME ft = {0}; ULARGE_INTEGER uWaitTime = {0}; uWaitTime.QuadPart = (LONGLONG) - 1 * 1000 * 1000; ft.dwHighDateTime = uWaitTime.HighPart; ft.dwLowDateTime = uWaitTime.LowPart; for (int i = 0; i < 5; i++) { //模拟等待5次 SetThreadpoolWait(pWait, hEvent, &ft); Sleep(1000); //休眠 SetEvent(hEvent); } WaitForThreadpoolWaitCallbacks(pWait, FALSE); CloseThreadpoolWait(pWait); CloseHandle(hEvent); return 0; }这种类型的回调函数的WaitResult参数实际上是一个DWORD类型,表示调用这个回调的原因,WAIT_OBJECT_0表示同步对象变为有信号,WAIT_TIMEOUT表示超时WAIT_ABANDONED_0表示穿入的互斥量被遗弃(只有在同步对象为互斥量的时候才会有这种值)完成端口线程池完成端口线程池的使用主要用这些API:CreateThreadpoolIo、StartThreadpoolIo、WaitForThreadpoolIoCallbacks、CloseThreadpoolIo,这些函数的使用也是十分的简单,下面再次将之前的完成端口写日志的例子进行改写:int _tmain(int argc, _TCHAR* argv[]) { TCHAR szAppPath[MAX_PATH] = _T(""); GetAppPath(szAppPath); StringCchCat(szAppPath, MAX_PATH, _T("NewIocpLog.txt")); HANDLE hFile = CreateFile(szAppPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { return 0; } //创建IOCP线程池 g_pThreadpoolIO = CreateThreadpoolIo(hFile, IoCompletionCallback, hFile, NULL); StartThreadpoolIo(g_pThreadpoolIO); //写入Unicode字节码 LPIOCP_OVERLAPPED pIocpOverlapped = (LPIOCP_OVERLAPPED)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(IOCP_OVERLAPPED)); pIocpOverlapped->dwDataLen = sizeof(WORD); pIocpOverlapped->hFile = hFile; WORD dwUnicode = MAKEWORD(0xff, 0xfe); //构造Unicode前缀 pIocpOverlapped->pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WORD)); CopyMemory(pIocpOverlapped->pData, &dwUnicode, sizeof(WORD)); //偏移文件指针 pIocpOverlapped->Overlapped.Offset = g_FilePointer.LowPart; pIocpOverlapped->Overlapped.OffsetHigh = g_FilePointer.HighPart; g_FilePointer.QuadPart += pIocpOverlapped->dwDataLen; //写文件 WriteFile(hFile, pIocpOverlapped->pData, pIocpOverlapped->dwDataLen, &pIocpOverlapped->dwWrittenLen, &pIocpOverlapped->Overlapped); //创建线程进行写日志操作 HANDLE hWrittenThreads[MAX_WRITE_THREAD]; for (int i = 0; i < MAX_WRITE_THREAD; i++) { hWrittenThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteThread, &hFile, 0, NULL); } //等待所有写线程执行完成 WaitForMultipleObjects(MAX_WRITE_THREAD, hWrittenThreads, TRUE, INFINITE); for (int i = 0; i < MAX_WRITE_THREAD; i++) { CloseHandle(hWrittenThreads[i]); } //等待线程池中待处理的IO完成请求 WaitForThreadpoolIoCallbacks(g_pThreadpoolIo, FALSE); CloseHandle(hFile); //关闭IOCP线程池 CloseThreadpoolIo(g_pThreadpoolIO); return 0; } VOID CALLBACK WriteThread(LPVOID lpParam) { TCHAR szBuf[255] = _T("线程[%04x]模拟写入一条日志记录\r\n"); TCHAR szWrittenBuf[255] = _T(""); StringCchPrintf(szWrittenBuf, 255, szBuf, GetCurrentThreadId()); for (int i = 0; i < EVERY_THREAD_WRITTEN; i++) { //提交一个IOCP历程 StartThreadpoolIo(g_pThreadpoolIO); LPIOCP_OVERLAPPED lpIocpOverlapped = (LPIOCP_OVERLAPPED)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(IOCP_OVERLAPPED)); size_t dwBufLen = 0; StringCchLength(szWrittenBuf, 255, &dwBufLen); lpIocpOverlapped->dwDataLen = dwBufLen * sizeof(TCHAR); lpIocpOverlapped->pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (dwBufLen + 1) * sizeof(TCHAR)); CopyMemory(lpIocpOverlapped->pData, szWrittenBuf, dwBufLen * sizeof(TCHAR)); lpIocpOverlapped->hFile = *(HANDLE*)lpParam; //同步文件指针 *((LONGLONG*)&(lpIocpOverlapped->Overlapped.Pointer)) = InterlockedCompareExchange64(&g_FilePointer.QuadPart, g_FilePointer.QuadPart + lpIocpOverlapped->dwDataLen, g_FilePointer.QuadPart); //写文件 WriteFile(lpIocpOverlapped->hFile, lpIocpOverlapped->pData, lpIocpOverlapped->dwDataLen, &lpIocpOverlapped->dwWrittenLen, &lpIocpOverlapped->Overlapped); } } VOID CALLBACK IoCompletionCallback(PTP_CALLBACK_INSTANCE Instance,PVOID Context,PVOID Overlapped,ULONG IoResult,ULONG_PTR NumberOfBytesTransferred,PTP_IO Io) { LPIOCP_OVERLAPPED pIOCPOverlapped = (LPIOCP_OVERLAPPED)Overlapped; //释放对应的内存空间 printf("线程[%04x]得到IO完成通知,写入长度%d\n", GetCurrentThreadId(), pIOCPOverlapped->dwDataLen); if (pIOCPOverlapped->pData != NULL) { HeapFree(GetProcessHeap(), 0, pIOCPOverlapped->pData); } if (NULL != pIOCPOverlapped) { HeapFree(GetProcessHeap(), 0, pIOCPOverlapped); pIOCPOverlapped = NULL; } }在新版的完成端口的线程池中,每当需要进行IO操作时,要保证在IO操作之前调用StartThreadpoolIo提交请求。如果没有那么我们的回调函数将不会被执行。注意:后面两种线程池与旧版的相比,最大的区别在于新版的是一次性的,也就是每提交一次,它只会执行一次,要想让其不停触发就需要不停的进行提交,而旧版的只需要绑定,一旦相应的事件发生,他就会不停地的执行线程池控制回调函数的终止操作线程池提供了一种便利的方法,用来描述当我们的回调函数返回之后,应该执行的一些操作,通过这种方式,可以通知其他线程,回调函数已经执行完毕。通过调用下面的一些API可以设置对应的同步对象,在线程池外的其他线程等待同步对象就可以知道什么时候回调执行完毕函数终止操作LeaveCriticalWhenCallbackReturns当回调函数返回时,线程池会自动调用LeaveCritical,并在参数中传入指定的CRITICAL_SECTION结构ReleaseMutexWhenCallbackReturns当回调函数返回时,线程池会自动调用ReleaseMutexWhen并在参数中传入指定的HANDLEReleaseSemaphoreWhenCallbackReturns当回调函数返回时,线程会自动调用ReleaseSemphore并在参数中传入指定的HANDLESetEventWhenCallbackReturns当回调函数返回时,线程会自动调用SetEvent,并在参数中传入指定的HANDLEFreeLibraryWhenCallbackReturns当回调函数返回时,线程会自动调用FreeLibrary并在参数中传入指定的HANDLE前4个函数给我们提供了一种方式来通知另外一个线程,回调函数调用完成,而最后一个函数则提供了一种在回调函数调用完成之时,清理动态库的方式,如果回调函数是在dll中实现的,但是在回调函数结束之时,我们希望卸载这个dll,这个时候不能调用FreeLibrary,这个时候回调函数虽然完成了任务,但是在后面还有函数栈平衡的操作,如果在返回时,我们将dll从内存中卸载,必然会导致最后的栈平衡操作访问非法内存,从而时应用程序崩溃。但是我们可以调用FreeLibraryWhenCallbackReturns,完成这个任务。下面是一个具体的例子:typedef struct tagWAIT_STRUCT { HANDLE hEvent; DWORD dwThreadId; }WAIT_STRUCT, *LPWAIT_STRUCT; WAIT_STRUCT g_waitStruct = {0}; VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work) { g_waitStruct.dwThreadId = GetCurrentThreadId(); Sleep(1000 * 10); SetEventWhenCallbackReturns(Instance, *(HANDLE*)&g_waitStruct); } int _tmain(int argc, _TCHAR* argv[]) { PTP_WORK pWork = CreateThreadpoolWork(WorkCallback, NULL, NULL); g_waitStruct.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); SubmitThreadpoolWork(pWork); WaitForSingleObject(g_waitStruct.hEvent, INFINITE); printf("线程池中线程[%04x]执行完成\n", g_waitStruct.dwThreadId); CloseThreadpoolWork(pWork); return 0; }上面的代码首先创建一个无信号的event对象,然后在回调函数中调用SetEventWhenCallbackReturns,当回调函数完成之时就会将event设置为有信号,这样我们在主线程中就可以等待,一旦回调函数执行完成,event变为有信号,wait函数就会返回。同时我们定义一个结构体尝试着从线程池中带出一个线程ID,并在主线程中使用它对线程池进行定制上面在讨论四种线程池的时候,使用的都是系统自带的线程池,这些线程池由系统管理,我们只能使用,而不能对它们的一些属性进行定制,但是新版本的线程池中提供了这样的方式,要对线程池进行定制,不能使用系统已经定义好的线程池,得自己定义,定义线程池使用API函数CreateThreadPool,这个函数只有一个参数,这个参数是Windows的保留参数目前应该赋值为NULL。该函数会返回一个PTP_POOL 类型的值,这个值是一个指针,用来标识一个线程池。创建完成之后,我们可以函数SetThreadpoolThreadMaximum 或者SetThreadpoolThreadMinimum来规定线程池中的最大和最小线程。当不需要自定义的线程池的时候可以使用函数CloseThreadPool,来清理自定义线程池。线程池的回调环境线程池的回调环境规定了回调函数的执行环境,比如由哪个线程池中的线程来调用,对应线程池的版本,对应的清理器和其他的属性等等。环境的结构定义如下:typedef struct _TP_CALLBACK_ENVIRON { TP_VERSION Version; //线程池的版本 PTP_POOL Pool; //关联的线程池 PTP_CLEANUP_GROUP CleanupGroup; //对应的环境清理组 PTP_CLEANUP_GROUP_CANCEL_CALLBACK CleanupGroupCancelCallback; PVOID RaceDll; struct _ACTIVATION_CONTEXT *ActivationContext; PTP_SIMPLE_CALLBACK FinalizationCallback; union { DWORD Flags; struct { DWORD LongFunction : 1; DWORD Private : 31; } s; } u; } TP_CALLBACK_ENVIRON, *PTP_CALLBACK_ENVIRON;虽然这个结构微软对外公布,而且是可以在程序中直接使用的,但是最好不要这么做,我们应该使用它提供的API对其进行操作,首先可以调用InitializeThreadpoolEnvironment来创建一个对应的回调环境,对我们传入的TP_CALLBACK_ENVIRON变量进行初始化。然后可以调用函数SetThreadpoolCallbackPool来规定由哪个线程池来调用对应的回调函数,如果将参数ptpp传入NULL,则使用系统默认的线程池。另外还可以调用SetThreadpoolCallbackRunsLong 来告诉线程池,我们的任务需要较长的时间来执行。最后当我们不需要这个回调环境的时候可以使用函数DestroyThreadpoolEnvironment来清理这个结构。我自己在看这一块的时候很长时间都转不过弯来,总觉得回调环境是由线程池持有的,每个线程池都有自己的回调环境,其实这个是错误的,既然它叫做回调环境,自然与线程池无关,它是用来控制回调行为的。当我们在创建对应的任务时,最后一个参数就是回调环境的指针,在提交任务时会首先将任务提交到回调环境所规定的线程池中,由对应的线程池来处理。函数SetThreadpoolCallbackPool从表面意思来看是未线程池设置一个回调环境其实这个意思正好相反,是为某个回调指定对应调用的线程池。在后面就可以看到,回调环境可比线程池大的多线程池的清理组为了得体的销毁自定义的线程池(系统自定义线程池不会被销毁),我们需要知道系线程池中各个任务何时完成,只有当所有任务都完成时销毁线程池才算得体的销毁,只有这样才能顺利的清理相关资源。但是由于线程池中的各项任务可能由不同的线程提交,提交的时机,任务执行完所需要的时间各不相同,所以基本上不可能知道线程池中的任务何时完成。为了解决这个问题,新版的线程池提供了清理组的概念。TP_CALLBACK_ENVIRON结构的PTP_CLEANUP_GROUP就为对应的执行环境绑定了一个清理组。当线程池中的任务都处理完成时能够得体的清理线程池可以调用CreateThreadpoolCleanupGroup来创建一个清理组,然后调用SetThreadpoolCallbackCleanupGroup来将线程池与对应的清理组。它的原型如下:VOID SetThreadpoolCallbackCleanupGroup( __inout PTP_CALLBACK_ENVIRON pcbe, __in PTP_CLEANUP_GROUP ptpcg, __in_opt PTP_CLEANUP_GROUP_CANCEL_CALLBACK pfng );第一个参数是一个回调环境第二个参数是一个对应的清理组,这两个参数就将对应的回调环境和清理组关联起来第三个参数是一个回调函数,每当一个工作项被取消,这个函数将会被调用。对应的回调函数的原型如下:VOID NTAPI CleanupGroupCancelCallback(PVOID pvObjectContext, PVOID CleanupContext);每当创建一个任务时,如果最后一个参数不为NULL,那么对应的清理组中会增加一项,表示又增加一个需要潜在清理的任务。最后我们调用对应的清理工作项的函数时,相当于显示的将需要清理的项从对应的清理组中去除。当我们的应用程序想要销毁线程池时,调用函数CloseThreadpoolCleanupGroupMembers。这个函数相比于之前的WaitForThreadpoolTimerCallbacks来说,它可以等待线程池中的所有工作项,而不管工作项是哪种类型,而对应的wait函数只能等待对应类型的工作项。VOID WINAPI CloseThreadpoolCleanupGroupMembers( __inout PTP_CLEANUP_GROUP ptpcg, __in BOOL fCancelPendingCallbacks, __inout_opt PVOID pvCleanupContext );CloseThreadpoolCleanupGroupMembers函数的第二个参数也是一个BOOL类型,它的作用与对应的wait函数中第二个参数的作用相同。如果第二个参数设置为NULL,那么每当该函数取消一个工作项,对应的PTP_CLEANUP_GROUP_CANCEL_CALLBACK 类型的回调就要被调用一次CleanupGroupCancelCallback函数中第一个参数是被取消项的上下文,这个上下文是由对应的创建工作项的函数的pvContext参数传递进来的,而第二个参数是由CloseThreadpoolCleanupGroupMembers函数的第三个参数传递进来的。当所有的工作项被取消后调用CloseThreadpoolCleanupGroup来释放清理组所占的资源。最后调用DestroyThreadpoolEnviroment和CloseThreadPool这样就可以得体的关闭线程池下面是使用的一个例子:VOID NTAPI CleanupGroupCancelCallback(PVOID pvObjectContext, PVOID CleanupContext) { printf("有任务[%d][%d]被取消\n", *(int*)pvObjectContext, *(int*)CleanupContext); } VOID CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_TIMER Timer) { Sleep(1000); printf("有对应的定时器历程被调用\n"); } int _tmain(int argc, _TCHAR* argv[]) { TP_CALLBACK_ENVIRON environ = {0}; //创建回调环境 InitializeThreadpoolEnvironment(&environ); PTP_CLEANUP_GROUP pCleanUp = CreateThreadpoolCleanupGroup(); //创建清理组 PTP_POOL pool = CreateThreadpool(NULL); //创建自定义线程池 //设置线程池中的最大、最小线程数 SetThreadpoolThreadMinimum(pool, 2); SetThreadpoolThreadMaximum(pool, 8); //设置对应的回调环境和清理组 SetThreadpoolCallbackPool(&environ, pool); SetThreadpoolCallbackCleanupGroup(&environ, pCleanUp, CleanupGroupCancelCallback); //创建对应的工作项 int i = 1; PTP_TIMER pTimerWork = CreateThreadpoolTimer(TimerCallback, &i, &environ); ULARGE_INTEGER uDueTime = {0}; FILETIME ft = {0}; uDueTime.QuadPart = (LONGLONG) - 10 * 1000 *1000; //设置时间为10s ft.dwHighDateTime = uDueTime.HighPart; ft.dwLowDateTime = uDueTime.LowPart; SetThreadpoolTimer(pTimerWork, &ft, 10 * 1000, 0); //休眠1s保证定时器历程被提交 Sleep(1000); int j = 2; //等待所有历程执行完成,并清理资源 CloseThreadpoolCleanupGroupMembers(pCleanUp, TRUE, &j); CloseThreadpoolCleanupGroup(pCleanUp); DestroyThreadpoolEnvironment(&environ); CloseThreadpool(pool); return 0; }上面的例子中,首先定义了一个回调环境并进行初始化,然后定义自定义线程和对应的清理环境,并将他们绑定。并且在定义清理器时指定对应的回调函数。接着又定义了一个定时器线程并给一个上下文。然后提交这个定时器历程。为了保证能顺利提交,在主程序中等待1s。最后我们直接取消它,由于定时器触发的时间为10s这个时候肯定还没有执行,而根据之前说的,当我们取消一个已提交但是未执行的工作项时会调用对应的清理组规定的回调,这个时候CleanupGroupCancelCallback会被调用。它的参数的值分别由CreateThreadpoolTimer和CloseThreadpoolCleanupGroupMembers给出,所以最终输出结果如下:自定义线程池可以很方便的控制它的行为。但是为了要得体的清理它所以得加上一个清理组,最终当我们使用自定义线程池时,基本步骤如下:调用函数InitializeThreadpoolEnvironment初始化一个回调环境调用CreateThreadpoolCleanupGroup创建一个清理组,并根据需要给出对应的清理回调调用CreateThreadpool创建自定义线程池调用对应的函数,设置自定义线程池的相关属性调用函数SetThreadpoolCallbackPool将线程池与回调环境绑定调用函数SetThreadpoolCallbackCleanupGroup将回调环境与对应的清理组绑定调用对应的函数创建工作项,并提交调用函数CloseThreadpoolCleanupGroupMembers等待清理组中的所有工作项被执行完或者被取消调用CloseThreadpoolCleanupGroup关闭清理组并释放资源调用DestroyThreadpoolEnvironment清理回调环境调用CloseThreadpool函数关闭自定义的线程池使用清理组的方式清理工作项相比于调用对应的close函数清理工作项来说,显得更方便,一来自定义线程池中工作项的种类繁多,每个工作项都调用一个Close函数显得太复杂,而且当工作项过多时,不知道何时哪个工作项执行完,这个时候如果强行调用函数关闭工作项,显得有点暴力,所以用工作组的方式更为优雅一些
2017年08月14日
6 阅读
0 评论
0 点赞
2017-08-08
老版VC++线程池
在一般的设计中,当需要一个线程时,就创建一个,但是当线程过多时可能会影响系统的整体效率,这个性能的下降主要体现在:当线程过多时在线程间来回切换需要花费时间,而频繁的创建和销毁线程也需要花费额外的机器指令,同时在某些时候极少数线程可能就可以处理大量,比如http服务器可能只需要几个线程就可以处理用户发出的http请求,毕竟相对于用户需要长时间来阅读网页来说,CPU只是找到对应位置的页面返回即可。在这种情况下为每个用户连接创建一个线程长时间等待再次处理用户请求肯定是不划算的。为了解决这种问题,提出了线程池的概念,线程池中保存一定数量的 线程,当需要时,由线程池中的某一个线程来调用对应的处理函数。通过控制线程数量从而减少了CPU的线程切换,而且用完的线程还到线程池而不是销毁,下一次再用时直接从池中取,在某种程度上减少了线程创建与销毁的消耗,从而提高效率在Windows上,使用线程池十分简单,它将线程池做为一个整体,当需要使用池中的线程时,只需要定义对应的回调函数,然后调用API将回调函数进行提交,系统自带的线程池就会自动执行对应的回调函数。从而实现任务的执行,这种方式相对于传统的VC线程来说,程序员不再需要关注线程的创建与销毁,以及线程的调度问题,这些统一由系统完成,只需要将精力集中到逻辑处理的回调函数中来,这样将程序员从繁杂的线程控制中解放出来。同时Windows中线程池一般具有动态调整线程数量的自主行为,它会根据线程中执行任务的工作量来自动调整线程数,即不让大量线程处于闲置状态,也不会因为线程过少而有大量任务处于等待状态。在windows上主要有四种线程池普通线程池同步对象等待线程池定时器回调线程池完成端口回调线程池这些线程池最大的特点是需要提供一个由线程池中线程调用的回调函数,当条件满足时回调函数就会被线程池中的对应线程进行调用。从设计的角度来说,这样的设计大大简化了应用程序考虑多线程设计时的难度,此时只需要考虑回调函数中的处理逻辑和被调用的条件即可,而不必考虑线程的创建销毁等等问题(一些设计还可以绕开繁琐的同步处理)。需要注意的就是一般不要在这些回调函数中设计处理类似UI消息循环那样的循环,即不要长久占用线程池中的线程。下面来依次说明各种线程池的使用:普通线程池普通线程池在使用时主要是调用QueueUserWorkItem函数将回调函数加入线程池队列,线程池中一旦有空闲的线程就会调用这个回调,函数原型如下:BOOL WINAPI QueueUserWorkItem( __in LPTHREAD_START_ROUTINE Function, __in_opt PVOID Context, __in ULONG Flags );第一个参数是一个回调函数地址,函数原型与线程函数原型相同,所以在设计时可以考虑使用宏开关来指定这个回调函数作为线程函数还是作为线程池的回调函数第二个参数是传给回调函数的参数指针第三个参数是一个标志值,它的主要值及其含义如下:标志含义WT_EXECUTEDEFAULT线程池的默认标志WT_EXECUTEINIOTHREAD以IO可警告状态运行线程回调函数WT_EXECUTEINPERSISTENTTHREAD该线程将一直运行而不会终止WT_EXECUTELONGFUNCTION执行一个运行时间较长的任务(这会使系统考虑是否在线程池中创建新的线程)WT_TRANSFER_IMPERSONATION以当前的访问字串运行线程并调用回调函数下面是一个具体的例子:void CALLBACK ThreadProc(LPVOID lpParam); int _tmain(int argc, _TCHAR* argv[]) { int nWaitTime; while (TRUE) { printf("请输入线程等待事件:"); scanf_s("%d", &nWaitTime); printf("\n"); if (0 == nWaitTime) { break; } //将任务放入到队列中进行排队 QueueUserWorkItem((LPTHREAD_START_ROUTINE)ThreadProc, &nWaitTime, WT_EXECUTELONGFUNCTION); } //结束主线程 printf("主线程[%04x]\n", GetCurrentThreadId()); return 0; } void CALLBACK ThreadProc(LPVOID lpParam) { int nWaitTime = *(int*)lpParam; printf("线程[%04x]将等待%ds\n", GetCurrentThreadId(), nWaitTime); Sleep(nWaitTime * 1000); printf("线程[%04x]执行完毕\n", GetCurrentThreadId()); }这段代码上我们加入了WT_EXECUTELONGFUNCTION标识,其实在计算机中,只要达到毫秒级的,这个时候已经达到了系统进行线程切换的时间粒度,这个时候它就是一个需要长时间执行的任务定时器回调线程池定时器回调主要经过下面几步:调用CreateTimerQueue:创建定时器回调的队列调用CreateTimerQueueTimer创建一个指定时间周期的计时器对象,并指定对应的回调函数及参数之后当指定的时间片到达,就会将对应的回调历程放入到队列中,一旦线程池中有空闲的线程就执行它另外可以调用对应的函数对其进行相关的操作:可以调用ChangeTimerQueueTimer修改一个已有的计时器对象的计时周期调用DeleteTimerQueueTimer删除一个计时器对象调用DeleteTimerQueue删除这样一个线程池对象,在删除这个线程池的时候它上面绑定的回调也会被删除,所以在编码时可以直接删除线程池对象而不用调用DeleteTimerQueueTimer删除每一个绑定的计时器对象。但是为了编码的完整性,最好加上删除计时器对象的操作下面是一个使用的具体例子VOID CALLBACK TimerCallback(PVOID lpParameter, BOOLEAN TimerOrWaitFired); int _tmain(int argc, _TCHAR* argv[]) { HANDLE hTimeQueue = CreateTimerQueue(); HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); HANDLE hTimer; CreateTimerQueueTimer(&hTimer, hTimeQueue, (WAITORTIMERCALLBACK)TimerCallback, &hEvent, 10000, 0, WT_EXECUTEDEFAULT); //等待定时器历程被调用 WaitForSingleObject(hEvent, INFINITE); //关闭事件对象 CloseHandle(hEvent); //删除定时器与定时器线程池的绑定 DeleteTimerQueueTimer(hTimeQueue, hTimer, NULL); //删除定时器线程池 DeleteTimerQueue(hTimeQueue); return 0; } VOID CALLBACK TimerCallback(PVOID lpParameter, BOOLEAN TimerOrWaitFired) { HANDLE hEvent = *(HANDLE*)lpParameter; if (TimerOrWaitFired) { printf("定时器回调历程[%04x]被执行\n", GetCurrentThreadId()); } SetEvent(hEvent); }上述的代码中我们定义了一个同步事件对象,这个事件对象将在定时器历程中设置为有信号,这样方便我们在主线程中等待计时器历程执行完成同步对象等待线程池使用同步对象等待线程池只需要调用函数RegisterWaitForSingalObject,将一个同步对象绑定,当这个同步对象变为有信号或者等待的时间到达时,会调用对应的回调历程。该函数原型如下:BOOL WINAPI RegisterWaitForSingleObject( __out PHANDLE phNewWaitObject, __in HANDLE hObject, __in WAITORTIMERCALLBACK Callback, __in_opt PVOID Context, __in ULONG dwMilliseconds, __in ULONG dwFlags ); 第一个参数是一个输出参数,返回一个等待对象的句柄,我们可以将其看做这个线程池的句柄第二个参数是一个同步对象第三个参数是对应的回调函数第四个参数是传入到回调函数中的参数指针第五个参数是等待的时间第六个参数是一个标志与函数QueueUserWorkItem中的标识含义相同对应回调函数的原型如下:VOID CALLBACK WaitOrTimerCallback( __in PVOID lpParameter, __in BOOLEAN TimerOrWaitFired );当同步对象变为有信号或者等待的时间到达时都会调用这个回调,它的第二个参数就表示它所等待的对象是否为有信号。下面是一个使用的例子void WaitEventCallBackProc(PVOID lpParameter, BOOLEAN TimerOrWaitFired); int _tmain(int argc, _TCHAR* argv[]) { HANDLE hWait; HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); //注册等待同步对象的线程池 RegisterWaitForSingleObject(&hWait, hEvent, (WAITORTIMERCALLBACK)WaitEventCallBackProc, NULL, 5000, WT_EXECUTELONGFUNCTION); for(int i = 0; i < 5; i++) { SetEvent(hEvent); Sleep(5000); } UnregisterWaitEx(hWait, hEvent); CloseHandle(hEvent); CloseHandle(hWait); return 0; } void WaitEventCallBackProc(PVOID lpParameter, BOOLEAN TimerOrWaitFired) { if (TimerOrWaitFired) { printf("线程[%04x]等到事件对象\n"); }else { printf("线程[%04x]等待事件对象超时\n"); } }完成端口线程池在前面讲述文件操作的博文中,讲解了在文件中完成端口的使用,其实完成端口本质上就是一个线程池,或者说,windows上自带的线程池是使用完成端口的基础之上编写的。所以在这,完成端口线程池的使用将比IO完成端口来的简单通过调用BindIoCompletionCallback函数来将一个IO对象句柄与对应的完成历程绑定,这样在对应的IO操作完成后,对应的历程将会被丢到线程池中准备执行相比于前面的文件中的完成端口,这个完成端口线程池要简单许多,文件的完成端口需要自己创建完成多个线程,创建完成端口,并且将线程与完成端口绑定。另外还需要在线程中调用相应的等待函数等待IO操作完成,而线程池则不需要这些操作,我只需要准备一个完成历程,然后调用BindIoCompletionCallback,这样一旦历程被调用,就可以肯定IO操作一定完成了。这样我们只需要将主要精力集中在完成历程的编写中函数BindIoCompletionCallback的原型如下:BOOL WINAPI BindIoCompletionCallback( __in HANDLE FileHandle, __in LPOVERLAPPED_COMPLETION_ROUTINE Function, __in ULONG Flags );第一个参数是一个对应IO操作的句柄第二个参数是对应的完成历程函数指针第三个参数是一个标志,与之前的标识相同完成历程的函数原型如下:VOID CALLBACK FileIOCompletionRoutine( __in DWORD dwErrorCode, __in DWORD dwNumberOfBytesTransfered, __in LPOVERLAPPED lpOverlapped );第一个参数是一个错误码,当IO操作发生错误时可以通过这个参数获取当前错误原因第二个参数是当前IO操作操作的字节数第三个参数是一个OVERLAPPED结构这函数的使用与之前文件完成端口中完成历程一样下面我们将之前文件完成端口的例子进行改写,如下:typedef struct tagIOCP_OVERLAPPED { OVERLAPPED Overlapped; HANDLE hFile; //操作的文件句柄 DWORD dwDataLen; //当前操作数据的长度 LPVOID pData; //操作数据的指针 DWORD dwWrittenLen; //写入文件中的数据长度 }IOCP_OVERLAPPED, *LPIOCP_OVERLAPPED; #define MAX_WRITE_THREAD 20 //写线程总数 #define EVERY_THREAD_WRITTEN 100 //每个线程写入信息数 LARGE_INTEGER g_FilePointer; //全局的文件指针 void GetAppPath(LPTSTR lpAppPath) { TCHAR szExePath[MAX_PATH] = _T(""); GetModuleFileName(NULL, szExePath, MAX_PATH); size_t nPathLen = 0; StringCchLength(szExePath, MAX_PATH, &nPathLen); for (int i = nPathLen; i > 0; i--) { if (szExePath[i] == _T('\\')) { szExePath[i + 1] = _T('\0'); break; } } StringCchCopy(lpAppPath, MAX_PATH, szExePath); } VOID CALLBACK WriteThread(LPVOID lpParam); VOID CALLBACK FileIOCompletionRoutine(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped); int _tmain(int argc, _TCHAR* argv[]) { TCHAR szAppPath[MAX_PATH] = _T(""); GetAppPath(szAppPath); StringCchCat(szAppPath, MAX_PATH, _T("IocpLog.txt")); HANDLE hFile = CreateFile(szAppPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { return 0; } //绑定IO完成端口 BindIoCompletionCallback(hFile, (LPOVERLAPPED_COMPLETION_ROUTINE)FileIOCompletionRoutine, 0); //往日志文件中写入Unicode前缀 LPIOCP_OVERLAPPED pIocpOverlapped = (LPIOCP_OVERLAPPED)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(IOCP_OVERLAPPED)); pIocpOverlapped->dwDataLen = sizeof(WORD); pIocpOverlapped->hFile = hFile; WORD dwUnicode = MAKEWORD(0xff, 0xfe); //构造Unicode前缀 pIocpOverlapped->pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WORD)); CopyMemory(pIocpOverlapped->pData, &dwUnicode, sizeof(WORD)); //偏移文件指针 pIocpOverlapped->Overlapped.Offset = g_FilePointer.LowPart; pIocpOverlapped->Overlapped.OffsetHigh = g_FilePointer.HighPart; g_FilePointer.QuadPart += pIocpOverlapped->dwDataLen; //写文件 WriteFile(hFile, pIocpOverlapped->pData, pIocpOverlapped->dwDataLen, &pIocpOverlapped->dwWrittenLen, &pIocpOverlapped->Overlapped); //创建线程进行写日志操作 HANDLE hWrittenThreads[MAX_WRITE_THREAD]; for (int i = 0; i < MAX_WRITE_THREAD; i++) { hWrittenThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteThread, &hFile, 0, NULL); } //等待所有写线程执行完成 WaitForMultipleObjects(MAX_WRITE_THREAD, hWrittenThreads, TRUE, INFINITE); for (int i = 0; i < MAX_WRITE_THREAD; i++) { CloseHandle(hWrittenThreads[i]); } CloseHandle(hFile); return 0; } VOID CALLBACK FileIOCompletionRoutine(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped) { LPIOCP_OVERLAPPED pIOCPOverlapped = (LPIOCP_OVERLAPPED)lpOverlapped; //释放对应的内存空间 printf("线程[%04x]得到IO完成通知,写入长度%d\n", GetCurrentThreadId(), pIOCPOverlapped->dwDataLen); if (pIOCPOverlapped->pData != NULL) { HeapFree(GetProcessHeap(), 0, pIOCPOverlapped->pData); } if (NULL != pIOCPOverlapped) { HeapFree(GetProcessHeap(), 0, pIOCPOverlapped); pIOCPOverlapped = NULL; } } VOID CALLBACK WriteThread(LPVOID lpParam) { TCHAR szBuf[255] = _T("线程[%04x]模拟写入一条日志记录\r\n"); TCHAR szWrittenBuf[255] = _T(""); StringCchPrintf(szWrittenBuf, 255, szBuf, GetCurrentThreadId()); for (int i = 0; i < EVERY_THREAD_WRITTEN; i++) { LPIOCP_OVERLAPPED lpIocpOverlapped = (LPIOCP_OVERLAPPED)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(IOCP_OVERLAPPED)); size_t dwBufLen = 0; StringCchLength(szWrittenBuf, 255, &dwBufLen); lpIocpOverlapped->dwDataLen = dwBufLen * sizeof(TCHAR); lpIocpOverlapped->pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (dwBufLen + 1) * sizeof(TCHAR)); CopyMemory(lpIocpOverlapped->pData, szWrittenBuf, dwBufLen * sizeof(TCHAR)); lpIocpOverlapped->hFile = *(HANDLE*)lpParam; //同步文件指针 *((LONGLONG*)&(lpIocpOverlapped->Overlapped.Pointer)) = InterlockedCompareExchange64(&g_FilePointer.QuadPart, g_FilePointer.QuadPart + lpIocpOverlapped->dwDataLen, g_FilePointer.QuadPart); //写文件 WriteFile(lpIocpOverlapped->hFile, lpIocpOverlapped->pData, lpIocpOverlapped->dwDataLen, &lpIocpOverlapped->dwWrittenLen, &lpIocpOverlapped->Overlapped); } }
2017年08月08日
4 阅读
0 评论
0 点赞
2017-07-25
windows 纤程
纤程本质上也是线程,是多任务系统的一部分,纤程为一个线程准并行方式调用多个不同函数提供了一种可能,它本身可以作为一种轻量级的线程使用。它与线程在本质上没有区别,它也有上下文环境,纤程的上下文环境也是一组寄存器和调用堆栈。它是比线程更小的调度单位。注意一般我们认为线程是操作系统调用的最小单位,而纤程相比于线程来说更小,但是它是有程序员自己调用,而不由操作系统调用。系统在调度线程的时候会陷入到内核态,线程对象本身也是一种内核对象,而纤程完全是建立在用户层上,它不是内核对象也没有对象的句柄。通过纤程的机制实际就绕开了Windows的随机调度线程执行的行为,调度算法由应用程序自己实现,这对一些并行算法非常有意义。因为纤程和线程本质上的类同性,所以也要按照理解线程为函数调用器的方式来理解纤程。纤程的创建纤程的创建需要必须建立在线程的基础之上。在线程中调用函数ConvertThreadToFiber可以将一个线程转化为纤程(或者说将一个线程与纤程绑定,以后可以将该纤程看做主纤程)。其他的纤程函数必须在纤程中调用,也就是说,如果目前在线程中,需要调用ConverThreadToFiber将线程转化为纤程,才能调用对应的API。这个函数的原型如下:LPVOID WINAPI ConvertThreadToFiber( LPVOID lpParameter ); 这个函数传入一个参数,类似于CreateThread函数中的线程函数参数,如果我们在主纤程中需要使用到它,可以使用宏GetFiberData取得这个参数。在调用这个函数创建新纤程后,系统大概会给纤程分配200字节的栈空间,用来执行纤程函数,和保存纤程环境。这个环境由下面几个部分的内容组成:用户定义的值,这个值就是纤程回调函数中传入的参数新的结构化异常处理的链表头纤程内存栈的最高和最低地址,当线程转换为纤程的时候,这也是线程的内存栈。之前说过纤程栈是在建立在线程的基础之上,保留这两个值是为了当纤程还原为线程后,用来还原线程栈环境各种CPU寄存器环境,相当于线程的CONTENT,但是没有这个结构那么复杂,它只是保存了几个简单的寄存器的值。需要特别注意的一点是,它并没有保存对应浮点数寄存器FPU的值,所以在纤程中使用浮点数计算可能会出现未知错误。如果一定要计算浮点数,那么可以使用ConverThreadToFiberEx,在第二个参数的位置传入FIBER_FLAG_FLOAT_SWITCH值,表示将初始化并保存FPU。可以在主纤程中调用CreateFiber函数创建子纤程。该函数原型如下:LPVOID WINAPI CreateFiber( DWORD dwStackSize, LPFIBER_START_ROUTINE lpStartAddress, LPVOID lpParameter );第一个参数是纤程的堆栈大小,默认给0的话,它会根据实际需求创建对应大小的堆栈,纤程的堆栈是建立在线程的基础之上,我们可以这样理解,它是从线程的堆栈中隔离一块作为纤程的堆栈。本质上它的堆栈是放在线程的堆栈上。第二个参数是一个回调,与线程函数类似,这个函数是一个纤程函数。第三个参数是传递到回调函数中的参数。函数CreateFiber 和 ConvertThreadToFiber 函数都返回一个void* 的指针,用来唯一标识一个纤程,在这我们可以将它理解为纤程的HANDLE .纤程的删除当纤程结束时需要调用DeleteFiber来删除线程,类似于CloseHandle来结束对应的内核对象。如果是调用转化函数由线程转化而来,调用DeleteFiber相当于调用ExitThread来终止线程,所以对于这种情况,最好是将纤程转化为线程,然后再设计一套合理的线程退出机制。纤程的调度在任何一个纤程内部调用SwitchToFiber函数,将纤程的void*指针传入,即可切换到对应的纤程,该函数可以在任意几个纤程中进行切换,不管这些纤程是在一个线程中或者在不同的线程中。但是最好不要在不同线程中的纤程中进行切换,它可能会带来意想不到的情况,假设存在这样一种情况,线程A创建纤程FA,线程B创建纤程FB,当我们在系统运行线程A时将纤程从FA切换到FB,由于纤程的堆栈是建立在线程之上的,所以这个时候纤程B仍然使用线程A的堆栈,但是它应该使用的线程B的堆栈,这样可能会对线程A的堆栈造成一定的破坏。下面是纤使用的一个具体的例子:#define PRIMARY_FIBER 0 #define WRITE_FIBER 1 #define READ_FIBER 2 #define FIBER_COUNT 3 #define COPY_LENGTH 512 VOID CALLBACK ReadFiber(LPVOID lpParam); VOID CALLBACK WriteFiber(LPVOID lpParam); typedef struct _tagFIBER_STRUCT { DWORD dwFiberHandle; HANDLE hFile; LPVOID lpParam; }FIBER_STRUCT, *LPFIBER_STRUCT; char *g_lpBuffer = NULL; LPVOID g_lpFiber[FIBER_COUNT] = {}; void GetApp(LPTSTR lpPath, int nBufLen) { TCHAR szBuf[MAX_PATH] = _T(""); GetModuleFileName(NULL, szBuf, MAX_PATH); int nLen = _tcslen(szBuf); for(int i = nLen; i > 0; i--) { if(szBuf[i] == '\\') { szBuf[i + 1] = _T('\0'); break; } } nLen = _tcslen(szBuf) + 1; int nCopyLen = min(nLen, nBufLen); StringCchCopy(lpPath, nCopyLen, szBuf); } int _tmain(int argc, _TCHAR* argv[]) { g_lpBuffer = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, COPY_LENGTH); FIBER_STRUCT fs[FIBER_COUNT] = {0}; TCHAR szDestPath[MAX_PATH] = _T(""); TCHAR szSrcPath[MAX_PATH] = _T(""); GetApp(szDestPath, MAX_PATH); GetApp(szSrcPath, MAX_PATH); StringCchCat(szSrcPath, MAX_PATH, _T("2.jpg")); StringCchCat(szDestPath, MAX_PATH, _T("2_Cpy.jpg")); HANDLE hSrcFile = CreateFile(szSrcPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); HANDLE hDestFile = CreateFile(szDestPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL); fs[PRIMARY_FIBER].hFile = INVALID_HANDLE_VALUE; fs[PRIMARY_FIBER].lpParam = NULL; fs[PRIMARY_FIBER].dwFiberHandle = 0x00001234; fs[WRITE_FIBER].hFile = hDestFile; fs[WRITE_FIBER].lpParam = NULL; fs[WRITE_FIBER].dwFiberHandle = 0x12345678; fs[READ_FIBER].hFile = hSrcFile; fs[READ_FIBER].dwFiberHandle = 0x78563412; fs[READ_FIBER].lpParam = NULL; g_lpFiber[PRIMARY_FIBER] = ConvertThreadToFiber(&fs[PRIMARY_FIBER]); g_lpFiber[READ_FIBER] = CreateFiber(0, (LPFIBER_START_ROUTINE)ReadFiber, &fs[READ_FIBER]); g_lpFiber[WRITE_FIBER] = CreateFiber(0, (LPFIBER_START_ROUTINE)WriteFiber, &fs[WRITE_FIBER]); //切换到读纤程 SwitchToFiber(g_lpFiber[READ_FIBER]); //删除纤程 DeleteFiber(g_lpFiber[WRITE_FIBER]); DeleteFiber(g_lpFiber[READ_FIBER]); CloseHandle(fs[READ_FIBER].hFile); CloseHandle(fs[WRITE_FIBER].hFile); //变回线程 ConvertFiberToThread(); return 0; } VOID CALLBACK ReadFiber(LPVOID lpParam) { //拷贝文件 while (TRUE) { LPFIBER_STRUCT pFS = (LPFIBER_STRUCT)lpParam; printf("切换到[%08x]纤程\n", pFS->dwFiberHandle); DWORD dwReadLen = 0; ZeroMemory(g_lpBuffer, COPY_LENGTH); ReadFile(pFS->hFile, g_lpBuffer, COPY_LENGTH, &dwReadLen, NULL); SwitchToFiber(g_lpFiber[WRITE_FIBER]); if(dwReadLen < COPY_LENGTH) { break; } } SwitchToFiber(g_lpFiber[PRIMARY_FIBER]); } VOID CALLBACK WriteFiber(LPVOID lpParam) { while (TRUE) { LPFIBER_STRUCT pFS = (LPFIBER_STRUCT)lpParam; printf("切换到[%08x]纤程\n", pFS->dwFiberHandle); DWORD dwWriteLen = 0; WriteFile(pFS->hFile, g_lpBuffer, COPY_LENGTH, &dwWriteLen, NULL); SwitchToFiber(g_lpFiber[READ_FIBER]); if(dwWriteLen < COPY_LENGTH) { break; } } SwitchToFiber(g_lpFiber[PRIMARY_FIBER]); } 上面这段代码中首先将主线程转化为主纤程,然后创建两个纤程,分别用来读文件和写文件,然后保存这三个纤程。并定义了一个结构体用来向各个纤程函数传入对应的参数。在主线程的后面首先切换到读纤程,在读纤程中利用源文件的句柄,读入512字节的内容,然后切换到写纤程,将读到的这些内容写回到磁盘的新文件中完成拷贝,然后切换到读纤程,这样不停的在读纤程和写纤程中进行切换,直到文件拷贝完毕。再切换回主纤程,最后在主纤程中删除读写纤程,将主纤程转化为线程并结束线程。
2017年07月25日
5 阅读
0 评论
0 点赞
2017-07-22
windows 下进程池的操作
在Windows上创建进程是一件很容易的事,但是在管理上就不那么方便了,主要体现在下面几个方面:各个进程的地址空间是独立的,想要在进程间共享资源比较麻烦进程间可能相互依赖,在进程间需要进行同步时比较麻烦在服务器上可能会出现一个进程创建一大堆进程来共同为客户服务,这组进程在逻辑上应该属于同一组进程为了方便的管理同组的进程,Windows上提供了一个进程池来管理这样一组进程,在VC中将这个进程池叫做作业对象。它主要用来限制池中内存的一些属性,比如占用内存数,占用CPU周期,进程间的优先级,同时提供了一个同时关闭池中所有进程的方法。下面来说明它的主要用法作业对象的创建调用函数CreateJobObject,可以来创建作业对象,该函数有两个参数,第一个参数是一个安全属性,第二个参数是一个对象名称。作业对象本身也是一个内核对象,所以它的使用与常规的内核对象相同,比如可以通过命名实现跨进程访问,可以通过对应的Open函数打开命名作业对象。添加进程到作业对象可以通过AssignProcessToJobObject ,该函数只有两个参数,第一个是对应的作业对象,第二个是对应的进程句柄关闭作业对象中的进程可以使用TerminateJobObject 函数来一次关闭作业对象中的所有进程,它相当于对作业对象中的每一个进程调用TerminateProcess,相对来说是一个比较粗暴的方式,在实际中应该劲量避免使用,应该自己设计一种更好的退出方式控制作业对象中进程的相关属性可以使用SetInformationJobObject函数设置作业对象中进程的相关属性,函数原型如下:BOOL WINAPI SetInformationJobObject( __in HANDLE hJob, __in JOBOBJECTINFOCLASS JobObjectInfoClass, __in LPVOID lpJobObjectInfo, __in DWORD cbJobObjectInfoLength );第一个参数是一个作业对象的句柄,第二个是一系列的枚举值,用来限制其中进程的各种信息。第三个参数根据第二参数的不同,需要传入对应的结构体,第四个参数是对应结构体的长度。下面是各个枚举值以及它对应的结构体枚举值含义对应的结构体JobObjectAssociateCompletionPortInformation设置各种作业对象事件的完成端口JOBOBJECT_ASSOCIATE_COMPLETION_PORTJobObjectBasicLimitInformation设置作业对象的基本信息(如:进程作业集大小,进程亲缘性,进程CPU时间限制值,同时活动的进程数量等)JOBOBJECT_BASIC_LIMIT_INFORMATIONJobObjectBasicUIRestrictions对作业中的进程UI进行基本限制(如:指定桌面,限制调用ExitWindows函数,限制剪切板读写操作等)一般在服务程序上这个很少使用JOBOBJECT_BASIC_UI_RESTRICTIONSJobObjectEndOfJobTimeInformation指定当作业时间限制到达时,系统采取什么动作(如:通知与作业对象绑定的完成端口一个超时事件等)JOBOBJECT_END_OF_JOB_TIME_INFORMATIONJobObjectExtendedLimitInformation作业进程的扩展限制信息(限制进程的内存使用量等)JOBOBJECT_EXTENDED_LIMIT_INFORMATIONJobObjectSecurityLimitInformation限制作业对象进程中的安全属性(如:关闭一些组的特权,关闭某些特权等)要求作业对象所属进程或线程要具备更改这些作业进程安全属性的权限JOBOBJECT_SECURITY_LIMIT_INFORMATION限制进程异常退出的行为在Windows中,如果进程发生异常,那么它会寻找处理该异常的对应的异常处理模块,如果没有找到的话,它会弹出一个对话框,让用户选择,但是这样对服务程序来说很不友好,而且有的服务器是在远程没办法操作这个对话框,这个时候需要使用某种方法让其不弹出这个对话框。在作业对象中的进程,我们可以使用SetInformationJobObject函数中的JobObjectExtendedLimitInformation枚举值,将结构体JOBOBJECT_EXTENDED_LIMIT_INFORMATION中的BasicLimitInformation.LimitFlags成员设置为JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION。这相当于强制每个进程调用SetErrorMode并指定SEM_NOGPFAULTERRORBOX标志获取作业对象属性和统计信息调用QueryInformationJobObject函数来获取作业对象属性和统计信息。该函数的使用方法与之前的SetInformationJobObject函数相同。下面列举下它可选择枚举值:枚举值含义对应的结构体JobObjectBasicAccountingInformation基本统计信息JOBOBJECT_BASIC_ACCOUNTING_INFORMATIONJobObjectBasicAndIoAccountingInformation基本统计信息和IO统计信息JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATIONJobObjectBasicLimitInformation基本的限制信息JOBOBJECT_BASIC_LIMIT_INFORMATIONJobObjectBasicProcessIdList获取作业进程ID列表JOBOBJECT_BASIC_PROCESS_ID_LISTJobObjectBasicUIRestrictions查询进程UI的限制信息JOBOBJECT_BASIC_UI_RESTRICTIONSJobObjectExtendedLimitInformation查询作业进程的扩展限制信息JOBOBJECT_EXTENDED_LIMIT_INFORMATIONJobObjectSecurityLimitInformation查询作业对象进程中的安全属性JOBOBJECT_SECURITY_LIMIT_INFORMATION这些信息基本上与上面的设置限制信息是对应的。使用上也是类似的作业对象与完成端口设置作业对象的完成端口一般是使用SetInformationJobObject,并将第二个参数的枚举值指定为JobObjectAssociateCompletionPortInformation,这样就可以完成一个作业对象和完成端口的绑定。当作业对象发生某些事件的时候可以向完成端口发送对应的事件,这个时候在完成端口的线程中调用GetQueuedCompletionStatus可以获取对应的事件,但是这个函数的使用与之前在文件操作中的使用略有不同,主要体现在它的各个返回参数的含义上。各个参数函数如下:lpNumberOfBytes:返回一个事件的ID,它的事件如下:事件事件含义JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS进程异常退出JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT同时活动的进程数达到设置的上限JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO作业对象中没有活动的进程了JOB_OBJECT_MSG_END_OF_JOB_TIME作业对象的CPU周期耗尽JOB_OBJECT_MSG_END_OF_PROCESS_TIME进程的CPU周期耗尽JOB_OBJECT_MSG_EXIT_PROCESS进程正常退出JOB_OBJECT_MSG_JOB_MEMORY_LIMIT作业对象消耗内存达到上限JOB_OBJECT_MSG_NEW_PROCESS有新进程加入到作业对象中JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT进程消耗内存数达到上限lpCompletionKey: 返回触发这个事件的对象的句柄,我们将完成端口与作业对象绑定后,这个值自然是对应作业对象的句柄lpOverlapped: 指定各个事件对应的详细信息,在于进程相关的事件中,它返回一个进程ID既然知道了各个参数的含义,我们可以使用PostQueuedCompletionStatus函数在对应的位置填充相关的值,然后往完成端口上发送自定义事件。只需要将lpNumberOfBytes设置为我们自己的事件ID,然后在线程中处理即可下面是作业对象操作的完整例子#include "stdafx.h" #include <Windows.h> DWORD IOCPThread(PVOID lpParam); //完成端口线程 int GetAppPath(LPTSTR pAppName, size_t nBufferSize) { TCHAR szAppName[MAX_PATH] = _T(""); DWORD dwLen = ::GetModuleFileName(NULL, szAppName, MAX_PATH); if(dwLen == 0) { return 0; } for(int i = dwLen; i > 0; i--) { if(szAppName[i] == _T('\\')) { szAppName[i + 1] = _T('\0'); break; } } _tcscpy_s(pAppName, nBufferSize, szAppName); return 0; } int _tmain(int argc, _TCHAR* argv[]) { //获取当前进程的路径 TCHAR szModulePath[MAX_PATH] = _T(""); GetAppPath(szModulePath, MAX_PATH); //创建作业对象 HANDLE hJob = CreateJobObject(NULL, NULL); if(hJob == INVALID_HANDLE_VALUE) { return 0; } //创建完成端口 HANDLE hIocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 1); if(hIocp == INVALID_HANDLE_VALUE) { return 0; } //启动监视进程 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)IOCPThread, (PVOID)hIocp, 0, NULL); //将作业对象与完成端口绑定 JOBOBJECT_ASSOCIATE_COMPLETION_PORT jacp = {0}; jacp.CompletionKey = hJob; jacp.CompletionPort = hIocp; SetInformationJobObject(hJob, JobObjectAssociateCompletionPortInformation, &jacp, sizeof(jacp)); //为作业对象设置限制条件 JOBOBJECT_BASIC_LIMIT_INFORMATION jbli = {0}; jbli.PerProcessUserTimeLimit.QuadPart = 20 * 1000 * 10i64; //限制执行的用户时间为20ms jbli.MinimumWorkingSetSize = 4 * 1024; jbli.MaximumWorkingSetSize = 256 * 1024; //限制最大内存为256k jbli.LimitFlags = JOB_OBJECT_LIMIT_PROCESS_TIME | JOB_OBJECT_LIMIT_JOB_MEMORY; SetInformationJobObject(hJob, JobObjectBasicLimitInformation, &jbli, sizeof(jbli)); //指定不显示异常对话框 JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = {0}; jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION; SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &jeli, sizeof(jeli)); //创建新进程 _tcscat_s(szModulePath, MAX_PATH, _T("JobProcess.exe")); STARTUPINFO si = {0}; PROCESS_INFORMATION pi = {0}; CreateProcess(szModulePath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB, NULL, NULL, &si, &pi); //将进程加入到作业对象中 AssignProcessToJobObject(hJob, pi.hProcess); //运行进程 ResumeThread(pi.hThread); //查询作业对象的运行情况,在这查询基本统计信息和IO信息 JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION jbaai = {0}; DWORD dwRetLen = 0; QueryInformationJobObject(hJob, JobObjectBasicAndIoAccountingInformation, &jbaai, sizeof(jbaai), &dwRetLen); //等待进程退出 WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); //给完成端口线程发送退出命令 PostQueuedCompletionStatus(hIocp, 0, (ULONG_PTR)hJob, NULL); //等待线程退出 WaitForSingleObject(hIocp, INFINITE); CloseHandle(hIocp); CloseHandle(hJob); return 0; } DWORD IOCPThread(PVOID lpParam) { BOOL bLoop = TRUE; HANDLE hIocp = (HANDLE)lpParam; DWORD dwReasonId = 0; HANDLE hJob = NULL; OVERLAPPED *lpOverlapped = {0}; while (bLoop) { BOOL bSuccess = GetQueuedCompletionStatus(hIocp, &dwReasonId, (PULONG_PTR)&hJob, &lpOverlapped, INFINITE); if(!bSuccess) { return 0; } switch (dwReasonId) { case JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS: { //进程异常退出 DWORD dwProcessId = (DWORD)lpOverlapped; HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwProcessId); if(INVALID_HANDLE_VALUE != hProcess) { DWORD dwExit = 0; GetExitCodeProcess(hProcess, &dwExit); printf("进程[%08x]异常退出,退出码为[%04x]\n", dwProcessId, dwExit); } } break; case JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT: { printf("同时活动的进程数达到上限\n"); } break; case JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO: { printf("没有活动的进程了\n"); } break; case JOB_OBJECT_MSG_END_OF_JOB_TIME: { printf("作业对象CPU时间周期耗尽\n"); } break; case JOB_OBJECT_MSG_END_OF_PROCESS_TIME: { DWORD dwProcessID = (DWORD)lpOverlapped; printf("进程[%04x]CPU时间周期耗尽\n", dwProcessID); } break; case JOB_OBJECT_MSG_EXIT_PROCESS: { DWORD dwProcessId = (DWORD)lpOverlapped; HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwProcessId); if(INVALID_HANDLE_VALUE != hProcess) { DWORD dwExit = 0; GetExitCodeProcess(hProcess, &dwExit); printf("进程[%08x]正常退出,退出码为[%04x]\n", dwProcessId, dwExit); } } break; case JOB_OBJECT_MSG_JOB_MEMORY_LIMIT: { printf("作业对象消耗内存数量达到上限\n"); } break; case JOB_OBJECT_MSG_NEW_PROCESS: { DWORD dwProcessID = (DWORD)lpOverlapped; printf("进程[ID:%u]加入作业对象[h:0x%08X]\n",dwProcessID,hJob); } break; case JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT: { DWORD dwProcessID = (DWORD)lpOverlapped; printf("进程[%04x]消耗内存数量达到上限\n",dwProcessID); } break; default: bLoop = FALSE; break; } } }在上面的例子中需要注意一点,在创建进程的时候我们给这个进程一个CREATE_BREAKAWAY_FROM_JOB标志,由于Windows在创建进程时,默认会将这个子进程丢到父进程所在进程池中,如果父进程属于某一个进程池,那么我们再将子进程放到其他进程池中,自然会导致失败,这个标志表示,新创建的子进程不属于任何一个进程池,这样在后面的操作才会成功
2017年07月22日
5 阅读
0 评论
0 点赞
2017-06-11
windows 下文件的高级操作
本文主要说明在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函数:FindFirstFile:建立一个指定搜索条件的搜索句柄,函数原型如下:HANDLE FindFirstFile( LPCTSTR lpFileName, LPWIN32_FIND_DATA lpFindFileData ); 第一个参数是一个搜索起始位置路劲的字符串,但是这个字符串的格式为“路径+特定文件的通配符”这样它会以这个路径作为起始路径,依次查找到目录中文件名符合通配符的文件,比如"c:\."会返回c盘下的所有文件,而"c:\"直接返回错误,"c:\a*.txt"会返回c盘中以a开头的txt文件FindNextFile:搜索符合条件的下一项,在循环中调用它的话,它会依次返回符合FindFirstFile要求的文件信息和所有子目录新消息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); }这段代码很容易理解,但是需要注意几点:之前说过的分配的缓冲一定要大于FILE_NOTIFY_INFORMATION 结构这个函数也是用来监控目录的,所以这里要传入一个目录路径,不能传入文件路径在使用CreateFile来打开目录的时候这个函数要求传入的文件句柄必须要以FILE_LIST_DIRECTORY标识打开,否则在调用的时候会报“参数错误”这个错文件映射Windows中,文件映射是文件内容到进程的虚拟地址空间的映射,这个映射称之为File Mapping,文件内容的拷贝就是文件视图(File View),从内存管理的角度来看,文件映射只是将磁盘的真实地址通过页表映射到进程的虚拟地址空间中,读写这段虚拟地址空间其实就是在读写磁盘。而文件视图就是将文件中的内容整个读到内存中,并将这段虚拟地址空间与真实物理内存对应。最终在关闭整个文件映射的时候如果存在文件视图,操作系统会将视图中的内容写会到磁盘,其实也就是简单的进行了下物理内存到磁盘的页面交换,从内存管理的角度来看,文件映射其实就是操作系统将磁盘上的数据与物理内存之间的页面交换,操作系统在二者之间来回倒腾数据而已文件映射本身是一个内核对象,操作系统在内核中维护了一个相关的数据结构,这个结构中记录了被映射到虚拟地址空间中的起始地址和被映射的数据的大小。由于内核对象的数据结构是在内核中被维护,而内核被所有进程共享,所以从理论上将不同的进程是可以共享同一个内核对象的,虽然它们的对象句柄会在不同进程中呈现不同的值,但是在内核中,却是指向同一个结构,那么虽然不同进程的文件映射对象不同,但是通过寻址得到的物理内存肯定是同一个,所以这就提供了另一种进程间共享内存的方法——文件映射。创建文件映射主要使用函数CreateFileMapping,这个函数第一个参数是一个文件句柄,这个句柄可以是一个真实存在在磁盘上的文件,这样创建的文件映射最终就是将磁盘中的数据映射到进程的虚拟地址空间,也可以传入一个INVALID_HANDLE_VALUE,这个时候也会返回成功,传入INVALID_HANDLE_VALUE一般是用来在进程间共享内存的。注意:这个函数只是创建了一个内核对象并返回它的句柄,并没有进行内存映射的相关操作。同时由于它第一个句柄参数可以填INVALID_HANDLE_VALUE,在使用CreateFile函数后一定要注意校验,不然可能看到CreateFileMapping函数返回的是一个有效句柄,但是并没有成功创建这个文件的映射然后调用MappingViewOfFile函数,将对应文件与一段进程的虚拟地址空间关联并将文件映射到内存,也就是将磁盘文件中的数据交换到物理内存中当我们不使用这块真实内存的时候,调用UnMapViewOfFile将内存中的数据交换到磁盘,最终使用文件映射完毕后,调用CloseHandle关闭所有句柄使用文件映射一般有几个好处:针对文件来说,文件映射本质上是磁盘到物理内存之间的页面交换,由操作系统的内存管理机制统一调度,效率比一般的文件读写要高,而且在使用完毕后,操作系统会自动的将内存中的数据写到磁盘中,不用手动的更新文件针对不同进程来说,使用文件映射来共享内存本质上是在使用同样一块内存,相比于管道油槽等方式传输数据来说显得更为高效下面通过几个例子来说明在这两种情况下使用文件映射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做了如下工作:创建一个命名的文件映射对象构建文件映射的视口,并写入一段内存等待关闭相关句柄在进程B中做了如下工作:打开之前A创建的文件映射对象构建文件映射的视口,读取内存关闭相关句柄在使用文件映射共享内存时需要注意:使用命名对象的时候,对象前面必须要加上“Global//”表示该对象是一个全局的对象不同进程在使用文件映射共享内存时调用函数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是完成之后返回,它一次只会写入一条数据到磁盘,而且它也知道具体写入了多少数据,这时候系统帮助我们完成了文件指针的偏移,但是在进行异步操作的时候可能会同时有多条数据写入,并且系统不知道具体会成功写入多少数据,所以它不可能帮我们进行文件指针的偏移,这个时候就需要自己进行偏移操作完成函数使用完成函数主要需要如下步骤:调用CreateFile在dwFlagsAndAttributes参数中加上FILE_FLAG_OVERLAPPED标识表示我们需要使用异步的方式来进行磁盘操作准备一个完成函数,函数的原型为: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个字节的剩余空间可以使用,所以使用它来存储文件指针的偏移没有任何问题。调用ReadFileEx或者WriteFileEx函数(ReadFile WriteFile不支持完成函数的方式)并将完成函数作为最后一个参数传入调用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操作完成,就会启动完成端口中的线程,完成后续的操作。完成端口的使用一般经过下面几个步骤:调用CreateIoCompletionPort创建完成端口对象,并制定最大并发线程数(一般制定CPU核数或者核数的两倍)创建用于完成端口的线程,一般大于等于最大并发数调用函数CreateIoCompletionPort,将文件句柄与完成端口绑定在IO操作中传入一个OVERLAPPED结构在完成端口的线程中调用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; }
2017年06月11日
5 阅读
0 评论
0 点赞
2017-06-04
使用CJSON库实现XML与JSON格式的相互转化
之前完成了一个两个平台对接的项目。由于这两个平台一个是使用json格式的数据,一个是使用xml格式的数据,要实现它们二者的对接就涉及到这两个数据格式的转化,在查阅相关资料的时候发现了这个CJSON库,cjson是使用c编写的,它轻巧易用,在网上查了相关的资料后决定在json格式的存储于解析这块采用cjson库,而xml就简单的来解析字符串。cjson库中常用的几个函数简介cJSON_Parse该函数需要传入一个json格式的字符串,函数会将这个字符串转化为json格式保存起来,函数会返回一个表示json对象的指针,如果传入json格式字符串有误,函数会返回NULL,所以在之后如果要使用它生成的json对象的指针,一定要校验指针值cJSON_CreateObject创建一个json格式的对相关,用来保存之后的json格式数据cJSON_CreateArray创建一个json格式的数组cJSON_AddItemToObject将某个数据插入到对应的json对象中,函数需要三个参数,第一个参数是一个json对象,表示要往哪个json对象里面插入数据,第二个参数是一个字符串指针,表示该项的键值,第三个参数是一个json对象,表示要将何种对象插入到json对象中,这个函数一般是用来插入一个数组对象cJSON_AddNumberToObject对于插入数值,或者字符串值,如果调用cJSON_AddItemToObject,需要向将他们转化为json对象然后插入,为了方便库中提供了一个宏来方便插入数字值,它的参数与cJSON_AddItemToObject类似,只是最后一个参数是一个数字值cJSON_AddStringToObject将字符串插入json对象中,它的用法与cJSON_AddNumberToObject相同cJSON_Print将json对象转化为json格式的字符串cJson_Delete由于cjson对象是用malloc函数分配的内存,所以需要使用这个函数来释放分配的内存,否则会造成内存泄露。这个函数会释放对象中的所有内存单元,包括使用相关函数添加到对象中的子对象,所以在释放了对象的内存后,它的子对象的内存就不需要再次释放了cJosn结构体typedef struct cJSON { struct cJSON *next; struct cJSON *prev; struct cJSON *child; int type; char *valuestring; int valueint; double valuedouble; char *string; } cJSON;cjson中采用该结构体来存储json格式的数据,这个结构体存储的是json格式的单个项,其中为了能存储所有常用类型的数据,在里面定义了三种类型的成员,分别表示不同的数据类型值,string 成员表示的是该项的键值;它里面的三个指针分别表示同级别的下一项,上一项以及它的子节点,这些值在遍历这个json对象中的数据时需要用到具体的算法json格式转化为xml格式string CJson::Json2Xml(const string &strJson) { string strXml = ""; cJSON *pRoot = cJSON_Parse(strJson.c_str()); if (NULL == pRoot) { return ""; } cJSON *pChild = pRoot->child; while (pChild != NULL) { if (pChild->child != NULL) //存在子节点的情况 { std::string strSubKey = pChild->string; //获取它的键 std::string strSubValue = Json2Xml(cJSON_Print(pChild)); //获取它的值 std::string strSubXml = "<" + strSubKey + ">" + strSubValue + "</" + strSubKey + ">"; strXml += strSubXml; }else { std::string strKey = pChild->string; std::string strVal = ""; if (pChild->valuestring != NULL) { string strTemp = pChild->valuestring; strVal = "\"" + strTemp + "\""; }else { //其余情况作为整数处理 strVal = cJSON_Print(pChild); } strXml = strXml + "<" + strKey + ">" + strVal + "</" + strKey + ">"; } pChild = pChild->next; } if(NULL != pRoot) { cJson_Delete(pRoot); } return strXml; }上述代码首先将传进来的json格式的字符串转化为json对象,然后再遍历这个json对象。cjson在存储json格式的数据时,首先利用一个空的cJson结构体来保存整个json格式,类似于存在头指针的链表,它的child节点指针指向的是里面的第一个成员的信息,所以在遍历之前需要将指针偏移到它的child节点处。这个遍历的整体思想是:依次遍历它的同级节点,分别取出它的键和值key、value,并且将这一项组织成类似于<key>value</key>它的同级节点以相同的字符串结构添加到它的后面。如果某个成员中有子节点,那么递归调用这个函数,,并将返回的值作为value,在它的两侧加上key的标签。另外在遍历的时候需要注意的是它的值,其实这块可以使用cjson结构中的type来做更精准的判断,之前我在写这块的代码的时候没有仔细的查看库的源代码,所以简单的利用valuestring指针来判断,如果是字符串那么在字符串的两侧加上引号,否则什么都不加,在生成的xml中只需要判断值中是否有引号,有则表示它是一个字符串,否则是一个数字类型的值xml转json//暂时不考虑xml标签中存在属性值的问题 string CJson::Xml2Json(const string &strxml) { cJSON *pJsonRoot = cJSON_CreateObject(); string strNext = strxml; int nPos = 0; while ((nPos = strNext.find("<")) != -1) { string strKey = GetXmlKey(strNext); string strValue = GetXmlValueFromKey(strNext, strKey); string strCurrXml = strNext; strNext = GoToNextItem(strNext, strKey); int LabelPos = strValue.find("<"); // < 所在位置 int nMarkPos = strValue.find("\""); // " 所在位置 if (strValue != "" && LabelPos != -1 && LabelPos < nMarkPos) //引号出现在标签之后 { //里面还有标签 string strNextKey = GetXmlKey(strNext); //下一个的标签与这个相同,则为一个数组 if (strNextKey == strKey) { cJSON *pArrayObj = cJSON_CreateArray(); int nCnt = GetArrayItem(strCurrXml); for (int i = 0; i < nCnt; i++) { strKey = GetXmlKey(strCurrXml); strValue = GetXmlValueFromKey(strCurrXml, strKey); string strArrayItem = Xml2Json(strValue); cJSON *pArrayItem = cJSON_Parse(strArrayItem.c_str()); cJSON_AddItemToArray(pArrayObj, pArrayItem); strCurrXml = GoToNextItem(strCurrXml, strKey); } cJSON_AddItemToObject(pJsonRoot, strNextKey.c_str(), pArrayObj); strNext = strCurrXml; }else { //否则为普通对象 string strSubJson = Xml2Json(strValue); cJSON *pSubJsonItem = cJSON_CreateObject(); pSubJsonItem = cJSON_Parse(strSubJson.c_str()); cJSON_AddItemToObject(pJsonRoot, strKey.c_str(), pSubJsonItem); } } else { if (strValue.find("\"") == -1) //这个是数字 { cJSON_AddNumberToObject(pJsonRoot, strKey.c_str(), atof(strValue.c_str())); }else { remove_char(strValue, '\"'); cJSON_AddStringToObject(pJsonRoot, strKey.c_str(), strValue.c_str()); } } } string strJson = cJSON_Print(pJsonRoot); cJson_Delete(pJsonRoot); return strJson; }就像注释上说的,这段代码没有考虑xml中标签存在属性的问题,如果考虑上的话,我的想法是将属性作为该项的子项,给子项对应的键名做一个约定,以某个规律来命名,比如"标签名_contrib",这样在解析的时候一旦出现后面带有contrib的字符样式,就知道它是属性,后面就遍历这个子节点取出并以字符串的形式保存即可算法的思想跟之前的类似,在这我定义了几个函数用来从xml中取出每一项的键,值信息,然后将这些信息保存到json对象中,最后生成一个完整的json对象,调用print函数将对象转化为json格式的字符串。在while表示如果它的后面没有"<"表示后面就没有对应的值,这个时候就是xml格式的数据遍历完了,这个时候结循环中判断了下是否存在下一个标签,如果没有则结束循环,返回json格式字符串,函数返回。在循环中依次遍历它的每一个标签,在第一个if判断中出现这样的语句strValue != "" && LabelPos != -1 && LabelPos < nMarkPos,strValue表示的是标签中的值,LabelPos表示值中出现"<"的位置,而nMarkPos表示引号出现的位置,结合它们三个变量表示的含义,其实这句话表示如果值里面有"<"并且这个出现在引号之前,那么就说明是标签套标签,也就是存在子标签,这个时候需要递归调用函数,解析子标签的内存,如果这个"<"符号出现在引号之后,则表示它只是值中字符串的一部分,并没有子标签,这个时候就不需要进行递归。另外还判断了是否存在数组的情况,在json中数组是以一个类似于子对象的方式存储的,所在转化为xml时会将它作为一个子项存储,只是它的标签于父项的标签相同,所以判断数组的语句是当它存在子项时进行的,当得到它是一个数组时,会往后一直遍历,直到下一个标签不同于它,找到数组之后依次将这些值插入数组对象,并将整个数组对象插入到json对象中。当它只是一个普通的对象时会根据是否存在引号来判断它是否是字符串,然后调用不同的添加项的函数来插入数据最后将json对象转化为字符串,清空内存并返回函数(万别忘记清理内存)整个项目的下载地址:下载
2017年06月04日
5 阅读
0 评论
0 点赞
2017-06-01
使用MSHTML解析HTML页面
最近在写一个爬虫项目,本来打算用C/C++来实现,在网上查找有关资料的时候发现了微软的这个MSHTML库,最后发现在解析动态页面的时候它的表现实在是太差:在项目中需要像浏览器那样,执行JavaScript等脚本然后形成静态的HTML页面,最后才分析这个静态页面。但是MSHTML在执行JavaScript等脚本时需要配合WebBroswer这个ActiveX控件,这个控件又必须在GUI程序中使用,但是我做的这个功能最终是嵌入到公司产品中发布,不可能为它专门生成一个GUI页面,所以这个方案就作废了。虽然最终没有采用这个方案,但是我在开始学习MSHTML并写Demo的过程中还是收益匪浅,所以在这记录下我的成果解析Html页面MSHTML是一个典型的DOM类型的解析库,它基于COM组件,在解析Html页面时需要一个IHTMLDocument2类型的接口。在GUI程序中很容易就获取这个接口,获取它的方法很容易就可以在网上找到,在这主要说一下如何通过一段HTML字符串来生成对应的IHTMLDocument2接口。至于如何生成这个HTML字符串,我们可以通过向web服务器发送http请求,并获取它的返回,解析这个返回的数据包即可获取到对应的HTML页面数据。获取这个接口主要需要经过下面的几个步骤:使用CoCreateInstance创建一个接口,对于IHTMLDocument2接口一般是使用下面的语句:HRESULT hr = CoCreateInstance(CLSID_HTMLDocument, NULL, CLSCTX_INPROC_SERVER, IID_IHTMLDocument2, (void**)&m_spDoc);2.创建一个COM中的数组,将HTML字符串写到数组中。这个数组主要用来进行VC与VB的交互,以便VB程序能够很方便的使用COM接口。在使用这个数组时不需要关注它的具体成员,VC提供了具体的接口来使用它,在初始化它的时候只需要调用下面几个:a)SafeArrayCreateVector:这个函数用来创建一个对应的数组结构。函数有三个参数,第一个参数表示数组中元素类型,一般给VT_VARIANT表示它是一个自动类型,第二个参数数组元素起始位置的下标,对于VC来说,数组元素总是从0开始,所以这个位置一般给0,第三个参数是数组的维数,在这我们只是简单的将它作为一个字符数组,所以它是一个一维数组。b)SafeArrayAccessData:允许用户操作这个数组,在需要读写这个数组时都需要调用这个函数,以便获取这个数组的操作权。它有两个参数,第一个参数是数组变量,第二个参数是一个输出参数,当调用这个函数成功,会提供一个缓冲区,我们操作这个缓冲区就相当于操作了这个数组。c)SafeArrayUnaccessData:每当操作数组完成时需要调用这个函数,函数与SafeArrayAccessData配套使用,这个函数用来回收这个权限,并使我们对数组的操作生效调用接口的write方法,将接口与HTML字符串绑定经过这样几步就可以利用这个接口来访问HTML中的元素了,下面是它的详细代码:IHTMLDocument2* CreateIHTMLDocument2(const string &strHtml) { IHTMLDocument2 *m_spDoc = NULL; HRESULT hr = CoCreateInstance(CLSID_HTMLDocument, NULL, CLSCTX_INPROC_SERVER, IID_IHTMLDocument2, (void**)&m_spDoc); HRESULT hresult = S_OK; VARIANT *param; SAFEARRAY *sfArray; // Creates a new one-dimensional array sfArray = SafeArrayCreateVector(VT_VARIANT, 0, 1); if (sfArray == NULL || m_spDoc == NULL) { return; } hresult = SafeArrayAccessData(sfArray,(LPVOID*) ¶m); param->vt = VT_BSTR; param->bstrVal = _com_util::ConvertStringToBSTR(strHtml.c_str()); hresult = SafeArrayUnaccessData(sfArray); hresult = m_spDoc->write(sfArray); return m_spDoc; }HTML元素的遍历MSHTML中,将元素的对应信息封装为IHTMLElement接口,得到对应元素的接口后可以使用它里面的get系列方法来获取它里面的各种信息,这些函数我没有一一列举,当需要时看看MSDN即可。当获取到了HTML文档的IID_IHTMLDocument2接口时,可以使用下面的步骤进行元素的遍历:接口的get_all方法获取所有的标签节点。这个函数通过一个输出参数输出IHTMLElementCollection类型的接口指针然后通过IHTMLElementCollection接口的get_length方法获取标签的总数量,根据这个数量写一个循环,在循环进行元素的遍历在循环中使用IHTMLElementCollection接口的item方法进行迭代,依次获取各个元素对应的IDispatch接口指针调用IDispatch接口指针的QueryInterface方法生成对应的IHTMLElement接口。通过这个接口获取元素的各中信息。它对应的代码如下:void EnumElements(IHTMLDocument2* m_spDoc) { CComPtr<IHTMLElementCollection> pCollec; m_spDoc->get_all(&pCollec); if (NULL == pCollec) { return ; } VARIANT varName; long len = 0; pCollec->get_length(&len); for (int i = 0; i < len; i++) { varName.vt = VT_I4; varName.llVal = i; CComPtr<IHTMLElement> pElement; CComPtr<IDispatch> pDisp; pCollec->item(varName, varName, &pDisp); if (NULL == pDisp) { continue; } pDisp->QueryInterface(IID_IHTMLElement, (LPVOID*)&pElement); if (NULL != pElement) { BSTR bstrTag; pElement->get_tagName(&bstrTag); string strTag = _com_util::ConvertBSTRToString(bstrTag); cout<<strTag.c_str()<<endl; } } }这个方法不能很好的体现各个元素的层次结构,它可以遍历所有的元素,但是默认将元素都作为同一层来表示,如果需要得到对应的子节点,可以调用get_children方法,它可以获取下面的所有子节点,使用方法与get_all类似调用JavaScript方法在这,调用JavaScript函数只能想调用普通的函数一样,根据函数名,给它参数,并获取返回值,但是不能得到它执行到中间的某个步骤,比如说这样一个函数function add(a, b){ window.location.href = "https://www.baidu.com"; return a + b }调用这个函数,只能得到a + b的值,但是并不知道它会跳转到另一个页面,在编写爬虫时如果存在这样的跳转或者通过某条语句生成了一个链接,那么使用后面说的方法是获取不到的言归正传,下面来说下如何实现调用JavaScript。调用JavaScript方法一般是使用IDispatch接口中的Invoke方法,但是使用这个略显麻烦,我在网上找到了更简单的方法,就是使用CComDispatchDriver接口中的Invoke方法,这个接口中主要有Invoke0、Invoke1、Invoke2、InvokeN几个用于调用JavaScript函数的方法,分别表示传入0个参数、1个参数、2个参数、任意个参数。一般使用如下步骤来调用:1.调用IID_IHTMLDocument2的get_Script方法,获取CComDispatchDriver接口调用CComDispatchDriver接口的GetIDOfName,传入JavaScript函数名称,获取JS函数对应的元素接口,这个函数会通过一个输出参数输出一个DISPID类型的变量。这个主要是一个ID,用来唯一标识一个js函数调用CComDispatchDriver接口的invoke函数,传入对应的参数,并调用js函数。下面是一个例子代码:bool CallJScript(IID_IHTMLDocument2* m_spDoc, const CString strFunc, CComVariant* paramArray,int nArgCnt,CComVariant* pVarResult) { CComDispatchDriver spScript; GetJScript(spScript); if (NULL == spScript) { return false; } DISPID pispid; BSTR bstrText = _com_util::ConvertStringToBSTR(strFunc); spScript.GetIDOfName(bstrText, &pispid); HRESULT hr = spScript.InvokeN(pispid, paramArray, nArgCnt, pVarResult); if(FAILED(hr)) { ShowError(GetSystemErrorMessage(hr)); return false; } return true; }在调用的时候需要组织一个CComVariant类型的数组,并提供一个数组元素个数作为参数。而对于Invoke0这样有确定函数参数的情况则要简单的多。获取js函数返回值js返回参数最终会被包装成一个VARIANT结构,在COM中为了方便操作这个结构,封装了一个CComVariant类。在操作返回值时就是围绕着CComVariant类来进行返回确定值当它返回一个确定值时很好解决,由于事先知道返回值得类型,只需要调用结构体的不同成员即可CComVariant varResult; parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult); cout<<varResult.lVal<<endl;当它返回一个数组时,一般需要经过这样几步的处理:创建一个CComDispatchDriver,并将返回值得pdispVal赋值给它调用CComDispatchDriver接口的GetPropertyByName方法,将它的第一个参数传入"length"字符串,让其返回数组元素的个数在循环中调用GetPropertyByName方法,传入索引,获取对应索引位置的CComVariant值。CComVariant varResult; parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult); CComVariant varArrayLen; CComDispatchDriver spDisp = varResult.pdispVal; spDisp.GetPropertyByName(L"length", &varArrayLen); for (int i = 0; i < varArrayLen.intVal; i++) { CComVariant varValue; CStringW csIndex; csIndex.Format(L"%d", i); spDisp.GetPropertyByName(csIndex, &varValue); cout<<varValue.intVal<<endl; }返回一个object对象js的object对象中可以有不同的属性,不同的属性对应不同的值,类似于一个字典结构,当返回这个类型,并且我们知道这个对象中的相关属性名称的时候可以通过下面的方法来获取各个属性中的值:创建一个CComDispatchDriver,并将返回值得pdispVal赋值给它调用CComDispatchDriver接口的GetPropertyByName方法,将它的第一个参数传入对应属性名称的字符串,让其返回属性的值//在这假设JavaScript方法返回一个object对象,其中有两个属性,str属性中保存字符串,value属性保存一个整型数据 CComVariant varResult; parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult); CComVariant varValue; CComDispatchDriver spDisp = varResult.pdispVal; spDisp.GetPropertyByName(L"result", &varValue); cout<<"result:"<<varValue.intVal<<endl; spDisp.GetPropertyByName(L"str", &varValue); string strValue = _com_util::ConvertBSTRToString(varValue.bstrVal); cout<<"str:"<<strValue.c_str()<<endl;返回类型不确定的object对象上面这种情况只有当JavaScript代码由自己编写或者与他人进行过相关的约定的时候才可能非常清楚js函数中将会返回何种类型的值,但是大多数情况下,是不知道将会返回何种数据,比如像我们在编写爬虫的时候。这种情况下一般使用IDispatchEx接口来枚举返回对象中的属性名称然后再根据上面的方法来获取属性的值CComVariant varResult; parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult); CComQIPtr<IDispatchEx> pDispEx = varResult.pdispVal; CComDispatchDriver spDisp = varResult.pdispVal; DISPID dispid; HRESULT hr = pDispEx->GetNextDispID(fdexEnumAll, DISPID_STARTENUM, &dispid); //枚举返回对象中所有属性对应的值 while (hr == NOERROR) { BSTR bstrName; pDispEx->GetMemberName(dispid, &bstrName); if (NULL != bstrName) { DISPPARAMS params; CComVariant varVaule; cout<<_com_util::ConvertBSTRToString(bstrName)<<endl; spDisp.GetPropertyByName(bstrName, &varVaule); SysFreeString(bstrName); } hr = pDispEx->GetNextDispID(fdexEnumAll, dispid, &dispid); }这些差不多就是我当初学会的一些东西,当初在利用这个方案实现爬虫的时候还是有许多坑,也看到了它的许多局限性,以至于我最终放弃了它,采用其他的解决方案。目前在使用的时候的我发现这样几个问题:在调用js时,如果不知道函数的名称,目前为止没有方法可以调用,这样就需要我们在HTML中使用正则表达式等方法进行提取,但是在HTML中调用js的方法实在太多,而有的只有一个函数,并没有调用,这些情况给工作带来了很大的挑战MSHTML提供的功能主要是用来与IE进行交互,以便很容易实现一个类似于IE的浏览器或者与IE进行交互,但是如果要在控制台下进行相关功能的编写,则显的力不从心在控制台下它没有提供一个很好的方式来进行HTML页面的渲染。在于js进行交互的时候,只能简单的获取到一个VARIANT结构,这个结构可以表示所有常见的类型,但是在很多情况下,我们并不知道它具体代表哪个类型最后放上demo的下载地址:http://download.csdn.net/detail/lanuage/9857075
2017年06月01日
9 阅读
0 评论
0 点赞
2017-05-21
Windows资源
Windows资源是一种二进制数据,由链接器链接进程序成为程序的一部分,通过资源的方式可以很方便的对应用程序进行扩展。在Windows中资源可以是系统自定义的,也可以是用户自定义的。在VC++中资源是以被称为资源脚本的文本文件描述的(扩展名为rc),另外为了方便代码中调用资源,VC++环境中还会自动生成一个resource.h的头文件供C++代码使用,这个文件中主要定义了各个资源的ID,在vc++中使用ID来唯一标识一个资源,这个ID可以是数字也可以是字符串,其实在VC中真正用来标识资源的是字符串,通过宏MAKEINTRESOURCE可以将数字型的ID转化为对应的字符串,一般的资源函数在操作资源时都需要提供一个资源的字符串,而这个串就是利用这个宏传入ID生成的。在VC中资源脚本的基本格式为:资源名(ID串) 类型名 [语言] 资源数据资源数据可以是一段指定格式的文本或者一个文件,比如我们将wav作为资源加入到程序中,可以这样写:MY_WAVE_RES IDR_WAVE sample.wav.其中语言如果没有指定,那么默认为操作系统当前的语言环境。另外我们也可以将不同的资源放入不同的文本文件中,先定义好,然后在.rc文件中使用#include 来包含进来,比如在一个名为wav.resinclude文件中定义了一个WAV资源,然后可以在.rc文件中加上一句"#include <wav.resinclude> ”下面介绍下资源的操作中比较高级的技术引用自定义资源对于系统自定义资源,系统都提供了专门的函数来进行加载和操作,但是对于自定义资源,在操作时相对比较复杂,一般先使用FindResource和FindResourceEx在进程中找到对应的资源句柄,然后使用LoadResource将资源加载到内存中,以后就可以使用这个资源了。下面的一个例子演示了如何在当前exe中如何将另一个EXE作为资源加载,并执行它。__inline VOID GetAppPath(LPTSTR pszBuf) { DWORD dwLen = 0; if(0 == (dwLen = ::GetModuleFileName(NULL,pszBuf,MAX_PATH))) { printf("获取APP路径失败,错误码0x%08x\n",GetLastError()); return; } DWORD i = dwLen; for(; i > 0; i -- ) { if( '\\' == pszBuf[i] ) { pszBuf[i + 1] = '\0'; break; } } } int _tmain(int argc, _TCHAR* argv[]) { HMODULE hModule = GetModuleHandle(NULL); HRSRC hRsrc = FindResource(hModule, MAKEINTRESOURCE(IDR_RCDATA1), RT_RCDATA); if (INVALID_HANDLE_VALUE == hRsrc) { printf("加载自定义资源失败!\n"); return 0; } HGLOBAL hGlobalRes = LoadResource(hModule, hRsrc); LPVOID pResMem = LockResource(hGlobalRes); DWORD dwSize = SizeofResource(hModule, hRsrc); if (NULL == pResMem) { printf("获取资源所在内存失败!\n"); return 0; } TCHAR szFilePath[MAX_PATH] = _T(""); GetAppPath(szFilePath); StringCchCat(szFilePath, MAX_PATH, _T("test.exe")); HANDLE hFile = CreateFile(szFilePath, GENERIC_WRITE | GENERIC_READ, 0, NULL, CREATE_ALWAYS, 0, NULL); if(!WriteFile(hFile, pResMem, dwSize, &dwSize, NULL)) { printf("写文件失败\n"); return 0; } CloseHandle(hFile); STARTUPINFO si = {0}; PROCESS_INFORMATION pi = {0}; CreateProcess(szFilePath, NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0; }为了执行上面的代码需要在该项目工程中新加一个资源,将目标EXE添加到资源中,其中资源文件会多出一行"IDR_RCDATA1 RCDATA "E:\Program\ResourcesDemo\Debug\stdWindow.exe" 在resource.h文件中生成了一个资源对应的ID,做好这些工作,该程序就能正常运行在上面的代码中,依次调用FindResource、 LoadResource、LockResource,获取资源在进程空间中的地址,并将它对应的物理页面锁定在内存中,不允许其进行内存交换。然后将这个部分的内存写入到文件形成一个新的exe,最后执行这个exe,最终上面的程序编译运行后我们会发现在程序对应的目录下会生成一个test.exe文件。更新资源在有的时候需要对程序中的资源进行更新,这种情况下一般是在原始的工程下 更改资源,然后重新编译,但是这个时候用户需要下载新的更新程序,在原始程序比较大的情况下,为了更改一个简单的资源就要重新花大量的时间下载并更新程序,可能有点杀鸡用牛刀的意思,在我们只需要更新程序中的资源的情况下,Windows提供了一种方法。首先使用BeginUpdateResource建立可执行程序文件模块的更新句柄使用UpdateResource传入之前的更新句柄,更新资源数据使用EndUpdateResource函数关闭修改句柄,如果想让整个更改存盘需要将函数的第二个参数传入FALSE,这个参数的意思是是否放弃更新,传入false表示保存更新下面是一个简单的例子 HMODULE hModule = GetModuleHandle(NULL); //加载资源 HRSRC hRsrc = FindResource(hModule, MAKEINTRESOURCE(IDI_ICON1), RT_GROUP_ICON); if (hRsrc == NULL) { printf("加载资源失败\n"); return GetLastError(); } HGLOBAL hIcon = LoadResource(hModule, hRsrc); PVOID pIconBuf = LockResource(hIcon); int nIconSize = SizeofResource(hModule, hRsrc); //更新资源 HANDLE hUpdate = BeginUpdateResource(_T("E:\\Program\\ResourcesDemo\\Debug\\stdWindow.exe"), TRUE); BOOL bRet = UpdateResource(hUpdate, MAKEINTRESOURCE(RT_GROUP_ICON), MAKEINTRESOURCE(IDI_STDWINDOW), GetUserDefaultLangID(), pIconBuf, nIconSize); bRet = EndUpdateResource(hUpdate, FALSE); return 0;枚举资源枚举资源主要使用函数EnumResourceTypes EnumResourceNames, 和EnumResourceLanguages,这几个函数分别枚举资源类型,名称和语言,在msdn中查找函数的定义发现他们的调用顺序必须是type name language,下面是一个简单的枚举的例子:BOOL CALLBACK EnumResLangProc(HANDLE hModule, LPCTSTR lpszType, LPCTSTR lpszName, WORD wIDLanguage, LONG_PTR lParam) { printf("\tlanguage :%d\n", wIDLanguage); return TRUE; } BOOL CALLBACK EnumRe1sNameProc(HMODULE hModule, LPCTSTR lpszType, LPTSTR lpszName, LONG_PTR lParam) { if ((ULONG)lpszName & 0xffff0000) { printf("\t名称:%s\n", lpszName); }else { printf("\t名称:%d\n", (USHORT)lpszName); } return EnumResourceLanguages(hModule, lpszType, lpszName, (ENUMRESLANGPROCW)EnumResLangProc, NULL); } BOOL CALLBACK EnumResTypeProc(HMODULE hModule, LPTSTR lpszType,LONG_PTR lParam) { if ((ULONG)lpszType & 0xFFFF0000) { printf("类型:%s\n", lpszType); }else { printf("类型:%d\n", (USHORT)lpszType); } return EnumResourceNames(hModule, lpszType, (ENUMRESNAMEPROCW)EnumRe1sNameProc, NULL); } int _tmain(int argc, _TCHAR* argv[]) { HMODULE hExe = LoadLibrary(_T("E:\\Program\\ResourcesDemo\\Debug\\stdWindow.exe")); if (hExe == NULL) { printf("加载目标程序出错!\n"); return GetLastError(); } printf("目标程序中包含以下资源:\n"); EnumResourceTypes(hExe, EnumResTypeProc, NULL); return 0; }这段代码有以下几点需要注意:LoadLibrary不仅仅可以用来加载dll,实际上它可以加载任意的PE文件到内存,而GetModuleHandle是在内存中查找已经存在的一个模块的句柄,而我们这个地方这个exe事先并没有加载到内存,所以这里用GetModuleHandle是不能正确加载的,只有使用LoadLibrary这几个枚举函数都需要一个回调函数,这些函数指针类型可以在msdn中查找到,在VC环境下也定义了这些函数指针,但是不知道为什么在填入函数指针时需要强制转化,否则会报错资源可以使用字符串表示,也可以使用ID表示,这些回调函数虽说传入的都是枚举到的字符串指针,但是它仍然可能是ID,所以在这不能简单的直接把他们作为字符串使用,需要进行判断,判断的依据是它是否大于65536,因为我们说只有在ID值大于这个时,系统才会将ID作为字符串来使用
2017年05月21日
7 阅读
0 评论
0 点赞
2017-05-16
枚举进程中的模块
在Windows中枚举进程中的模块主要是其中加载的dll,在VC上主要有2种方式,一种是解析PE文件中导入表,从导入表中获取它将要静态加载的dll,一种是利用查询进程地址空间中的模块,根据模块的句柄来得到对应的dll,最后再补充一种利用Windows中的NATIVE API获取进程内核空间中的模块,下面根据给出这些方式的具体的代码片段:解析PE文件来获取其中的dll在之前介绍PE文件时说过PE文件中中存在一个导入表,表中记录了程序中加载的导入dll以及这些dll中函数的信息,这个结构的定义如下:typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; }; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR;我只需要获取这个结构并且根据RVA计算出它在文件中的偏移即可找到对应名称,利用之前PE解析器中的CPeFileInfo类来解析它即可,下面是具体的代码:void EnumModulesByPe(LPCTSTR pszPath) { CPeFileInfo peFile; peFile.strFilePath = pszPath; peFile.LoadFile(); if (!peFile.IsPeFile()) { printf("is not a pe file!\n"); return ; } peFile.InitDataDirectoryTable(); PIMAGE_IMPORT_DESCRIPTOR pImportTable = peFile.GetImportDescriptor(); while(!peFile.IsEndOfImportTable(pImportTable)) { printf("%s\n", peFile.RVA2fOffset(pImportTable->Name, (DWORD)peFile.pImageBase)); pImportTable++; } }利用之前的PE解析的类,首先给类中的文件路径赋值,然后加载到内存,并初始化它的数据目录表信息,从表中取出导入表的结构,根据结构中的Name字段的值来计算它的真实地址,即可解析出它里面的模块,这里我们只能解析出PE文件中自身保存的信息,如果dll是在程序运行之时调用LoadLibrary动态加载的,利用这个方法是找不到的。解析进程地址空间中的模块这个方法首先通过OpenProcess函数获取对应进程的句柄,然后调用EnumProcessModules枚举进程地址空间中当前存在的模块,这个函数会返回一个HMODULE句柄的数组,我们遍历这个数组,对其中的每个句柄调用GetModuleFileNameEx(很多模块GetModuleFileName获取不到,具体原因我没有深入研究)获取对应的文件路径。下面是具体的代码: HMODULE* phMods = NULL; HANDLE hProcess = NULL; DWORD dwNeeded = 0; DWORD i = 0; TCHAR szModName[MAX_PATH] = {}; hProcess = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwProcessId ); if (NULL == hProcess) { printf("不能打开进程[ID:0x%x]句柄,错误码:0x%08x\n",dwProcessId); return; } EnumProcessModules(hProcess, NULL, 0, &dwNeeded); phMods = (HMODULE*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwProcessId); if( EnumProcessModules(hProcess, phMods, dwNeeded, &dwNeeded)) { for ( i = 0; i < (dwNeeded / sizeof(HMODULE)); i++ ) { ZeroMemory(szModName,MAX_PATH*sizeof(TCHAR)); //在这如果使用GetModuleFileName,有的模块名称获取不到,函数返回无法找到该模块的错误 if ( GetModuleFileNameEx(hProcess, phMods[i], szModName,MAX_PATH)) { printf("%ws\n", szModName); } } } HeapFree(GetProcessHeap(), 0, phMods); CloseHandle( hProcess );由于静态加载的dll在进程启动之时就已经被加载到内存中,所以利用这个方法自然可以获取静态加载的dll,但是由于它是获取进程地址空间中加载的dll,所以要求进程要正在运行,毕竟进程如果没有运行,那么也就不存在地址空间,也就无法获取其中加载的dll,另外它只能获取当前进程地址空间中的dll,有的dll这个时候还没有被加载的话,它自然也获取不到。所以这个方法也不是能获取所有加载的dll使用进程快照获取模块这种方式是先使用 CreateToolhelp32Snapshot 函数来创建一个进程模块的快照,然后通过 Module32First 和 Module32Next 函数来遍历快照中的模块信息Module32First 和Module32Next 函数会通过参数返回一个 MODULEENTRY32结构的模块信息,该结构的定义如下:typedef struct tagMODULEENTRY32 { DWORD dwSize; DWORD th32ModuleID; DWORD th32ProcessID; DWORD GlblcntUsage; DWORD ProccntUsage; BYTE* modBaseAddr; DWORD modBaseSize; HMODULE hModule; TCHAR szModule[MAX_MODULE_NAME32 + 1]; TCHAR szExePath[MAX_PATH]; } MODULEENTRY32, *PMODULEENTRY32;dwSize 参数表示该结构体的大小,在调用Module32First 和Module32Next之前需要先设置这个成员的值th32ProcessID:对应进程的IDmodBaseAddr:模块的起始地址,也就是模块被加载到内存的基地址hModule: 模块的句柄szModule:模块名称szExePath: 模块对应文件所在的路径下面是一个使用该方式获取加载模块的例子BOOL GetModulesTH32(WORD wPID) { HANDLE hSnap; MODULEENTRY32 me; me.dwSize = sizeof(me); hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, wPID); if (hSnap == INVALID_HANDLE_VALUE) { FreeLibrary(hToolhelp); return FALSE; } keepGoing = Module32First(hSnap, &me); while (keepGoing) { printf("%d, %08x, %s, %s\r\n", me.modBaseAddr, me.modBaseSize, me.szModule, me.szExePath); keepGoing = Module32Next(hSnap, &me); } CloseHandle(hSnap); FreeLibrary(hToolhelp); return TRUE; }获取内核地址空间中的模块不管是解析PE文件还是调用EnumProcessModules都只能获取用户层地址空间中的模块,但是进程不光有用户空间,还有内核空间,所以在这再提供一种枚举内核地址空间的模块的方法。枚举内核地址空间主要使用函数ZwQuerySystemInformation(也可以使用NtQuerySystemInformation)在msdn中明确指出,这两个函数未来可能不在使用,不推荐使用,但是至少现在是仍然支持的,并且可以很好的完成任务。这两个函数主要在ntdll.dll中导出,两个函数的参数用法完全相同,只是一个是比较上层一个比较底层而已。在这主要说明一个,另一个完全一样:NTSTATUS WINAPI ZwQuerySystemInformation( __in SYSTEM_INFORMATION_CLASS SystemInformationClass, __inout PVOID SystemInformation, __in ULONG SystemInformationLength, __out_opt PULONG ReturnLength );函数的第一个参数是一个枚举类型,用来表示我们将要调用此函数来获取系统哪方面的信息,第二个参数是一个缓冲区,用来存储该函数输出的值,第三个参数是缓冲区的长度,第四个参数是实际需要缓冲区的长度,说到这应该很快就可以反应过来,我们可以第一次调用这个函数传入一个NULL缓冲,缓冲长度给0,让他返回具体的长度,然后根据这个长度,动态分配一块内存,再次调用传入正确的缓冲和长度,获取数据。在调用这个函数时需要注意下面几点:这个函数是未导出的,所以在微软的开发环境中是没有它的定义的,要使用它需要我们自己定义,定义的代码如下://这个NTSTATUS结构在应用层有定义,直接使用即可 typedef NTSTATUS(WINAPI *ZWQUERYSYSTEMINFORMATION)( __in SYSTEM_INFORMATION_CLASS SystemInformationClass, __inout PVOID SystemInformation, __in ULONG SystemInformationLength, __out_opt PULONG ReturnLength );这个函数使用的一些结构是在内核开发环境DDK中定义的,在应用层中可能没有它的定义,所以在这我们也需要对它们进行定义:#define NT_SUCCESS(status) ((NTSTATUS)(status)>=0) typedef enum _SYSTEM_INFORMATION_CLASS { SystemBasicInformation, SystemProcessorInformation, SystemPerformanceInformation, SystemTimeOfDayInformation, SystemPathInformation, SystemProcessInformation, SystemCallCountInformation, SystemDeviceInformation, SystemProcessorPerformanceInformation, SystemFlagsInformation, SystemCallTimeInformation, SystemModuleInformation, SystemLocksInformation, SystemStackTraceInformation, SystemPagedPoolInformation, SystemNonPagedPoolInformation, SystemHandleInformation, SystemObjectInformation, SystemPageFileInformation, SystemVdmInstemulInformation, SystemVdmBopInformation, SystemFileCacheInformation, SystemPoolTagInformation, SystemInterruptInformation, SystemDpcBehaviorInformation, SystemFullMemoryInformation, SystemLoadGdiDriverInformation, SystemUnloadGdiDriverInformation, SystemTimeAdjustmentInformation, SystemSummaryMemoryInformation, SystemNextEventIdInformation, SystemEventIdsInformation, SystemCrashDumpInformation, SystemExceptionInformation, SystemCrashDumpStateInformation, SystemKernelDebuggerInformation, SystemContextSwitchInformation, SystemRegistryQuotaInformation, SystemExtendServiceTableInformation, SystemPrioritySeperation, SystemPlugPlayBusInformation, SystemDockInformation, SystemProcessorSpeedInformation, SystemCurrentTimeZoneInformation, SystemLookasideInformation } SYSTEM_INFORMATION_CLASS, *PSYSTEM_INFORMATION_CLASS;缓冲区中存储的数据是一个表示返回数组中元素个数的DWORD类型的数据和一个对应结构体的数组,在MSDN上对这个缓冲进行解释时说这个缓冲区的头4个字节存储了对应数组的元素个数,而后面的存储的是对应结构的数组,所以在获取这个结构的数组时需要向后偏移4个字节。这个结构与我们传入的枚举值有关,比如我们在这获取的是进程内核空间中加载的模块信息,即传入的枚举值是SystemModuleInformation,它对应的结构应该是SYSTEM_MODULE_INFORMATION,它们之间的对应关系可以在MSDN中找到。这个结构也需要自己定义,它的定义如下:typedef struct _SYSTEM_MODULE_INFORMATION // Information Class 11 { ULONG Reserved[2]; PVOID pBase; ULONG Size; ULONG Flags; USHORT Index; USHORT Unknown; USHORT LoadCount; USHORT ModuleNameOffset; CHAR ImageName[256]; } SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;下面就是这个的代码:void EnumKernelModules() { HMODULE hNtDll = LoadLibrary(_T("ntdll.dll")); if (INVALID_HANDLE_VALUE == hNtDll) { printf("加载ntdll.dll失败\n"); return ; } ZWQUERYSYSTEMINFORMATION ZwQuerySystemInformation = (ZWQUERYSYSTEMINFORMATION)GetProcAddress(hNtDll, "ZwQuerySystemInformation"); if (NULL == ZwQuerySystemInformation) { printf("导出函数失败\n"); return; } PULONG pBuffInfo = NULL; DWORD dwSize = 0; ZwQuerySystemInformation(SystemModuleInformation, pBuffInfo, 0, &dwSize); pBuffInfo = (PULONG)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwSize); NTSTATUS status = ZwQuerySystemInformation(SystemModuleInformation, pBuffInfo, dwSize, &dwSize); if (!NT_SUCCESS(status)) { return; } //在这为了验证之前说的,通过这两句输出发现他们的结果相同 printf("%d\n", *pBuffInfo); printf("%d\n", dwSize / sizeof(SYSTEM_MODULE_INFORMATION)); PSYSTEM_MODULE_INFORMATION pModuleInfo = (PSYSTEM_MODULE_INFORMATION)((ULONG)pBuffInfo + 4); for (int i = 0; i < *pBuffInfo; i++) { printf("%s\n", pModuleInfo[i].ImageName); } }
2017年05月16日
8 阅读
0 评论
0 点赞
2017-05-08
PE解析器的编写(四)——数据目录表的解析
在PE结构中最重要的就是区块表和数据目录表,上节已经说明了如何解析区块表,下面就是数据目录表,在数据目录表中一般只关心导入表,导出表和资源这几个部分,但是资源实在是太复杂了,而且在一般的病毒木马中也不会存在资源,所以在这个工具中只是简单的解析了一下导出表和导出表。这节主要说明导入表,下节来说导出表。RVA到fRva的转化RVA转化为fRva主要是通过某个数据在内存中的相对偏移地址找到其在文件中的相对偏移地址,在对某个程序进行逆向时,如果找到关键的那个变量或者那句指令,我根据变量或者代码指令在内存中的RVA找到它在文件中的偏移,就可以找到它的位置,修改它可能就可以破解某个程序。废话不多说,直接上代码:DWORD CPeFileInfo::RVA2fOffset(DWORD dwRVA, DWORD dwImageBase) { InitSectionTable(); vector<IMAGE_SECTION_HEADER>::iterator it; for (it = m_SectionTable.begin(); it != m_SectionTable.end(); it++) { if (dwRVA >= (DWORD)(it->VirtualAddress) && dwRVA <= (DWORD)((DWORD)(it->VirtualAddress) + it->Misc.VirtualSize) ) { break; } } if (it == m_SectionTable.end()) { return -1; } return (DWORD)(dwRVA - (DWORD)(it->VirtualAddress) + (DWORD)(it->PointerToRawData) + dwImageBase); }系统在将PE文件加载到内存中时,PE中的区块是按页的方式对齐的,也就是说同一节中的内容在一页内存中的排列方式与在文件中的排列方式相同,所以这里利用这一关系就可以根据RVA推算出它在文件中的偏移,即:fRva - Roffset = Rva - Voffset ,其中fRva是某个成员在文件中的偏移,Roffset是区块在文件中的偏移,Voffset是该区块在内存中的偏移。上述代码就是利用这个原理来计算的,代码中存在一个循环,VirtualAddress 和 VirtualSize分别代表这个区块在内存中的起始地址和这个区块所占内存的大小,当这个RVA大于起始地址,小于起始地址 + 区块大小也就说明这个RVA是处在这个区块中,这样我们就找到RVA所在区块,用RVA - 区块起始地址就得到它在区块中的偏移,这个偏移加上区块在文件中的首地址,得到的就是RVA对应的在文件中的偏移,只要知道文件的起始地址就可以知道它在文件中的详细位置了。获取数据目录表的信息数据目录表的信息主要存储在PE头结构中的OptionHeader中,回顾一下它的定义:typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; // // NT additional fields. // DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;这个结构的最后一个结构是数据目录表的数组,而NumberOfRvaAndSizes表示数据目录表中元素的个数,一般都是8,而IMAGE_DATA_DIRECTORY 结构的定义如下:typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;第一个是指向某个具体的表结构的RVA,第二个是这个表结构的大小,在这个解析器中,主要显示这两项,同时为了方便在文件中查看,我们新加了一项,就是它在文件中的偏移在这个解析器的代码中,我们定义了一个结构来存储这些信息struct IMAGE_DATA_DIRECTORY_INFO { PVOID pVirtualAddress; PVOID pFileOffset; DWORD dwVirtualSize; };在类中定义了一个该结构的vector结构,同时定义一个InitDataDirectoryTable函数来初始化这个结构void CPeFileInfo::InitDataDirectoryTable() { if (!m_DataDirectoryTable.empty()) { //先清空之前的内容 m_DataDirectoryTable.clear(); } PIMAGE_SECTION_HEADER pSectionHeader = GetSectionHeader(); PIMAGE_OPTIONAL_HEADER pOptionalHeader = GetOptionalHeader(); PIMAGE_DATA_DIRECTORY pDataHeader = pOptionalHeader->DataDirectory; IMAGE_DATA_DIRECTORY_INFO dataInfo; for (int i = 0; i < IMAGE_NUMBEROF_DIRECTORY_ENTRIES; i++) { dataInfo.pVirtualAddress = (PVOID)(pDataHeader[i].VirtualAddress); dataInfo.dwVirtualSize = pDataHeader[i].Size; dataInfo.pFileOffset = (PVOID)RVA2fOffset((DWORD)(dataInfo.pVirtualAddress), 0); //这里调用这个函数计算它的偏移所以这里假定文件的起始地址为0 m_DataDirectoryTable.push_back(dataInfo); } }上述代码比较简单,获得了OptionHeader结构的指针后直接找到DataDirectory的地址,就可以得到数组的首地址,然后在循环中依次遍历这个数组就可以得到各项的内容,对于文件中的偏移直接调用之前写的那个转化函数即可导入表的解析导入的dll的信息的获取导入表在数据目录表的第1项,所以我们只需要区数据目录表数组中的第一个元素,从中就可以得到它的RVA,然后调用RVA到文件偏移的转化函数就可以在文件中找到它的位置,在代码中也是这样做的PIMAGE_IMPORT_DESCRIPTOR CPeFileInfo::GetImportDescriptor() { //由于这个表中保存的是RVA,要在文件中遍历,需要转为在文件中的偏移 PVOID pImportRVA = m_DataDirectoryTable[1].pVirtualAddress; //在读取这些数据的时候,是从内存中读取的,从内存中读取时,需要考虑文件被加载到内存中的基址 return (PIMAGE_IMPORT_DESCRIPTOR)RVA2fOffset((DWORD)pImportRVA, (DWORD)pImageBase); }导入表在Windows中的定义如下:typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 一般给0 DWORD OriginalFirstThunk; //指向first thunk,IMAGE_THUNK_DATA,该 thunk 拥有 Hint 和 Function name 的地址。这个IMAGE_THUNK_DATA在后面解析具体函数时会用到 }; DWORD TimeDateStamp; //忽略,很少使用它 DWORD ForwarderChain; //忽略,很少使用它 DWORD Name; //这个dll的名称 DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;这个结构是以一个数组的形式存储在对应的位置,所以说我们只要找到第一个结构的位置就可以找到剩余的位置,但是这个数组的个数事先并不知道,数组的最后一个元素都为0,所以在遍历到对应的都为0 的成员是就到了它的尾部,根据这个我们定义了一个函数,来判断它是否到达数组尾部,当不在尾部时将数组成员取出来,存储到事先定义的vector中。void CImportDlg::InitImportTable() { //获取导入函数表在文件中的偏移 PIMAGE_IMPORT_DESCRIPTOR pImportTable = m_pPeFileInfo->GetImportDescriptor(); if (NULL != pImportTable) { int i = 0; while (!IsEndOfTable(&pImportTable[i])) { m_ImportTable.push_back(pImportTable[i]); i++; } } } BOOL CImportDlg::IsEndOfTable(PIMAGE_IMPORT_DESCRIPTOR pImportTable) { //是否到达表的尾部,这个表中没有给出总共有多少项,需要自己判断 //判断条件是最后一项的所有内容都是null if (0 == pImportTable->OriginalFirstThunk && 0 == pImportTable->TimeDateStamp && 0 == pImportTable->ForwarderChain && 0 == pImportTable->Name && 0 == pImportTable->FirstThunk) { return TRUE; } return FALSE; }在显示时需要注意两点:name属性保存的是ASCII码形式的字符串,如果我们程序使用Unicode编码,需要进行对应的转化。. 要将时间戳转化为我们常见的时分秒的格式。下面是显示这些信息的部分代码: //根据Name成员中的RVA推算出其在文件中的偏移 char *pName = (char*)m_pPeFileInfo->RVA2fOffset(it->Name, (DWORD)(m_pPeFileInfo->pImageBase)); if (NULL == pName || -1 == (int)pName) { m_ImportList.InsertItem(i, _T("-")); }else { #ifdef UNICODE //如果是UNICODE字符串,那么需要进行转化 WCHAR wszName[256] = _T(""); MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, pName, strlen(pName), wszName, 256); m_ImportList.InsertItem(i, wszName); #else m_ImportList.InsertItem(i, pName); #endif } //显示时间戳 tm p; errno_t err1; err1 = gmtime_s(&p,(time_t*)&it->TimeDateStamp); TCHAR s[100] = {0}; _tcsftime (s, sizeof(s) / sizeof(TCHAR), _T("%Y-%m-%d %H:%M:%S"), &p); m_ImportList.SetItemText(i, 1, s);导入dll中的函数信息dll中函数的信息需要使用之前的FirstThunk来获取,其实OriginalFirstThunk与FirstThunk指向的是同一个结构,都是指向一个IMAGE_THUNK_DATA STRUC的结构,这个结构的定义如下:typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; DWORD Function; DWORD Ordinal; DWORD AddressOfData; } u1; } IMAGE_THUNK_DATA32;这个结构是一个公用体,它其实只占4个字节,当 它的最高位为 1时,表示函数以序号方式输入,这时候低 31位被看作一个函数序号。 当 它的最高位为 0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个 RVA,指向一个 IMAGE_IMPORT_BY_NAME 结构。 这个结构的定义如下:typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; BYTE Name[1]; } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;为什么要用两个指针指向同一个结构呢?这个跟dll的加载有关,由OriginalFirstThunk指向的结构是一个固定的值,不会被重写的值,一般它里面保存的是函数的名称,而由FirstThunk 保存的结构一般是由PE解析器进行重写,PE 装载器首先搜索 OriginalFirstThunk ,找到之后加载程序迭代搜索数组中的每个指针,找到每个 IMAGE_IMPORT_BY_NAME 结构所指向的输入函数的地址,然后加载器用函数真正入口地址来替代由 FirstThunk 数组中的一个入口,也就是说此时的FirstThunk 不再指向这个INAGE_IMPORT_BY_NAME结构,而是真实的函数的RVA。因此我们称为输入地址表(IAT)。所以在解析这个PE文件时一般使用OriginalFirstThunk这个成员来获取dll中的函数信息,因为需要获取函数名称。void CImportDlg::ShowFunctionInfoByDllIndex(int nIndex) { IMAGE_IMPORT_DESCRIPTOR ImageDesc = m_ImportTable[nIndex]; //获取到对应项所指向的IMAGE_THUNK_DATA的指针 PIMAGE_THUNK_DATA pThunkData = (PIMAGE_THUNK_DATA)m_pPeFileInfo->RVA2fOffset(ImageDesc.OriginalFirstThunk, (DWORD)m_pPeFileInfo->pImageBase); CString strInfo = _T(""); int i = 0; if (NULL == pThunkData || 0xffffffff == (int)pThunkData) { return; } //这个结构数组以0结尾 while (0 != pThunkData->u1.AddressOfData) { //当它的最高位为0时表示函数以字符串类型的函数名方式输入,此时才解析这个信息得到函数名 strInfo.Format(_T("%08x"), pThunkData); m_TunkList.InsertItem(i, strInfo); strInfo.Format(_T("%08x"), pThunkData->u1.AddressOfData); m_TunkList.SetItemText(i, 1, strInfo); if (0 == (pThunkData->u1.AddressOfData & 0x80000000)) { PIMAGE_IMPORT_BY_NAME pIibn= (PIMAGE_IMPORT_BY_NAME)m_pPeFileInfo->RVA2fOffset(pThunkData->u1.AddressOfData, (DWORD)m_pPeFileInfo->pImageBase); //name 这个域保存的是函数名的第一个字符,所以它的地址就是函数名字符串的地址 char *pszName = (char*)&(pIibn->Name); #ifdef UNICODE WCHAR wszName[256] = _T(""); MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, pszName, strlen(pszName), wszName, 256); m_TunkList.SetItemText(i, 2, wszName); #else m_TunkList.SetItemText(i, 3, pszName); #endif strInfo.Format(_T("%04x"), pIibn->Hint); m_TunkList.SetItemText(i, 3, strInfo); } else { m_TunkList.SetItemText(i, 2, _T("-")); m_TunkList.SetItemText(i, 3, _T("-")); } pThunkData++; } }上面的代码主要用来解析对应dll中的函数。在上面的代码中,根据用户点击鼠标的序号得到对应的dll项的结构信息,根据OriginalFirstThunk中保存的RVA找到结构IMAGE_THUNK_DATA对应的地址,得到地址后利用0x80000000这个值对它的最高位进行判断,如果为0,那么就可以获得函数名称,在获得名称时,也是需要注意函数名称在Unicode环境下需要转化。在这段代码中主要显示了函数的Thunk的rva,这个rva转化后对应的值,函数名,以及里面的Hint导出表的解析一般的exe文件不存在导出表,只有在dll中存在导出表。导出表中主要存储的是一个序号和对应的函数名,序数是指定DLL 中某个函数的16位数字,在所指向的DLL 文件中是独一无二的。 导出表在数据目录表的第0个元素。导出表的结构如下:typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; //属性信息 DWORD TimeDateStamp; //生成日期 WORD MajorVersion; //主版本号 WORD MinorVersion; //副版本号 DWORD Name; //dll的名称 DWORD Base; //导出函数序号的起始值 DWORD NumberOfFunctions; //文件中包含的导出函数的总数。 DWORD NumberOfNames; //文件中命名函数的总数,这个一般与上面的那个总数相同 DWORD AddressOfFunctions; //指向导出函数地址的RVA DWORD AddressOfNames; //指向导出函数名字的RVA DWORD AddressOfNameOrdinals; //指向导出函数序号的RVA } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;下面对这3个RVA进行详细说明:AddressOfFunctions,这个RVA指向的是一个双字数组,数组中的每一项是一个RVA 值,存储的是所有导出函数的入口地址,数组的元素个数等于NumberOfFunctionsAddressOfNames:这个RVA指向一个包含所有导出函数名称的表的指针,这个地址表是一个双字数组,数组中的每一项指向一个函数名称字符串的RVA。数组的项数等于NumberOfNames 字段的值AddressOfNameOrdinals:指向一个字型数组(注意这里不是双字)存储的是对应函数的地址,假如现在我们使用函数GetProcAddress在dll中导出一个函数A,它会根据这个函数名称在名称的表中查找,假设它找到的是函数名称表中的第x项与之相同,那么它会在AddressOfNameOrdinals表中查找第x项得到函数的序号,最后根据这个序号在AddressOfFunctions中找到对应的函数地址。void CExportInfoDlg::ShowFunctionInfo() { if (NULL == m_pPeFileInfo) { return; } PIMAGE_EXPORT_DIRECTORY pExportTable = m_pPeFileInfo->GetExportDeirectory(); PDWORD pAddressOfFunc = (PDWORD)m_pPeFileInfo->RVA2fOffset((DWORD)pExportTable->AddressOfFunctions, (DWORD)m_pPeFileInfo->pImageBase); PWORD pOriginals = (PWORD)m_pPeFileInfo->RVA2fOffset((DWORD)pExportTable->AddressOfNameOrdinals, (DWORD)m_pPeFileInfo->pImageBase); PDWORD pFuncName = (PDWORD)m_pPeFileInfo->RVA2fOffset((DWORD)pExportTable->AddressOfNames, (DWORD)m_pPeFileInfo->pImageBase); int nCount = pExportTable->NumberOfFunctions; CString strInfo = _T(""); if (pAddressOfFunc == NULL || (int)pAddressOfFunc == -1 || pOriginals == NULL || (int)pOriginals == -1 || pFuncName == NULL || (int)pFuncName == -1) { return; } for (int i = 0; i < nCount; i++) { //导出序号等于base + 在数组中的索引(pOriginals数组保存的值) if (pOriginals[i] > nCount) { //这个索引值无效 strInfo = _T("-"); }else { strInfo.Format(_T("%04x"), pOriginals[i] + pExportTable->Base); } m_FuncInfoList.InsertItem(i, strInfo); strInfo.Format(_T("%08x"), pAddressOfFunc[pOriginals[i]]); m_FuncInfoList.SetItemText(i, 2, strInfo); char *pszName = (char*)m_pPeFileInfo->RVA2fOffset(pFuncName[i], (DWORD)m_pPeFileInfo->pImageBase); #ifdef UNICODE WCHAR wszName[256] = _T(""); MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, pszName, strlen(pszName), wszName, 256); strInfo = wszName; #else strInfo = pszName; #endif m_FuncInfoList.SetItemText(i, 1, strInfo); } }上面的代码描述了这个过程。在代码中首先获取了导出函数表的数据,根据数据中的三个RVA获取它们在文件中的真实地址。首先在名称表中遍历所有函数名称,然后在对应的序号表中找到对应的序号,我在这个解析器中显示出的序号与Windows显示给外界的序号相同,但是在pe文件内部,在进行寻址时使用的是这个序号 - base的值,寻址时使用的是减去base后的值作为元素的位置。pAddressOfFunc[pOriginals[i]] 这句首先找到它在序号表中的序号值,然后根据这个序号在地址表中找到它的地址,在这得到的只是一个RVA地址,如果想得到具体的地址,还需要加上在内存或者文件的起始地址
2017年05月08日
7 阅读
0 评论
0 点赞
1
...
6
7
8
...
15