读《CLR via C# 》之后感觉对.net 的理解有了一个飞跃(虽然大部分内容还要慢慢消化和吸收),对CLR,JIT编译器,垃圾回收,多线程使用,和锁等概念都有了更深的认识。我从四月末开始看,直到八月初才看完,有几节跳过了,像WinRT 觉得过时了就没看,毕竟Microsoft 目前大力推广.net core。能让我坚持看完的原因很大一部分是作者 Jeffrey 的文字平易近人,描述细腻,让人感觉作者确实是想把毕生所学倾囊相授。就像译者周靖在结尾评价的那样:"字里行间,全是殷勤叮嘱。无浮夸之文字,倾心血而写就"。
以后我将逐步整理看完此书的习得,也是对书中难理解的地方做一下回顾。
第一篇 CLR 基础
1 将源代码编译成托管模块
公共语言运行时(Common Language Runtime ,CLR) 是一个可由多种编程语言使用的“运行时”。CLR的核心功能(比如内存管理、程序集加载、安全性、异常处理和线程同步)可由面向CLR的所有语言使用。
只要编译器是面向CLR的,可以用任何语言编译出托管模块。
托管模块 是标准的32位Microsoft Windows 可移植执行体(PE32)文件,或者是标准的64位Windows可移植执行体(PE32+)文件,它们都需要CLR才能执行。
元数据: 每个托管模块都包含元数据表,主要有两种表:一种表描述源代码种定义的类型和成员,另一种描述源代码引用的类型和程序。简单的说就是一个数据表集合。元数据是一些老技术的超集,这些老技术包括COM的”类型库“,和“接口定义语言“(IDL)文件。但CLR元数据远比它们全面。元数据与类型库和IDL同元数据总是和包含IL代码的文件关联。编译器会同时生成元数据和代码,把它们绑定在一起,最终生成在托管模块。
元数据的用途:
1 元数据避免了编译时对原生C/C++头文件和库文件的需求,因为这些头文件和库已经包含在了元数据中。
2 帮你写代码,给你提示一个类型提供哪些方法、属性等。
3 CLR的代码验证过程使用元数据确保代码只执行”类型安全“的操作。
4 元数据允许将对象的字段序列化到内存块,将其发送给另一台机器,然后反序列化,在另一台机器重建对象状态。
5 元数据允许垃圾回收器跟踪对象生存期,垃圾回收器能判断元数据中引用了哪些对象。
每个面向CLR的编译器生成的都是IL(中间语言)代码。IL代码有时称为托管代码,因为CLR管理它的执行。
2 生成、打包、部署、和管理应用程序及类型
用户要想执行托管代码以及托管数据的模块,必须在自己的计算机上安装好CLR(目前作为.net Framework 的一部分提供)。
Microsoft 的C++编译器默认生成包含非托管代码的EXE/DLL模块,也可以指定/CLR命令,让C++编译出托管代码模块。
3 将托管模块合并成程序集
CLR实际不和模块工作,他和程序集工作。
程序集是一个或多个模块/资源文件的逻辑性分组。其次,程序集是重用、安全性以及版本控制的最小单元。在CLR的世界中,程序集相当于”组件“。利用程序集这种概念性的东西,一组文件可作为一个单独的实体来对待。
程序集包含什么?
1 托管代码和文件清单,编译器生成的是含有清单的托管代码;
2 包含与引用的程序集有关的信息(包括它们的版本号)。让程序集更容易部署。
3 加载公共语言运行时
windows检查EXE文件头,决定是创建32位还是64位进程之后,会在进程地址空间加载MSCorEE.dll的x86、x64或者ARM版本。
如果是Windows的x86或者ARM版本,MSCorEE.dll的x86版本在%SystemRoot%\System32 目录中。如果是Windows的x64版本,MSCorEE.dll的x86版本在%SystemRoot%\SysWow64目录中,64位版本则在%SystemRoot%\System32目录中(为了向后兼容)。
然后,进程的主线程调用MSCorEE.dll中定义的一个方法。这个方法初始化CLR,加载EXE程序集,在调用其入口方法(main)。随即,托管应用程序启动并运行。
4 执行程序集中的代码
托管程序集同时包括元数据和IL。IL能访问和操作 对象类型,并提供了指令来创建和初始化对象、调用对象上的虚方法以及直接操作数组元素。甚至提供了抛出和捕获异常的指令来实现错误处理,可以将IL视为一种面向对象的机器语言。
IL也是一种计算机语言,和其他任何机器语言一样,IL也能使用汇编语言编写。
高级语言(C#、C++、python)通常只公开了CLR全部功能的一个子集。然而,IL汇编语言允许开发人员访问CLR的全部功能。
为了执行方法,首先必须把方法的IL转换成本机CPU指令,这是CLR的JIT编译器的职责。
在Main方法调用前,CLR会检测出Main的代码所引用的所有类型,这导致CLR分配一个内部数据结构来管理对引用类型的访问。
图1-4中的Main方法引用了一个Console类型,导致CLR分配一个内部数据结构。在这个数据结构中,Console类型定义的每个方法都会有一个对应的entry,每个entry都含有一个地址,根据此地址即可找到方法的实现。
对这个内部数据结构初始化时,CLR会将每个entry都指向包含在CLR内部的一个未编档函数。称为JITCompiler。
JITCompiler函数负责将方法的IL代码编译成本机CPU指令。由于IL是”即时 “编译的,所以通常将CLR的这个组件称为JITter或者JIT编译器。
JITCompiler执行过程
JITCompiler 函数被调用时,他知道要调用的是哪个方法,以及具体是什么类型定义了该方法。
然后JITCompiler会在定义(该类型的)程序集的元数据中查找被调用方法的IL。
JITCompiler验证IL代码,并将IL代码编译成本机CPU指令。本机CPU指令保存到动态分配的内存块中。
然后JITCompiler回到CLR为类型创建的内部数据结构中,找到与被调用方法对应的那条记录(entry),修改最初对JITCompiler的引用,使其指向内存块(刚刚编译好的存储在内存块中的CPU指令)地址。
最后JITCompiler 函数跳转到内存块中的代码,这些代码正是WriteLine方法的具体实现。代码执行完毕返回时,会回到Main中的代码,并继续执行。
Main要第二次调用WriteLine时,由于已对WrtieLine的代码进行了验证和编译,所以会直接执行内存块中的代码,完全跳过JITCompiler函数。
编译器优化
CLR的JIT编译器会对本机代码进行优化,类似于非托管代码C++编译器的后端所作的事情。
托管代码相较于非托管代码的优势
1 JIT编译器能判断应用程序是否运行在Intel Pentium 4CPU上,并生成相应的本机代码来利用Pentium 4 支持的任何特殊指令。非托管的程序是针对具有最小功能集合的CPU编译的,不会使用能提升性能的特殊指令。
2 JIT 编译器能判断一个特定的测试在它运行的机器上是否总是失败。
3 应用程序运行时,CLR可以评估代码的执行,并将IL重新编译成本机代码。重新编译的代码可以重新组织,根据刚才观察到的执行模式,减少不正确的分支预测。虽然目前版本的CLR还不能做到这一点,但将来的版本也许就可以了。
4 .net Framework SDK 配套提供的NGen.exe工具。该工具将程序集的所有IL代码编译成本机代码,并将这些本机代码保存到一个磁盘文件中。在运行时加载程序集时,CLR会自动判断是否存在该程序集的预编译版本。
5 还可以考虑使用 System.Runtime.ProfileOptimization 类,该类导致CLR检查程序运行时哪些方法被JIT编译,结果被记录到一个文件中。程序再次启动时,如果是再多CPU机器上运行,就用其他线程并发编译这些方法。
4 IL和验证
IL 基于栈,这意由于味着他的所有指令都要将操作数压入一个执行栈,并从栈弹出结果。IL没有操作寄存器的指令,人们可以容易的创建新的语言和编译器,生成面向CLR的代码。
IL指令是无类型的。
作者认为,IL最大的优势不是它对底层CPU的抽象,而是应用程序的健壮性和安全性。将IL 编译生成本机CPU指令时,CLR执行一个名为验证的过程。托管模块的元数据包含验证过程要用到的所有方法及类型信息。
5 不安全的代码
Microsoft C# 编译器默认生成安全(safe)代码,这种代码的安全性可以验证,然而,Microsoft C# 编译器也允许开发人员写不安全的代码。
这个强大的功能,通常只有在与非托管代码进行互操作,或者提升对效率要求极高的一个算法的性能的时候,才需要这样做。
PEVerify.exe 应用程序检查一个程序集的所有方法,并报告其中不安全代码的方法。可以对想要引用的程序集运行一下PEVerify.exe,看看应用程序在通过内网或Internet运行时是否会出现问题。
6 本机代码生成器
使用.NET Framework 提供的NGen.exe工具,可以在应用程序安装到用户的计算机上时,将IL代码编译成本机提供的代码。这样CLR的JIT编译器不需要再运行时编译IL代码,有助于提升应用程序的性能。
1 可以提高应用程序的启动速度
2 减小应用程序的工作集,
工作集 是指在进程的所有内存中,已映射的物理内存那一部分。
进程还有一部分虚拟内存,CPU不能直接访问虚拟内存,需要Windows映射之后才能访问。还有一部分内存在磁盘上的分页文件里。
NGen.exe 的弊端
1 没有知识产权保护。
2 NGen.exe生成的文件可能失去同步。
3 较差的执行时性能。NGen不能优化的使用特定CPU指令。
6 Framework 类库
FCL (Framework Class Library) Framework 类库。
WCF (Windows Communication Foundation)技术,可以非常简单的处理通过Internet发送的消息。
7 通用类型系统 Common Type System
CLR一切都围绕类型展开,类型向应用程序和其他类型公开了功能
。通过类型,用一种编程语言写的代码能与另一种编程语言写的代码沟通。
CTS规范规定,一个类型可以包含零个或者多个成员。
字段 Field
方法 Method
属性 Property
对于调用者,属性看起来像是字段。但是对于类型的实现者,属性看起来像是一个方法,或者两个方法。
事件 Event
事件在对象以及其他对象之间实现了通知机制。
CTS还指定了类型可见性规则以及类型成员的访问规则。
private
family(C++和C#)都用protected修饰符来标识family。
family and assembly
成员可由派生类型访问,但是这些派生类型必须在同一个程序集中定义。
assembly (许多语言都用internal 修饰符来标识)
成员可由同一个程序集中的任何代码访问。
family or assembly
成员可由任何程序集中的派生类型访问,成员也可由同一个程序集中的任何类型访问。C#用 protected internal 修饰符来标识 family or assembly。
public
CTS规定:所有类型最终必须从预定义的System.Obejct类型继承. 具体的讲,System.Object类型允许做下面这些事情:
1 比较两个实例的相等性.
2 获取实例的哈希码.
3 查询一个实例的真正类型.
4 执行实例的浅拷贝.
5 获取实例对象当前状态的字符串表示.
8 公共语言规范
不同语言创建的对象可通过COM互相通信. CLR集成了所有语言, 用一种语言创建的对象在另一种语言中, 和用后者创建的对象有相同的地位. 之所以能实现这样的集成, 是因为CLR 使用了标准类型集, 元数据 , 以及公共执行环境.
Common Language Specification (CLS), 它详细定义了一个最小功能集.
9 与非托管代码的互操作性
CLR支持三种操作情形.
1 托管代码能调用DLL中的非托管代码.
2 托管代码可以使用现有COM组件(服务器)
3 非托管代码可以使用托管类型(服务器)