65.9K
CodeProject 正在变化。 阅读更多。
Home

专业系统库:简介

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (86投票s)

2008年7月29日

CPOL

14分钟阅读

viewsIcon

210003

downloadIcon

3440

提供一种简化且统一的方式来访问有关进程、系统和环境的最常用信息。

目录

前言

大多数软件开发人员在职业生涯中都会遇到类似性质的项目,而很少有开发人员能够从一个项目成功过渡到完全不同的项目。

就我个人而言,我发现有一个特定领域,它在我的一个项目到另一个项目之间一直占用我大量的时间,那就是围绕检索和更改系统/操作系统、当前进程、线程、系统各种软硬件配置、安全上下文等信息而编写代码……我希望这一切对许多开发者来说听起来都很熟悉。

不仅要花一些时间去弄清楚如何访问所需相同的信息,这取决于正在使用的开发平台,而且这些知识很难从一个平台推断到另一个平台。例如,VC++、VB6、C#、Delphi、Office——在各个方面都有很大不同,你在一个平台上的代码在另一个平台上的代码可能看起来完全无法使用。

然后,复杂性就出现了。如果我们能在 C++ 中以相当自然的方式完成几乎任何技巧,那么在更严格的环境中做同样的事情要么是不可能的,要么会破坏系统的完整性,比如在 .NET 中使用非托管代码,或者大量进行外部过程导入以用于 VB6 等。

在各种开发环境中系统性实践的基础上,产生了一个想法,就是总结我在这个领域的所有知识,并为软件开发人员提供一种简单统一的方式,让他们能够轻松地以相同的方式访问所有这些信息,无论使用哪种开发环境,而且只需付出极小的努力。

整篇文章都是关于编写一个库的倡议的介绍,该库将允许轻松访问系统、客户端进程和程序环境中经常使用的信息。

这是一个非常近期开始的项目(2008 年 7 月)。我正试图组织关于该项目以及为其开发工作的额外信息,请访问此网站

简介

应用程序可以通过四种方式从系统中检索信息

  1. 标准 Windows API
  2. 未公开的 Windows API
  3. 通过 Windows 注册表 + 文件系统直接访问系统
  4. WMI(Windows 管理接口)

在选择哪种方法最好时,我们的选择主要取决于以下标准(按普通开发人员看这些东西的顺序排列)

  1. 实现复杂性
  2. 可靠性
  3. 执行速度
  4. 资源消耗

Professional System Library (ProSysLib) 是一个提供统一访问进程/系统/环境信息的项目,开发人员将不再需要艰难地选择这些标准,试图决定哪个最重要或哪个可以牺牲。

ProSysLib 使用类似 .NET 中的根命名空间的概念来呈现所有信息,其中 `System` 是所有内容的根命名空间。同样,ProSysLib 也有自己的 `System` 根命名空间,它定义了库的所有子命名空间和功能的入口点。

本文开头的图示显示了 `System` 下的命名空间层次结构。这些定义了可访问信息进一步分类的基础。

技术亮点

ProSysLib DLL 是一个 Unicode COM 进程内服务器,使用中性内存模型。它是线程安全的,并且仅实现自动化接口。32 位和 64 位组件版本的协议(类型库)声明是相同的,从而允许与可以混合 32 位和 64 位模式的开发工具进行无缝集成,同时使用相同的类型库(签名)组件。ProSysLib 还免疫 DLL Hell 问题(请参阅 ProSysLib SDK 获取详细信息)。

实现完全使用 VC++ 2008 完成,仅使用 COM、ATL、STL 和 Windows API。

整个 ProSysLib 框架基于即时激活,这意味着每个命名空间和对象仅在客户端应用程序首次使用时才会被实例化和初始化,在此之前,ProSysLib 在资源方面完全是无负担的。

在本文发布时,库中仅引入了少数对象和命名空间。至于其余的命名空间、属性和方法,如果应用程序尝试使用它们,库将抛出 COM 异常“NOT IMPLEMENTED”,告知您正在尝试使用库中已声明但尚未实现的某些内容。

使用代码

由于库概念是基于根命名空间的,因此它是在创建客户端应用程序才能访问其他所有内容的唯一接口,非常类似于 .NET 中的 `System` 命名空间。事实上,即使您尝试,该库的防错实现也不会让您创建库的任何其他接口。

在不同的开发环境中声明和实例化变量可能看起来有所不同,而使用它则看起来几乎相同。在本文中,我们将所有代码示例简化为仅针对 C# 客户端。任何开发人员都应该能够弄清楚这在他们选择的环境中看起来会是什么样子。

PSLSystem sys = new PSLSystem();
// Declare and instantiate ProSysLib root namespace;

现在,使用这个变量,我们可以访问根命名空间下的任何我们想要的内容。

虽然 ProSysLib 旨在实现对多种信息的访问,但该项目才刚刚开始,目前实现的功能还不多。但是,我不想凭空画出抽象,你可以通过查看 ProSysLib 文档来了解它们,所以这里提供的所有示例都是真实的,即已经完全可用。

所以,让我们来看看 ProSysLib 到目前为止(项目开始不到一个月)的一些示例。

特权

许多应用程序需要了解和控制进程可用的特权。例如,*Debug* 特权在访问系统中其他方式无法获得的某些高级信息时可能很重要。ProSysLib 在 `PSLSystem.Security.Privileges` 命名空间下提供所有可用特权的集合。如果需要启用进程中的 *Debug* 特权,代码应如下所示

sys.Security.Privileges.Find(“SeDebugPrivilege”).Enabled = true;

也就是说,我们访问特权集合,找到感兴趣的特权,然后启用它。我们通常需要验证 `Find` 方法是否成功在列表中找到该特权,但由于 *Debug* 特权始终可用,我们可以在这里简化它。

进程枚举

CodeProject 上非常受欢迎的一个主题是关于枚举系统中所有可用的进程,或者查找特定进程,或者如何按名称或 ID 杀死进程等等。ProSysLib 在 `PSLSystem.Software.Processes` 命名空间下枚举系统中运行的所有进程。这个集合非常灵活,允许对系统中的进程进行任何类型的操作。

本文附加了一个简单的 C# 应用程序,它展示了 ProSysLib 的一个用法示例。该示例枚举所有进程或当前用户帐户下启动的进程。它仅显示每个进程的一些可用信息,并允许通过按一个按钮来终止任何进程。

这里是示例中的一小段代码,其中我们用进程信息填充列表视图对象

foreach (PSLProcess p in sys.Software.Processes) // Go through all processes;
{
   ListViewItem item = ProcessList.Items.Add(p.ProcessID.ToString());

   string sProcessName = "";
   if (p.ProcessID == 0) // System Idle Process
       sProcessName = "System Idle Process";
   else
   {
       sProcessName = p.FileName;
       if (sys.Software.OS.Is64Bit && p.Is64Bit == false)
       // If OS is 64-bit, and the found process
            sProcessName += " *32";
            // is not 64-bit, we add " *32" like in TaskManager;
   }

    // Adding some of the available process details...
    //
    // NOTE: p.FilePath for 64-bit processes will always be
    // empty for a 32-bit client running on a 64-bit system,
    // simply because Windows prohibits 32-bit processes to
    // have any access to 64-bit-specific folders;

    item.SubItems.Add(sProcessName);
    item.SubItems.Add(p.FilePath); 
    item.SubItems.Add(p.UserName);
    item.SubItems.Add(p.ThreadCount.ToString());
    item.SubItems.Add(p.HandleCount.ToString());

    item.Tag = p;
    // Associate each list item with the process object;
}

演示应用程序二进制文件同时提供 32 位和 64 位版本。

从演示应用程序中,您可能会注意到 ProSysLib 的一个相当有趣之处在于,它不需要在您的 PC 上注册任何内容即可成功运行。如果有人认为这是 .NET 的 COM 隔离,他们就错了。这是 COM 的隐秘部署的实现,是我在分发 COM 项目的长期实践中想出的。在 ProSysLib SDK部署章节中有对该想法的完整描述。

命名对象的访问权限/掩码

许多应用程序的另一个典型任务是找出当前进程对系统中某个特定对象的访问权限。通常,它是文件或文件夹,我们想知道我们可以用它做什么。我确切地知道,在 C++ 中获取此信息并不那么直接,在其他环境中可能更加麻烦。

`PSLSystem.Security` 命名空间提供了 `GetNamedObjectAccess` 函数,该函数允许在一行代码中获取系统中任何命名对象(文件、文件夹、打印机、服务、注册表项、网络共享)的*访问掩码*。

long AccessMask = 0;
long lErrorCode = sys.Security.GetNamedObjectAccess(ntFileOrFolder, 
                  "my file or folder path", ref AccessMask);

进程亲和性

到目前为止,我在库中实现的另一个小功能是如何控制*进程亲和性*。`PSLSystem.Process` 命名空间包含当前进程的所有属性(或者最终会包含)。其中之一是 `AffinityMask`,可以非常轻松地更改。例如,如果您有一个双核系统,并且只想在第二个核心上执行您的进程,您的代码将是

sys.Process.AffinityMask = 2;
// 2 in binary corresponds to the second core;

同样,对于 `PSLSystem.Software.Processes` 集合中的每个进程,我们都有一个 `AffinityMask` 来获取/设置系统中任何其他进程的*亲和掩码*,就像任务管理器可以做的那样。我在演示中没有使用它,因为我无法一次展示所有内容。

WMI

Windows Management Interface 是一项遗留技术,有时宁愿它从未存在过。就 WMI 而言,我们谈论的是由尽管非常必要但构思不周的技术引起的一系列问题。这是编写 ProSysLib 的另一个原因,即提供一种更简单、更快速的从系统获取信息的替代方法。

下面是 WMI 中可能存在的主要问题列表

  1. 它基于过时的 DCOM,DCOM 本身就非常慢,更不用说在网络上使用它了。Microsoft 针对 .NET 2.0 - 3.5 的 WMI 层更像是一个笑话,因为他们试图将好坏融为一体;
  2. WMI 非常消耗资源;
  3. WMI 设备供应商经常在其 WMI 提供程序中犯错误/产生 bug,使用它们会导致应用程序崩溃;
  4. WMI 仅被少数设备供应商支持,因此,大量的硬件信息无法通过 WMI 获取;
  5. 对于 C++ 这样的低级语言,WMI 的使用可能相当复杂。

不幸的是,尽管 WMI 存在所有这些缺陷,但关于系统的某些细节似乎无法以其他方式获取,或者需要付出太多努力。我的个人建议——只有在您确实需要时才使用 WMI。

对于那些情况,ProSysLib 通过 `PSLSystem.Tools.WMI` 命名空间提供了对 WMI 功能的简化访问。它提供了一些方法,可以以最简单的方式从 WMI 获取信息。

让我们看一个从 WMI 类 `Win32_OperatingSystem` 获取属性 `Caption` 的例子,该属性是当前操作系统的标题。

string OSCaption = sys.Tools.WMI.GetValue(null, “Win32_OperatingSystem”, “Caption”);

在此示例中,我们为命名空间传递了 `null`,因为 `Win32_OperatingSystem` 类通常位于“root\cimv2”的默认命名空间中(也是接口的 `DefaultNamespace` 属性)。

`WMI` 命名空间提供了一些方法,可以以最简单的方式从 WMI 获取信息。而且,虽然它最多能将 .NET 下的 WMI 编码简化一倍,但对于 C++ 开发人员来说,它能将 WMI 的使用简化 90%。

例如,如果您想从同一个类中获取属性“`BuildNumber`”、“`CountryCode`”和“`Locale”的信息,您可以使用以下方法

Array a = sys.Tools.WMI.GetColValues("root\\cimv2", 
          "Win32_OperatingSystem", "BuildNumber, CountryCode, Locale");

`WMI` 命名空间有一个方法,它接受一个逗号分隔的属性名称列表,并将它们的别名作为变体数组返回。这用于单记录 WMI 类。

同样,如果您想获取所有记录但只有一列(多记录 WMI 类)的值,您可以进行如下调用

Array a = sys.Tools.WMI.GetRowValues("root\\cimv2", "Win32_Product", "Caption");

此方法返回所有行和选定列的值,也作为变体数组。

而且,如果您想完全使用 WMI,即发出 WQL 查询以获取整个数据表,您的代码将如下所示

// Selecting Caption and DeviceID for all queued printers:
PSLTable t = sys.Tools.WMI.GetData("root\\cimv2", 
             "SELECT Caption, DeviceID FROM Win32_Printer WHERE Queued=True");
// Adding our table into the list of values:
for(int i = 0;i < t.nRows;i ++)
{
    MyList.Add(t.GetValue(i, 0).ToString()); // Adding Caption value to the list;
    MyList.Add(t.GetValue(i, 1).ToString()); // Adding DeviceID value to the list;
}

此方法返回一个 `PSLTable` 对象,该对象简化了使用数组类型寻址 `{row, column}` 的数据访问。此外,它还具有记录集对象的功能,始终可以使用以下方式获取列列表

// We would get "Caption" for our example here;
string FirstColumnName = t.GetColName(0);
// "DeviceID"
string SecondColumnName = t.GetColName(1);

这在您发出 WQL 查询并使用 `SELECT *` 时特别有用,即选择所有列,因此您不知道哪一列在哪里。

错误和异常处理

ProSysLib 在错误和异常处理方面也名副其实。任何错误/异常都会得到妥善处理,并通过*COM 异常*暴露给客户端,提供对任何功能性问题的数值和文本解释。

*COM 异常*易于处理,并且在任何环境中都自动支持。例如,在 .NET 中,COM 异常由 `System.Runtime.InteropServices.COMException` 对象处理,而在 C++ 中,则通过类型库导入机制生成的 `_com_error` 类型处理。

ProSysLib 根命名空间包含一个 `DecodeException` 方法,该方法允许将数值 COM 错误轻松解释为枚举类型。让我们看一个异常处理示例,其中我们尝试使用一个超出合理范围的索引来访问集合对象,期望发生“索引越界”类型的异常

try
{
    string sName = sys.Software.Processes[100000].FileName;
}
catch(System.Runtime.InteropServices.COMException ex)
{
    if(sys.DecodeException(ex.ErrorCode) == 
           PSLException.exceptionIndexOutOfRange)
    {
        // Yes, this is the exception that we indeed expected;
    }
    MessageBox.Show(ex.Message); // Show the exception message;
}

总结

这些只是 ProSysLib 架构中已实现的一些示例,并且还有更多内容正在进行中。如果您查看命名空间树和 ProSysLib SDK 文档,您可以看到该项目的宏伟蓝图。本文再次只是触及了该项目的表面。而且,我希望它能找到愿意加入开发该项目的开发者,用他们独特的 C++ 经验为所有软件平台的开发者提供他们的知识。

随着我继续开发该项目,我将在此发布更多文章,重点介绍新推出的特定功能,而不再考虑整体。

兴趣点

我一直很喜欢编写专业的 COM 服务器,这些服务器大量使用 COM 集合、内部 COM 实例化、自动化事件封送器以及互联网上通常覆盖不足的许多其他 COM 技巧。事实上,我从这个项目中学习了很多。

例如,该项目的大部分代码都基于未公开的 Windows API 或文档不完善的 API,学习这些 API 是一项不错的挑战。挖掘有关 `ZwQuerySystemInformation` API 函数的 64 位实现真相很有趣,这需要通过代码调试来了解实际情况。互联网上关于 `ZwQuerySystemInformation` 类的所有信息都仅适用于 32 位,但似乎没有人知道这一点,甚至 Microsoft 在 MSDN 上也发布了误导性信息:)

总之,我计划在开始实施 ProSysLib 时发布这些技巧以及许多其他技巧,因为我相信,这一切都是一个独立的主题,目前,我只是想让这篇文章保持简单。

历史

  • 2008 年 7 月 29 日:文章初稿
  • 2008 年 8 月 1 日:将下载更新为最新版本的 ProSysLib SDK,现在该 SDK 安装了项目本身的完整源代码
  • 2008 年 8 月 5 日。将项目网站从 prosyslib.com 更改为 prosyslib.org,以便新读者更容易看到项目的免费开源性质。此外,还更新了本文中的链接。
  • 2008 年 8 月 20 日。文章中的许多小更新,如重新措辞、添加目录、简化下载列表等。此外,我认为有必要添加一个后续文章:ProSysLib:剖析进程
  • 2010 年 10 月 12 日:更新了文章的下载文件
  • 2010 年 11 月 20 日:更新了文章的下载文件

附言:感谢您对文章的评论和公正评价。

© . All rights reserved.