C#技术漫谈之公共语言运行库(CLR)
概述
.NET Framework的核心是其运行库的执行环境,称为公共语言运行库(CLR)或.NET运行库。通常将在CLR的控制下运行的代码称为托管代码(managed code)。
但是,在CLR执行编写好的源代码之前,需要编译它们(在C#中或其它语言中)。在.NET中,编译分为两个阶段:
1、把源代码编译为Microsoft中间语言(IL)。
2、CLR把IL编译为平台专用的代码。
这个两阶段的编译过程非常重要,因为Microsoft中间语言(托管代码)是提供.NET的许多优点的关键.
.NET平台的整体结构:
.NET Framework是架构在Windows平台上的一个虚拟的运行平台,你可以想象将最下层Windows换做其他的操作系统,例如说Linux, 一样可以实现使用符合CLS(Common Language Specification, 通用语言规范)的.NET语言(VB.NET、C#、JScript.NET等)来创建ASP.NET或Windows Form(可能会叫Linux Forms)应用程序的功能,这其实就是Mono计划要实现的功能。所以可以这么认为,理论上,C#是一种可以跨平台的语言,这很象Java。
C#另一个比较象Java的地方是,它也是一种(特殊意义上的)语言,同Java一样,C#编写的程序代码也是先通过C#编译器编译为一种特殊的字节代码 (Microsoft Intermediate Language, MSIL,微软中间语言),运行时再经由特定的编译器(JIT编译器)编译为机器代码,以供操作系统执行。
不仅是C#语言,所有.NET语言(将会包括我们常用的几十种现代的编码语言)都可以编写面向CLR的程序代码,这种代码在.NET中被称为托管代码(Managed Code),所有的Managed Code都直接运行在CLR上,具有与平台无关的特性。
解释性的语言很安全,并且可以通过他的运行平台为其赋予更多的功能,例如自动内存管理,异常处理等。
CLR结构图
C#所具有的许多特点都是由CLR提供的,如类型安全(Type Checker)、垃圾回收(Garbage Collector)、异常处理(Exception Manager)、向下兼容(COM Marshaler)等,具体的说,.NET上的CLR为开发者提供如下的服务:
1、平台无关:CLR实际上是提供了一项使用了虚拟机技术的产品,它构架在操作系统之上,并不要求程序的运行平台是 Windows系统,只要是能够支持它的运行库的系统,都可以在上面运行.NET应用。所以,一个完全由托管代码组成的应用程序,只要编译一次,就可以在任何支持.NET的平台上运行。
2、跨语言集成:CLR允许以任何语言进行开发,用这些语言开发的代码,可以在CLR环境下紧密无缝的进行交叉调用,例如,可以用VB声明一个基类对象,然后在C#代码中直接创建次基类的派生类。
3、自动内存管理:CLR提供了垃圾收集机制,可以自动管理内存。当对象或变量的生命周期结速后,CLR会自动释放他们所占用的内存.
4、跨语言应用
当编程人员在用自己喜欢的编程语言写源代码的时候, 这个源代码在被转化成媒介语言(IL)之前,先被编译成了一个独立的可执行单元(PE)。这样无论你是一个VB.NET程序员,或一个C#程序员,甚至是使用托管的C++的程序员。只要被编译成IL就是同等的。 首先,编译输出的exe是一个由中间语言(IL),元数据(Metadata)和一个额外的被编译器添加的目标平台的标准可执行文件头(比如Win32平台就是加了一个标准Win32可执行文件头)组成的PE(portable executable,可移植执行体)文件,而不是传统的二进制可执行文件 —— 虽然他们有着相同的扩展名。
中间语言是一组独立于CPU的指令集,它可以被即时编译器Jitter翻译成目标平台的本地代码。中间语言代码使得所有Microsoft.NET平台的高级语言C#, VB.NET, VC.NET等得以平台独立,以及语言之间实现互操作。元数据是一个内嵌于PE文件的表的集合。
5、版本控制
由于使用了元数据,所以你可以使用XCOPY简单的复制就可以了,而CLR也可以在运行时期读取元数据,以确保多版本程序运行在同一进程中。使用公共语言运行库的程序集的所有版本控制都在程序集级别上进行。一个程序集的特定版本和依赖程序集的版本在该程序集的清单中记录下来。除非被配置文件(应用程序配置文件、发行者策略文件和计算机的管理员配置文件)中的显式版本策略重写,否则运行库的默认版本策略是,应用程序只与它们生成和测试时所用的程序集版本一起运行。
6、.NET安全
.NET提供了一组安全方案。负责进行代码的访问安全性检查。允许我们对保护资源和操作的访问。代码需要经过身份确认和出处鉴别后才能得到不同程度的信任。安全策略是一组可配置的规则,公共语言运行库在决定允许代码执行的操作时遵循此规则。安全策略由管理员设置,并由运行库强制。
运行库确保代码只能访问安全策略允许的资源和调用安全策略允许的代码。 每当发生加载程序集的尝试时,运行库就使用安全策略确定授予程序集的权限。在检查了描述程序集标识的信息(称为证据)后,运行库使用安全策略决定代码的信任程度和由此授予程序集的权限。证据包括但不仅限于代码的出版商、它的站点以及它的区域。安全策略还确定授予应用程序域的权限。
7、简单的组件互操作性。
8、自描述组件:自描述组件是指将所有数据和代码都放在一个文件中的执行文件。自描诉组件可以大大简化系统的开发和配置,并且改进系统的可靠性。
通用语言运行时(CommonLanguageRuntiome, CLR)最早被称为下一代Windows服务运行时(NGWS Runtime)。它是直接建立在操作系统上的一个虚拟环境,主要的任务是管理代码的运行。CLR现在支持几十种现代的编程语言为它编写代码,然后以一种中间语言(Intermediate Langeoage, IL)代码的形成被执行。并且,CLR还提供了许多功能以简化代码的开发和应用配置,同时也改善了应用程序的可靠性。如你所知,如果某种语言的编译器是以运行时为目标的,那么利用该语言开发生成的代码在.NET中被称为托管代码,因为这样的代码是直接运行在CLR上的,所以具有与平台无关的特点。
在.NET平台结构图中,CLR的上面是.NET的基类库,这组基类库包括从基本输入输出到数据访问等各方面,提供了一个统一的面向对象的,层次化的,可扩展的编程接口。从.NET平台结构图中也可以看到,基类库可以被各种语言调用和扩展,也就是说不管是C#,VB.NET还是VC++.NET,都可以自由的调用.NET的类库,因为C#自身只有77个关键字,而且语法对程序员来说无需费工夫学习。 BCL则相反,它包含了4500个以上的类和无数的方法、属性,在你的C#程序中随时都可能会用到它来完成自己的任务。
还有一个很重要的概念你需要明白,这就是公共语言架构(Common Language Infrastructure, CLI). CLI是CLR的一个子集,也就是.NET中最终对编译成MSIL代码的应用程序的运行环境进行管理的那一部分。在CLR结构图中CLI位于下半部分,主要包括类加载器(Class Loader)、实时编译器(IL To Native Compilers)和一个运行时环境的垃圾收集及将使用任何语言编写的代码,通过其特定的编译器转换为MSIL代码之后运行其上,甚至还可以自己写 MSIL在CLI上运行。
9、调用和配置
当运行库试图解析对另一个程序集的引用时,就开始进行定位并绑定到程序集的进程。该引用可以是静态的,也可以是动态的。在生成时,编译器在程序集清单的元数据中记录静态引用。动态引用是由于调用各种方法而动态构造的,例如 System.Reflection.Assembly.Load 方法。 引用程序集的首选方式就是使用完全引用,包括程序集名称、版本、区域性和公钥标记(如果存在)。
运行库就会使用这些信息来定位程序集。无论是对静态程序集的引用还是对动态程序集的引用,运行库均使用相同的解析过程。 还可通过向调用方法仅提供有关程序集的部分信息的方式(例如仅指定程序集名称),对程序集进行动态引用。在这种情况下,仅在应用程序目录下搜索程序集,不进行其他检查。您可以使用不同加载程序集方法中的任何方法(例如 System.Reflection.Assembly.Load 或 AppDomain.Load)进行部分引用。如果希望运行库在全局程序集缓存和应用程序目录下检查引用的程序集,可以用 System.Reflection.Assembly.LoadWithPartialName 方法指定部分引用。
最后,可以使用诸如 System.Reflection.Assembly.Load 之类的方法进行动态引用并只提供部分信息;然后在应用程序配置文件中用 <qualifyAssembly> 元素限定该引用。该元素使您可以在应用程序配置文件中而不是在代码中提供完全引用信息,包括名称、版本、区域性和公钥标记(如果适用)。如果要在应用程序目录外完全限定对某个程序集的引用,或者如果要引用全局程序集缓存中的程序集,但又希望方便地在配置文件中而不是在代码中指定完全引用,就可以采用这一技术。
10、GC
一个跟踪过程,它传递性地跟踪指向当前使用的对象的所有指针,以便找到可以引用的所有对象,然后重新使用在此跟踪过程中未找到的任何堆内存。公共语言运行库垃圾回收器还压缩使用中的内存,以缩小堆所需要的工作空间。 垃圾收集器的基本算法很简单: ● 将所有的托管内存标记为垃圾 ● 寻找正被使用的内存块,并将他们标记为有效 ● 释放所有没有被使用的内存块 ● 整理堆以减少碎片。
CLR的基本特性
1、与本机代码无关 - MSIL (中间语言)
2、让我们使用同一种语言 - CLR (公共语言运行时)
3、我们手中的零件 - Assembly (装配件)
4、让我们在同一个系统中运行 - CTS (通用类型系统)
5、宇宙大爆炸后的产物 - metadata (元数据)
6、让我们的语言可以交流 - CLS (公共语言系统)
7、在动态中交互 - Reflection (反射)
8、属于我们自己的空间 - NameSpace (名称空间)
MSIL:微软中间语言 |
Reflection:反射 |
Metadata:元数据 |
PE:可执行可移植文件 |
Assembly: 程序集(装配件) |
NameSpace:名称空间 |
CTS:通用类型系统 |
GC(Garbage Colection) :无用单元回收 |
CLR:公共语言系统 |
Attribute:属性(注意不要和Property混淆) |
Boxing: 装箱 |
UnBoxing: 拆箱 |
MSIL 使用.NET支持的语言所编写的代码
MSIL(Microsoft Intermediate Language)微软的中间语言。和JAVA的虚拟机类似,是与CPU无关的指令集。当编译为托管代码时,编译器将源代码翻译为MSIL, 如上图所示。MSIL包括用于加载、存储和初始化对象以及对对象调用方法的指令,还包括用于算术和逻辑运算、控制流、直接内存访问、异常处理和其他操作的指令。在可以执行代码前,必须将 MSIL 转换为 CPU 特定的代码,这通常是通过实时 (JIT) 编译器完成的。由于公共语言运行库为它支持的每种计算机结构都提供了一种或多种 JIT 编译器,因此可以在任何受支持的结构上对同一组 MSIL 进行 JIT 编译和执行。这样总结上面的就是:中间语言是一组独立于CPU的指令集,它可以被即时编译器Jitter翻译成目标平台的本地代码。
PE
Windows PE和一个 .NET PE的主要区别在于Windows PE是由操作系统执行的,而 .NET PE 却被转变成为.NET Framework的CLR. 识别一个PE是 .NET还是Windows取决于他的通用的目标文件格式 (COFF) 是否使用Windows的操作系统。目标文件格式 (COFF) 指定了任何文件都分成两个部分:文件数据本身以及描述文件内包含的数据内容的头文件串。MSIL 汇编程序从 MSIL 汇编语言生成可移植可执行的 (PE) 文件。可以运行结果可执行文件(该文件包含 MSIL 和所需的元数据)以确定 MSIL 是否按预期执行。这就是我为什么会谈到PE。
那么PE文件是怎么执行的呢?下面是一个典型的.NET应用程序的执行过程:
1. 用户执行编译器输出的应用程序(PE文件),操作系统载入PE文件,以及其他的DLL(.NET动态连接库)。
2. 操作系统装载器根据PE文件中的可执行文件头跳转到程序的入口点。显然,操作系统并不能执行中间语言,该入口点也被设计为跳转到mscoree.dll(.NET平台的核心支持DLL)的_CorExeMain()函数入口。
3. CorExeMain()函数开始执行PE文件中的中间语言代码。这里的执行的意思是CLR(通用语言运行时)按照调用的对象方法为单位,用JIT(即时编译器)将中间语言编译成本地机二进制代码,执行并根据需要存于机器缓存。
4. 程序的执行过程中,GC(垃圾收集器)负责内存的分配,释放等管理功能。
5. 程序执行完毕,操作系统卸载应用程序。
.NET Framework 环境
下面的插图显示公共语言运行时和类库与应用程序之间以及与整个系统之间的关系。该插图还显示托管代码如何在更大的结构内运行。
公共语言运行时(CLR)的功能
公共语言运行时管理内存、线程执行、代码执行、代码安全验证、编译以及其他系统服务。这些功能是在公共语言运行时上运行的托管代码所固有的。
-
至于安全性,取决于包括托管组件的来源(如 Internet、企业网络或本地计算机)在内的一些因素,托管组件被赋予不同程度的信任。
-
运行时强制实施代码访问安全。例如,用户可以相信嵌入在网页中的可执行文件能够在屏幕上播放动画或唱歌,但不能访问他们的个人数据、文件系统或网络。这样,运行时的安全性功能就使通过 Internet 部署的合法软件能够具有特别丰富的功能。
-
运行时还通过实现称为常规类型系统 (CTS) 的严格类型验证和代码验证基础结构来加强代码可靠性。CTS 确保所有托管代码都是可以自我描述的。各种 Microsoft 编译器和第三方语言编译器都可生成符合 CTS 的托管代码。这意味着托管代码可在严格实施类型保证和类型安全的同时使用其他托管类型和实例。
-
此外,运行时的托管环境还消除了许多常见的软件问题。例如,运行时自动处理对象布局并管理对对象的引用,在不再使用它们时将它们释放。这种自动内存管理解决了两个最常见的应用程序错误:内存泄漏和无效内存引用。
-
运行时还提高了开发人员的工作效率。例如,程序员可以用他们选择的开发语言编写应用程序,却仍能充分利用其他开发人员用其他语言编写的运行时、类库和组件。任何选择以运行时为目标的编译器供应商都可以这样做。以 .NET Framework 为目标的语言编译器使得用该语言编写的现有代码可以使用 .NET Framework 的功能,这大大减轻了现有应用程序的迁移过程的工作负担。
- 尽管运行时是为未来的软件设计的,但是它也支持现在和以前的软件。托管和非托管代码之间的互操作性使开发人员能够继续使用所需的 COM 组件和 DLL。
- 运行时旨在增强性能。尽管公共语言运行时提供许多标准运行时服务,但是它从不解释托管代码。一种称为实时 (JIT) 编译的功能使所有托管代码能够以它在其上执行的系统的本机语言运行。同时,内存管理器排除了出现零碎内存的可能性,并增大了内存引用区域以进一步提高性能。
- 最后,运行时可由高性能的服务器端应用程序(如 Microsoft SQL Server 和 Internet 信息服务 (IIS))承载。此基础结构使您在享受支持运行时承载的行业最佳企业服务器的优越性能的同时,能够使用托管代码编写业务逻辑。
公共语言运行时(CLR)细节
- 若要使公共语言运行时能够向托管代码提供服务,语言编译器必须生成一些元数据来描述代码中的类型、成员和引用。元数据与代码一起存储;每个可加载的公共语言运行时可迁移执行 (PE) 文件都包含元数据。公共语言运行时使用元数据来完成以下任务:查找和加载类,在内存中安排实例,解析方法调用,生成本机代码,强制安全性,以及设置运行时上下文边界。
- 公共语言运行时自动处理对象布局并管理对象引用,当不再使用对象时释放它们。按这种方式实现生存期管理的对象称为托管数据。垃圾回收消除了内存泄漏以及其他一些常见的编程错误。如果您编写的代码是托管代码,则可以在 .NET Framework 应用程序中使用托管数据、非托管数据或者同时使用这两种数据。由于语言编译器会提供自己的类型(如基元类型),因此您可能并不总是知道(或需要知道)这些数据是否是托管的。
- 有了公共语言运行时,就可以很容易地设计出对象能够跨语言交互的组件和应用程序。也就是说,用不同语言编写的对象可以互相通信,并且它们的行为可以紧密集成。例如,可以定义一个类,然后使用不同的语言从原始类派生出另一个类或调用原始类的方法。还可以将一个类的实例传递到用不同的语言编写的另一个类的方法。这种跨语言集成之所以成为可能,是因为基于公共语言运行时的语言编译器和工具使用由公共语言运行时定义的常规类型系统(CTS),而且它们遵循公共语言运行时关于定义新类型以及创建、使用、保持和绑定到类型的规则。
- 所有托管组件都带有生成它们所基于的组件和资源的信息,这些信息构成了元数据的一部分。公共语言运行时使用这些信息确保组件或应用程序具有它需要的所有内容的指定版本,这样就使代码不太可能由于某些未满足的依赖项而发生中断。注册信息和状态数据不再保存在注册表中(因为在注册表中建立和维护这些信息很困难)。取而代之的是,有关您定义的类型(及其依赖项)的信息作为元数据与代码存储在一起,这样大大降低了组件复制和移除任务的复杂性。
- 语言编译器和工具公开公共语言运行时的功能的方式对于开发人员来说不仅很有用,而且很直观。这意味着,公共语言运行时的某些功能可能在一个环境中比在另一个环境中更突出。您对公共语言运行时的体验取决于所使用的语言编译器或工具。例如,如果您是一位 Visual Basic 开发人员,您可能会注意到:有了公共语言运行时,Visual Basic 语言的面向对象的功能比以前多了。
.NET Framework 类库
- .NET Framework 类库是一个与公共语言运行时紧密集成的可重用的类型集合。该类库是面向对象的,并提供您自己的托管代码可从中导出功能的类型。这不但使 .NET Framework 类型易于使用,而且还减少了学习 .NET Framework 的新功能所需要的时间。此外,第三方组件可与 .NET Framework 中的类无缝集成。