.NET 应用程序引导






4.98/5 (15投票s)
本文解释了 .NET 应用程序的引导过程。
撰写本文的目的是分享开始学习任何新技术之前应遵循的最佳实践之一,即了解技术背后的基础知识。此外,也是提醒所有 .NET 开发者回顾一下他们可能错过了什么。本文的预期读者是对了解 .NET 应用程序引导过程感兴趣的人。当然,开发者都很聪明,会意识到这些概念。如果你已经了解,这将是一个参考点,否则,它将添加到你的知识库中。
引言
我们学习任何新技术总是从我们的好朋友“Hello World
”程序开始 : )。如果程序能够执行,那么我们就得出结论,所有先决条件(包括与技术相关的任何配置)都已正确设置,我们可以开始深入学习了。通过这个,我们也理解了程序的基本生命周期。所有开发者都渴望执行程序并观察输出,但对程序内部发生的事情却漠不关心。当然,你也可以在不知道其内部机制的情况下开发系统,但这总是显得不完整。只有当开发者了解其完整的生命周期时,才能提供一个万无一失的解决方案。让我们开始我们的旅程。
背景
引导通常是指将基本基础设施加载到计算机内存中的过程,然后由该基础设施负责按需加载其他软件。该过程涉及一系列阶段,在每个阶段,一个更小、更简单的程序加载并执行下一个阶段中更大、更复杂的程序。当这些陈述与我们的 .NET 应用程序进行比较时,引导包括将基本基础设施(CLR)加载到进程中,CLR 进一步创建 AppDomain
(默认 AppDomain
)并将所有必需的以及引用的程序集加载到 AppDomain
。让我们回顾一下 CLR、AppDomain
和程序集是什么。我们不会深入探讨这些陈述,因为本文的重点仅在于这些元素的引导,有关这些元素的更多信息,你可以参考其他文章或 MSDN 知识库。下面是运行 .NET 应用程序的三个重要实体。
- CLR (Common Language Runtime): 负责管理和执行程序集中的代码。CLR 的核心功能(如内存管理、程序集加载、安全、异常处理和线程同步)可供所有以它为目标的编程语言使用。
- AppDomain:
AppDomain
是一个隔离单元,也是一组程序集的逻辑容器。当 CLR 初始化时创建的第一个AppDomain
称为默认AppDomain
;只有当 Windows 进程终止时,此AppDomain
才会销毁。 - Assembly: 它是部署和版本控制的基本单元。程序集包含一个或多个托管模块。
Using the Code
这里提供的代码片段只是演示了如何获取当前的 AppDomain
、创建一个新的 AppDomain
以及将程序集加载到新创建的 AppDomain
中。本文将进一步深入解释所有这些概念以及负责创建所有这些实体的对象。
static void Main(string[] args)
{
// Get a reference to the AppDomain that the calling thread is executing
AppDomain currentDomain = Thread.GetDomain();
// Display the name of the current AppDomain.
Console.WriteLine("Current AppDomain name :" + currentDomain.FriendlyName);
// Create an instance of Test class
Test testObj = new Test();
// Call the display method from current Domain.
// Here, the current Domain is the Default domain that is created by CLR.
// The friendly name of the current AppDomain is always the name of the executing assembly.
testObj.Display("Hi, Display method called from Current AppDomain");
// Create a new AppDomain
AppDomain domain = AppDomain.CreateDomain("MyNewDomain");
// Display friendly name of the newly created app domain.
// The friendly name is the one that is passed as parameter to the CreateDomain method.
Console.WriteLine("Name of the newly created domain: " + domain.FriendlyName);
// Load assembly Sample Test
Assembly assemblyObj = domain.Load("SampleTest");
Console.WriteLine("SampleTest assembly loaded in the new App Domain(MyNewAppDomain)");
// Get the type
Type myType = assemblyObj.GetType("SampleTest.Test");
// Create an instance of the type.
var type = Activator.CreateInstance(myType);
// Invoke the display method from the newly created AppDomain
myType.InvokeMember("Display", BindingFlags.InvokeMethod, null, type, new object[]
{ "Hi, Display method called from newly created AppDomain" });
Console.ReadKey();
}
幕后
我们都知道任何 Windows / .NET 应用程序都在自己的进程中运行。这是 Windows 平台提供的强大功能,用于保护应用程序及其数据。从上图可以看出,应用程序位于单个进程下。一个进程可以包含许多 AppDomain
。每个进程都有自己加载的程序集副本。此外,你还可以注意到,尽管两个 AppDomain
都包含 System.dll,但除了 Domain Neutral 类型的程序集(如 MsCorlib.dll)之外,没有共享公共程序集的机制,因为 AppDomain
的目的是提供完全隔离。只有那些对 .NET Framework 至关重要的 DLL 才以 Domain Neutral 的方式加载,以最大限度地利用资源。进程通过创建的 Main
线程执行所有这些功能。进程的生命周期仅通过创建的 Main
线程得以维持,否则它就是死气沉沉的。这只是对系统的整体概览,你可以从中关联进程、CLR、AppDomains 和程序集。
注意: Domain Neutral 程序集的主要缺点是,直到应用程序关闭,它们永远无法被卸载。
摘要
- .NET/Windows 应用程序运行在自己的进程中。
- 每个进程可以有多个
AppDomain
。 - 除了 Domain Neutral 程序集,任何程序集都不能在
AppDomain
之间共享。 - 任何程序集都无法从任何
AppDomain
中卸载,卸载程序集的唯一方法是卸载整个AppDomain
。
应用程序引导过程
到目前为止,你已经了解了 .NET 应用程序相对于进程的实际快照。现在让我们继续探讨上图中所示的所有必需组件如何加载到进程中以及应用程序如何获得生命。在 .NET 中,所有可执行应用程序,如控制台 UI 应用程序、Windows Forms 应用程序和 WPF 应用程序,都是自托管应用程序的示例,并具有托管 EXE 文件。我们知道任何 Windows 托管或非托管可执行应用程序都必须符合 PE (Portable Executable) 文件格式。操作系统不会区分 .NET 程序集和 Win32 可执行二进制文件;对它而言,它们都是普通的 PE 文件。但实际的区分只在读取 PE 头中的内容后才会发生,Windows 会从中获得许多详细信息,例如要创建的进程类型(32 位或 64 位)、程序集是托管的还是非托管的?文件类型以及所有必要信息。PE 文件格式实际上是分段的。
这是任何 Windows 平台可执行文件都必须遵守的标准协议。每个部分都充当 Windows 进程采取必要操作的参考数据(简而言之,它可以作为元数据)。它们都默认具有三个部分:.text、.reloc 和 .rsrc。.text 部分包含导入表、导入地址表和 .NET 部分。.reloc 仅用于重新定位入口点指令跳转到的地址(它是 IAT 中唯一的地址)。.rsrc 部分仅包含可执行文件的主要图标,因为所有其他资源都在 .NET 部分中。 .NET 部分包含 CLR 头,它提供了与 CLR 相关的所有信息,如版本、位置、大小、入口点、使程序集成为托管模块的元数据。
利用这些数据,Windows 进程可以正确地执行所有必需的初始化和数据加载。在这里,我们不逐一介绍每个部分,只考虑 .NET 应用程序所需的部分。为了使讨论更有趣,我们将绘制整个过程的流程图,以提供清晰的图景。
注意: 引导过程仍然很复杂,但我只提取了整体过程的精华,并只考虑了与 .NET 应用程序相关的过程。存在许多中间阶段,但它们不在本文的讨论范围之内。
引导过程中涉及的步骤总结
- 用户双击 .exe (可执行文件)。
- Windows 验证 PE 文件。标准的 Windows PE 文件头类似于 COFF (Common Object File Format) 头。如果头使用 PE32 格式,则文件可以在 32 位或 64 位版本的 Windows 上运行。如果头使用 PE32+ 格式,则文件需要 64 位版本的 Windows 才能运行。因此,根据这些信息,Windows 会创建一个合适的进程以继续执行。
- 接下来,它检查 .text 部分的 15 个目录,这些信息表明可执行文件是托管的还是非托管的。
- 如果它是托管可执行文件,则进程加载 MSCorEE.dll (Shim)。它也被称为 Microsoft Component Runtime Execution Engine。
- 进程检查 CLR 头以查找要加载的 CLR 版本,并将 CLR 信息告知 Shim。
- Shim 创建 CLR 实例,CLR 被初始化。它是 .NET Framework 的核心,因为它负责创建 CLR 实例。
- CLR 创建默认
AppDomain
。在此过程之后,CLR 还负责初始化内存、线程同步等。 - CLR 将所有必需的以及引用的 DLL 加载到
AppDomain
。注意: 引用 DLL 是按需加载的。这意味着并非所有引用的 DLL 都会一次性加载,它只在访问与相应 DLL 相关的数据时才会被加载。一旦加载,它就无法被卸载。 - CLR 调用
Main
方法,应用程序启动。
CLR 引导过程
如果你进一步深入研究 CLR 的活动,我们可以发现 CLR 在其引导过程中执行以下操作。在 CLR 执行第一行托管代码之前,它会创建三个应用程序域。其中两个在托管代码内部是不可见的,甚至对 CLR 主机也是不可见的。它们只能通过 Shim(mscoree.dll 和 mscorwks.dll)提供的 CLR 引导过程来创建。
System Domain: SystemDomain
负责创建和初始化 SharedDomain
和默认 AppDomain
。它将系统库 mscorlib.dll 加载到 SharedDomain
中。它跟踪进程中的所有域,并实现加载和卸载 AppDomain
的功能。
Shared Domain: 所有 Domain-Neutral 代码都加载到 SharedDomain
中。Mscorlib(系统库)是所有 AppDomain
中用户代码所必需的。它会自动加载到 SharedDomain
中。SharedDomain
还管理一个由基地址索引的程序集映射,该映射充当查找表,用于管理加载到 DefaultDomain
的程序集以及在托管代码中创建的其他 AppDomain
的共享依赖项。来自 System
命名空间的基本类型,如 Object
、ValueType
、Array
、Enum
、String
和 Delegate
,在 CLR 引导过程中会预先加载到此域中。
Default Domain: DefaultDomain
是一个 AppDomain
实例,应用程序代码通常在该实例内执行。虽然某些应用程序需要在运行时创建额外的 AppDomain
,但大多数应用程序在其生命周期内只创建一个域。在此域中执行的所有代码都在域级别上是上下文绑定的。如果应用程序有多个 AppDomain
,任何跨域访问都将通过 .NET Remoting 代理进行。
关于 CLR 的一些事实
- Microsoft 将 CLR 实现为包含在 DLL 中的 COM 服务器。
- 代表 CLR 的 COM 服务器就像任何其他 COM 服务器一样在 Windows 注册表中注册。
- CLR 的实际代码包含在名为 MSCorWks.dll 的文件中(适用于版本 1.0、1.1、2.0),而对于版本 4.0,它包含在 Clr.dll 文件中。
- 要在一个进程中创建 CLR 实例,可以调用 MSCorEE.dll 中定义的
CLRCreateInstance
函数,该函数返回ICLRMetaHost 接口
。主机可以调用ICLRMetaHost 接口
的GetRuntime
函数,并指定 CLR 版本。然后,Shim 将所需的 CLR 版本加载到主机的进程中。通过这种方式,你甚至可以根据需要加载 CLR。 - CLR 可以在应用程序中禁用,这只会停止托管代码的执行,但非托管代码仍可继续运行。
- CLR 头提供了与 CLR 相关的所有信息,如版本、位置、大小、入口点、使程序集成为托管模块的元数据。
关于 AppDomain 的一些事实
- CLR 创建的第一个
AppDomain
称为默认AppDomain
。 - 只有当 Windows 进程终止时,默认
AppDomain
才会销毁。 - 在一个
AppDomain
中创建的对象不能被另一个AppDomain
中的代码直接访问。要做到这一点,你可以使用封送(按值或按引用)进行通信。 AppDomain
可以被卸载。AppDomain
中的任何程序集都不能被卸载。要做到这一点,你需要卸载AppDomain
本身。- 创建的每个
AppDomain
都可以单独配置,包括与程序集加载、搜索路径、影子复制等相关的配置。 - 在一个 Windows 进程中运行的
AppDomain
的数量没有硬性限制。 - 当 CLR 将程序集加载到目标
AppDomain
时,它会使用该特定AppDomain
的配置设置。 AppDomain
的FirstChanceException
负责捕获AppDomain
中发生的任何异常,前提是有人正在监听它。
注意: 引导过程非常广泛和复杂。我已对其进行了调整,以便为你提供对该过程的基本理解。存在许多阶段,但我只考虑了负责启动 .NET 应用程序的主要阶段。
关注点
写这篇文章真的很有趣。没有一个单一的数据存储点能让我获得引导过程的连续信息。这个主题有很多文章,但对我来说,很难将实际流程映射出来。在收集这些信息时,深入了解概念非常有意思。希望你喜欢阅读这篇文章。如果有什么需要改进的地方,请提出建议。