• 纵谈进程枚举


    当程序出现异常而失去响应,我们通常的做法是打开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开发高手》。)

  • 相关阅读:
    Git 自救指南:这些坑你都跳得出吗?
    敢不敢模拟超过 5 万的并发用户?
    一条简单的 SQL 执行超过 1000ms,纳尼?
    JVM 最多支持多少个线程?
    19 条效率至少提高 3 倍的 MySQL 技巧
    LeetCode 剑指offer 面试题04. 二维数组中的查找
    LeetCode 剑指offer 面试题03 数组中重复的数字
    东华大学计算机软件工程 复试最后一百题
    东华大学计算机软件工程复试 挑战练习
    东华大学计算机软件工程复试 进阶练习
  • 原文地址:https://www.cnblogs.com/liguangsunls/p/7399470.html
Copyright © 2020-2023  润新知