zoukankan      html  css  js  c++  java
  • 纵谈进程枚举

    当程序出现异常而失去响应,我们通常的做法是打开Windows任务管理器强行将其"杀死"。Windows任务管理器是个好东西,它能显示当前系统中执行的全部进程,以及它们的实时性能參数。可是作为程序猿,你知道这些功能是怎么实现的吗?

    "这有什么难的?。"你可能会说,"不就是调用那几个进程枚举函数嘛!

    "是啊。单纯实现Windows任务管理器相似的功能是不难。

    可是,你先别急,关于进程枚举。可能你仅仅知其一,不知其二。更何况。我们这里还有其三、其四。除此之外,我们这里还要增强功能。显示与各个进程相关联的模块(即DLL,动态链接库)信息。

    进程与DLL的基础知识
    大家知道,Windows 98/2000/XP都是多任务操作系统。所谓多任务,就是系统中能够同一时候执行多个进程。而所谓进程。就是应用程序的执行实例。通俗地讲,进程就是一个执行起来的.EXE程序。

    系统中的进程都用一个DWORD类型的数据来唯一标识。我们称之为PID。

    即使同一个应用程序执行多个实例。它们的PID也是不一样的。

    另外。进程拥有自己私有的虚拟地址空间,进程与进程之间不会相互干扰;每一个进程都至少包括一条线程。



    那么,DLL与进程又有什么关系呢?大家知道,自Windows诞生之日起,Windows操作系统就使用DLL来支持公共函数调用。DLL中实现的函数代码不出如今.EXE文件里,但能够被各个进程所使用。

    使用DLL的优点包括:
    1)    能够显著地减小每一个组件的大小(特别是对于一些大型软件系统)。
    2)    使升级更为简单。假设我们想要使用新版本号的函数,改变DLL中的函数后,仅仅需又一次编译DLL项目,然后再连接使用该函数的各个应用程序;而应用程序本身不须要又一次编译。
    3)    便于功能模块化,乃至开发任务的团队协作。

    一般来说。一个进程总是调用这个或那个DLL中的函数。进程与DLL是一种依赖关系。

    在我们的演示程序中,我们不仅要做进程枚举。我们还要来揭示进程与DLL的这样的依赖关系。

    演示程序的用户界面例如以下:

    图1 演示程序之用户界面 

    好了,言归正转,我们直奔主题。接下去。我们就来逐一介绍各种进程枚举方法。



    方法一:使用工具库(Tool Help Library)函数
    这是一种历史最悠久、也是最主要的方法(从Windows 95開始就支持这样的方法)。这些API函数中。最重要的当属CreateToolhelp32Snapshot。它的函数原型例如以下:
    HANDLE WINAPI CreateToolhelp32Snapshot(
      DWORD dwFlags,       
      DWORD th32ProcessID  
    );

    这个函数的功能就是给系统拍"快照"。拍照的对象由參数dwFlags决定,比方dwFlags值为TH32CS_SNAPPROCESS表示对象为系统中的全部进程,值为TH32CS_SNAPMODULE表示对象为由th32ProcessID參数指定的进程调用的全部模块(也就是DLL)。

    当调用CreateToolhelp32Snapshot函数给指定的对象拍完快照之后,我们就能够使用Process32First、Process32Next、Module32First、Module32Next等函数进行"取片"工作了。就是遍历刚才拍下来的全部进程、进程调用的全部模块。



    我们的演示程序提供了完整的代码实现:

    BOOL CToolHelpSpy::BuildProcessList(void)
    {
        // 给系统中全部进程拍快照
        HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 
        if (hProcessSnap == INVALID_HANDLE_VALUE) 
        {
            return FALSE; 
        }
        
        PROCESSENTRY32 pe32 = {0}; 
        pe32.dwSize = sizeof(PROCESSENTRY32); 
    
        // 遍历拍下来的全部进程 
        if (Process32First(hProcessSnap, &pe32)) 
        {
            do 
            { 
                if (pe32.th32ProcessID && strcmp(pe32.szExeFile, "System"))
                {
                    // 保存进程的名字、PID
                    CProcessItem  processItem;
                    processItem.SetProcessName(pe32.szExeFile);    
                    processItem.SetProcessId(pe32.th32ProcessID);
                    // 添加列表保存
                    mProcList.AddTail(processItem);
                }
            } while (Process32Next(hProcessSnap, &pe32)); 
        } 
        CloseHandle(hProcessSnap); 
    
        return TRUE;
    }
    
    BOOL CToolHelpSpy::BuildModuleList(CProcessItem& inProcess)
    {
        // 给指定的进程调用的全部模块拍快照
        HANDLE hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, 
            inProcess.GetProcessId());
        if (hModuleSnap == INVALID_HANDLE_VALUE) 
        {
            return FALSE; 
        }
    
        MODULEENTRY32 me32 = {0}; 
        me32.dwSize = sizeof(MODULEENTRY32);
        
        inProcess.CleanupModuleList();
        // 遍历全部模块
        if (Module32First(hModuleSnap, &me32)) 
        {
            do
            {
                // 保存模块文件全路径
                inProcess.AddModuleItem(me32.szExePath);
            } while (Module32Next(hModuleSnap, &me32));
        }
        CloseHandle(hModuleSnap); 
    
        return TRUE;
    }

    注:工具库函数在Kernel32.dll中实现。程序开发中。我们须要包括头文件Tlhelp32.h,连接库文件Kernel32.lib。

    注:我们这里使用自己定义类CProcessItem来描写叙述一个进程,它保存了进程的名字、PID等信息。另外还维持一个该进程调用的全部模块的列表。相应地。我们也使用一个自己定义类CModuleItem来描写叙述模块。它保存模块文件的全路径、版本号号、文件大小、说明信息、所属产品名等。(下同)

    方法二:使用PSAPI (Process Status API)函数
    这是一种Windows NT/2000下的方法。核心是使用EnumProcesses函数。

    它的原型例如以下:
    BOOL EnumProcesses(
      DWORD *lpidProcess,    // 用于保存全部进程的PID的数组
      DWORD cb,                     // 上述数组的大小
      DWORD *cbNeeded        // PID数组中实际返回的(有效)字节数
    );

    当获得系统中全部进程的PID后,我们就能够使用OpenProcess函数打开指定的进程,再调用GetModuleBaseName获得该进程的名字,调用EnumProcessModules枚举该进程调用的全部模块。调用GetModuleFileNameEx获得模块文件的全路径。



    我们的演示程序提供了完整的代码实现:

    BOOL CPSApiSpy::BuildProcessList(void)
    {
        // 枚举获得系统中的全部进程的PID
        DWORD  processes[1024], needed;
        if (!EnumProcesses(processes, sizeof(processes), &needed))
        {
            return FALSE;
        }
    
        char  szName[MAX_PATH]   = "";
        DWORD actualProcessCount = needed / sizeof(DWORD);
        for (DWORD i = 0; i < actualProcessCount; i++)
        {
            // 保存进程的PID
            CProcessItem  processItem;
            processItem.SetProcessId(processes[i]);
    
            // 打开当前进程以获得进程操作句柄
            HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
                FALSE, processes[i]);
            if (hProcess)
            {
                HMODULE hModule;
                DWORD   needed;
                // 枚举当前进程调用的全部模块
                if (EnumProcessModules(hProcess, &hModule, sizeof(hModule), &needed))
                {
                    // 获得并保存进程的名字
                    GetModuleBaseName(hProcess, hModule, szName, sizeof(szName));
                    processItem.SetProcessName(szName);    
                    mProcList.AddTail(processItem);
                }
                CloseHandle(hProcess);
            }
        }
        return TRUE;
    }
    
    BOOL CPSApiSpy::BuildModuleList(CProcessItem& inProcess)
    {
        // 依据PID打开该进程,获得一个进程操作句柄
        HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
            FALSE, inProcess.GetProcessId());
        if (hProcess)
        {
            HMODULE modules[1024];
            DWORD   needed;
            // 枚举当前进程调用的全部模块
            if (EnumProcessModules(hProcess, modules, sizeof(modules), &needed))
            {
                char szName[MAX_PATH] = "";
                inProcess.CleanupModuleList();
                DWORD actualModuleCount = needed / sizeof(DWORD);
                // 获得各个模块文件的全路径
                for (DWORD i = 1; i < actualModuleCount; i++)
                {
                    GetModuleFileNameEx(hProcess, modules[i], szName, sizeof(szName));
                    inProcess.AddModuleItem(szName);
                }
            }
            CloseHandle(hProcess);
        }
        
        return TRUE;
    }
    

    注:PSAPI函数在Psapi.dll中实现。程序开发中。我们须要包括头文件Psapi.h,连接库文件Psapi.lib。这些文件在安装了微软的Platform SDK后就可获得。

    方法三:利用系统收集的性能数据(Performance Data)
    这也是一种Windows NT/2000下的方法。首先,我们须要介绍一些关于性能监视(Performance Monitoring)的背景知识。



    所谓性能监视。实际上是Windows NT/2000提供的一种系统功能。它能实时採集、分析系统内的应用程序、服务、驱动程序等的性能数据。以此来分析系统的瓶颈、监视组件的表现。终于帮助用户进行系统的合理调配。这里。还要引入一个性能对象(Performance Object)的概念,即被监视者。一般系统中的性能对象包括处理器(Processor)、进程(Process)、线程(Thread)、网络通讯(如TCP、UDP、ICMP、IP等)、系统服务(如ACS/RSVP Service)等。

    (本文我们关心的是进程,即名为"Process"的对象。)以下。我们给出系统性能数据的结构參考图:

    图2 系统性能数据的结构 

    性能对象有两种:一种仅仅支持一个实例,还有一种支持多个实例。(我们关心的进程对象支持多个实例。而每一个实例相应系统中的一个进程。)一个对象能够有多个性能指标;每一个性能指标都用一个计数器(Counter)来记录。就进程对象而言,它拥有的计数器种类包括ID Process(进程的PID)、Thread Count(线程数)、Priority Base(进程优先级)、IO Read Bytes/sec(每秒IO读取字节数)、IO Writer Bytes/sec(每秒IO写出字节数)等。

    (本文我们仅仅关心ID Process计数器的值。)

    支持单一实例的对象数据结构例如以下(也就是图2中各个对象数据块的展开形式):

    图3 支持单一实例的对象数据结构

    支持多实例的对象数据结构例如以下(添加了各个实例的定义部分):

    图4 支持多实例的对象数据结构

    知道了性能数据结构。接下去我们怎么来读取它们呢?最主要的方法就是通过注冊表函数,如RegOpenKeyEx、RegQueryValueEx、RegCloseKey等。值得注意的是,这里尽管使用的是注冊表函数,但性能数据并不存储在注冊表数据库中。读取性能数据时调用函数RegOpenKeyEx。主键值应该是HKEY_PERFORMANCE_DATA。

    而当性能数据获得之后。依据各部分数据结构的定义,计算偏移量,我们就能获取我们感兴趣的数据了。



    我们的演示程序提供了完整的代码实现:

    #define INITIAL_SIZE        51200
    #define EXTEND_SIZE         25600
    #define REGKEY_PERF         _T("Software\Microsoft\Windows NT\Currentversion\Perflib")
    #define REGSUBKEY_COUNTERS  _T("Counters")
    #define PROCESS_COUNTER     _T("process")
    #define PROCESSID_COUNTER   _T("id process")
    
    BOOL CPerformanceSpy::BuildProcessList(void)
    {
        // 步骤一:从特定的注冊表路径下获取系统中全部的对象、计数器的名字
        LANGID lid = MAKELANGID(LANG_ENGLISH, SUBLANG_NEUTRAL);
        TCHAR  szSubKey[1024];
        _stprintf(szSubKey, _T("%s\%03x"), REGKEY_PERF, lid);
        HKEY  hSubKey;
        DWORD rt = RegOpenKeyEx(HKEY_LOCAL_MACHINE, szSubKey, 0, 
            KEY_READ, &hSubKey);
        if (rt != ERROR_SUCCESS)
        {
            return FALSE;
        }
    
        DWORD  dwType = 0;
        DWORD  dwSize = 0;
        LPBYTE buffer = NULL;
        BOOL   pass = FALSE;
        // 获得装载全部计数器名字的缓冲大小
        rt = RegQueryValueEx(hSubKey, REGSUBKEY_COUNTERS, NULL,
            &dwType, NULL, &dwSize);
        if (rt == ERROR_SUCCESS)
        {
            buffer = (LPBYTE) malloc(dwSize);
            memset(buffer, 0, dwSize);
            rt = RegQueryValueEx(hSubKey, REGSUBKEY_COUNTERS, NULL,
                &dwType, buffer, &dwSize);
        }
    
        LPSTR  p, p2;
        DWORD  dwProcessIdTitle; 
        DWORD  dwProcessIdCounter; 
        PPERF_DATA_BLOCK             pPerf;
        PPERF_OBJECT_TYPE            pObj;
        PPERF_INSTANCE_DEFINITION    pInst;
        PPERF_COUNTER_BLOCK          pCounter;
        PPERF_COUNTER_DEFINITION     pCounterDef;
        if (rt == ERROR_SUCCESS)
        {
            pass = TRUE;
            // 步骤二:查找名为"Process"的对象以及名为"ID Process"的计数器
            // 获取它们的索引值(由于对象、计数器在性能数据中是靠索引来标识的)
            p = (LPSTR) buffer;
            while (*p) 
            {
                if (p > (LPSTR) buffer) 
                {
                    for (p2 = p - 2; _istdigit(*p2); p2--)
                        ;
                }
    
                if (_tcsicmp(p, PROCESS_COUNTER) == 0)
                {
                    // 获取"Process"对象的索引
                    for (p2 = p - 2; _istdigit(*p2); p2--) 
                        ;
                    _tcscpy(szSubKey, p2+1);
                } 
                else if (stricmp(p, PROCESSID_COUNTER) == 0) 
                {
                    // 获取"ID Process"计数器的索引
                    for (p2 = p - 2; _istdigit(*p2); p2--) 
                        ; 
                    dwProcessIdTitle = atol(p2 + 1);
                }
                // Point to the next string
                p += (_tcslen(p) + 1);
            }
    
            // 步骤三:获取进程对象的全部性能数据
            free(buffer);
            buffer = NULL;
            dwSize = INITIAL_SIZE;
            buffer = (LPBYTE) malloc(dwSize);
            memset(buffer, 0, dwSize);
            while (pass)
            {
                rt = RegQueryValueEx(HKEY_PERFORMANCE_DATA, szSubKey, NULL,
                    &dwType, buffer, &dwSize);
                pPerf = (PPERF_DATA_BLOCK) buffer;
                // 性能数据块开头以四个字符"PERF"标识
                if ((rt == ERROR_SUCCESS) && (dwSize > 0) &&
                    pPerf->Signature[0] == (WCHAR)'P' &&
                    pPerf->Signature[1] == (WCHAR)'E' &&
                    pPerf->Signature[2] == (WCHAR)'R' &&
                    pPerf->Signature[3] == (WCHAR)'F')
                {
                    break;
                }
    
                // 假设缓冲不够大,扩大缓冲后再试
                if (rt == ERROR_MORE_DATA)
                {
                    dwSize += EXTEND_SIZE;
                    buffer  = (LPBYTE) realloc(buffer, dwSize );
                    memset(buffer, 0, dwSize );
                }
                else
                {
                    pass = FALSE;
                }
            }
        }
    
        if (pass)
        {
            pObj = (PPERF_OBJECT_TYPE) ((DWORD)pPerf + pPerf->HeaderLength);
            // 步骤四:在进程对象数据的计数器定义部分寻找"ID Process"计数器 
            pCounterDef = (PPERF_COUNTER_DEFINITION) ((DWORD)pObj + pObj->HeaderLength); 
            for (DWORD i = 0; i < (DWORD)pObj->NumCounters; i++) 
            { 
                if (pCounterDef->CounterNameTitleIndex == dwProcessIdTitle) 
                { 
                    dwProcessIdCounter = pCounterDef->CounterOffset; 
                    break; 
                } 
                pCounterDef++; 
            } 
            
            // 步骤五:遍历全部实例,获取实例的名字(即进程名)以及PID
            TCHAR  szProcessName[MAX_PATH];
            pInst = (PPERF_INSTANCE_DEFINITION) ((DWORD)pObj + pObj->DefinitionLength);
            for (i = 0; i < (DWORD)pObj->NumInstances; i++)
            {
                // 获取进程名
                p  = (LPSTR) ((DWORD)pInst + pInst->NameOffset);
                rt = WideCharToMultiByte(CP_ACP, 0, (LPCWSTR)p, -1, szProcessName,
                    sizeof(szProcessName), NULL, NULL);
                // 获取进程PID
                pCounter = (PPERF_COUNTER_BLOCK) ((DWORD)pInst + pInst->ByteLength);
                DWORD processId = *((LPDWORD) ((DWORD)pCounter + dwProcessIdCounter));
                if (strcmp(szProcessName, "System") && processId)
                {
                    CProcessItem  processItem;
                    processItem.SetProcessId(processId);
                    processItem.SetProcessName(szProcessName);    
                    mProcList.AddTail(processItem);
                }
                // Point to the next process 
                pInst = (PPERF_INSTANCE_DEFINITION) ((DWORD)pCounter + pCounter->ByteLength);
            }
        }
    
        if (buffer) 
        {
            free(buffer);
            buffer = NULL;
        }
        RegCloseKey(hSubKey);
        RegCloseKey(HKEY_PERFORMANCE_DATA);
        return pass;
    }

    注:方法三用到的仅仅是注冊表操作函数,而这些函数在advapi32.dll中实现。程序开发中。我们须要包括头文件winperf.h。另外,该方法中各个进程所调用的模块。仍然用法二的PSAPI函数获得。这里就不再列出。

    方法四:使用PDH (Performance Data Helper)函数
    这样的方法的底层实现跟方法三事实上是一样的。

    但我们看到,方法三实现起来非常繁琐。

    为了简化应用,PDH函数对方法三的实现进行了一层封装。我们这里的进程枚举,主要使用PdhEnumObjectItems函数,它的函数原型例如以下:
    PDH_STATUS PdhEnumObjectItems(
      LPCTSTR szDataSource,                      // 数据源
      LPCTSTR szMachineName,                 // 机器名
      LPCTSTR szObjectName,                    // 对象名
      LPTSTR mszCounterList,                    // 计数器列表
      LPDWORD pcchCounterListLength,   // 计数器列表长度
      LPTSTR mszInstanceList,                    // 实例列表
      LPDWORD pcchInstanceListLength,   // 实例列表长度
      DWORD dwDetailLevel,                      // 获取信息的级别
      DWORD dwFlags                                 // 保留为0
    );

    对于每一个获得的进程实例,我们还要得到它的PID。也就是得到"ID Process"计数器的值。这时,我们会用到其它的PDH函数。包括:PdhOpenQuery、PdhAddCounter、PdhCollectQueryData、PdhGetFormattedCounterValue、PdhCloseQuery等。

    我们的演示程序提供了完整的代码实现:

    BOOL CPDHSpy::BuildProcessList(void)
    {
        LPTSTR      szCounterListBuffer     = NULL;
        DWORD       dwCounterListSize       = 0;
        LPTSTR      szInstanceListBuffer    = NULL;
        DWORD       dwInstanceListSize      = 0;
        
        BOOL pass = FALSE;
        // 第一次调用PdhEnumObjectItems以获取须要的列表长度 
        PDH_STATUS pdhStatus = PdhEnumObjectItems(NULL, NULL, TEXT("Process"),
            szCounterListBuffer, &dwCounterListSize, szInstanceListBuffer,
            &dwInstanceListSize, PERF_DETAIL_WIZARD, 0); 
        if (pdhStatus == ERROR_SUCCESS) 
        {
            szCounterListBuffer  = (LPTSTR) malloc((dwCounterListSize * sizeof (TCHAR)));
            szInstanceListBuffer = (LPTSTR) malloc((dwInstanceListSize * sizeof (TCHAR)));
            // 第二次调用PdhEnumObjectItems
    	// 获得"Process"对象的全部计数器和实例
            pdhStatus = PdhEnumObjectItems(NULL, NULL, TEXT("Process"),
                szCounterListBuffer, &dwCounterListSize, szInstanceListBuffer,
                &dwInstanceListSize, PERF_DETAIL_WIZARD, 0);     
            if (pdhStatus == ERROR_SUCCESS) 
            {
                pass = TRUE;
                LPTSTR  pInst = szInstanceListBuffer;
                // 获得每一个实例名,也就是进程名
                for (; *pInst != 0;    pInst += lstrlen(pInst) + 1) 
                {
                    if (strcmp(pInst, "System") && strcmp(pInst, "Idle") &&
                        strcmp(pInst, "_Total"))
                    {
                        CProcessItem  processItem;
                        // 获得进程的PID
                        processItem.SetProcessId(GetPIDCounterValue(pInst));
                        processItem.SetProcessName(pInst);    
                        mProcList.AddTail(processItem);
                    }
                }
            }
        }
    
        if (szCounterListBuffer != NULL) 
        {
            free(szCounterListBuffer);
            szCounterListBuffer = NULL;
        }
        if (szInstanceListBuffer != NULL) 
        {
            free(szInstanceListBuffer);
            szInstanceListBuffer = NULL;
        } 
        return pass;
    }
    
    DWORD CPDHSpy::GetPIDCounterValue(LPTSTR inInstanceName)
    {
        // 打开一个查询对象
        HQUERY   hQuery   = NULL;
        PDH_STATUS pdhStatus = PdhOpenQuery (0, 0, &hQuery);
    
        HCOUNTER hCounter = NULL;
        char szPathBuffer[MAX_PATH];
        sprintf(szPathBuffer, "\Process(%s)\ID Process", inInstanceName);
        pdhStatus = PdhAddCounter(hQuery, szPathBuffer, 0, &hCounter);
        pdhStatus = PdhCollectQueryData(hQuery);
    
        // 获得当前实例的"ID Process"计数器的值
        DWORD                  ctrType;
        PDH_FMT_COUNTERVALUE   fmtValue;
        pdhStatus = PdhGetFormattedCounterValue(hCounter, PDH_FMT_LONG, 
            &ctrType, &fmtValue);
    
        // 关闭查询对象
        pdhStatus = PdhCloseQuery (hQuery);
    
        return fmtValue.longValue;
    }

    注:PDH函数在Pdh.dll中实现。程序开发中,我们须要包括头文件Pdh.h,连接库文件Pdh.lib。

    演示程序说明
    我们的演示程序使用VC6.0开发完毕,是一个基于对话框的MFC程序。程序设计秉承OOP风格,以及用户界面(User Interface)与业务逻辑(Business Logic)分离的原则,结构简单、条理清晰,相信大家非常easy能够读懂代码。

    由于本文总共介绍了四种进程枚举的方法,我们设计了例如以下一个逻辑控制类继承结构:

    图5 演示程序逻辑控制类结构

    另外。演示程序对于进程调用的模块採用了延后枚举(Lazy Enumerating)的策略。即在程序启动的时候并没有将全部进程调用的模块都枚举好,而仅在须要的时候进行。这样能够显著节省程序启动的时间。

    写在最后
    进程隐藏(与其相对的就是进程枚举)一直是一个非常热门的话题,思路有非常多,当中有一种就是拦截系统API函数EnumProcesses的调用。通读本文后,你认为这样的思路可行吗?或者你有了其它新的想法。这些都是笔者写作此文的初衷。(注:成文于2004年,发表于《CSDN开发高手》。)

  • 相关阅读:
    Java学习二十九天
    Java学习二十八天
    47. Permutations II 全排列可重复版本
    46. Permutations 全排列,无重复
    subset ii 子集 有重复元素
    339. Nested List Weight Sum 339.嵌套列表权重总和
    251. Flatten 2D Vector 平铺二维矩阵
    217. Contains Duplicate数组重复元素
    209. Minimum Size Subarray Sum 结果大于等于目标的最小长度数组
    438. Find All Anagrams in a String 查找字符串中的所有Anagrams
  • 原文地址:https://www.cnblogs.com/liguangsunls/p/7399470.html
Copyright © 2011-2022 走看看