首页 技术 正文
技术 2022年11月10日
0 收藏 495 点赞 4,699 浏览 10624 个字

什么是CLR,.NET虚拟机?

实际上,.NET不仅提供了自动内存管理的支持,他还提供了一些列的如类型安全、应用程序域、异常机制等支持,这些 都被统称为CLR公共语言运行库。

CLR是.NET类型系统的基础,所有的.NET技术都是建立在此之上,熟悉它可以帮助我们更好的理解框架组件的核心、原理。
在我们执行托管代码之前,总会先运行这些运行库代码,通过运行库的代码调用,从而构成了一个用来支持托管程序的运行环境,进而完成诸如不需要开发人员手动管理内存,一套代码即可在各大平台跑的这样的操作。

这套环境及体系之完善,以至于就像一个小型的系统一样,所以通常形象的称CLR为”.NET虚拟机”。那么,如果以进程为最低端,进程的上面就是.NET虚拟机(CLR),而虚拟机的上面才是我们的托管代码。换句话说,托管程序实际上是寄宿于.NET虚拟机中。

什么是CLR宿主进程,运行时主机?

那么相对应的,容纳.NET虚拟机的进程就是CLR宿主进程了,该程序称之为运行时主机。

这些运行库的代码,全是由C/C++编写,具体表现为以mscoree.dll为代表的核心dll文件,该dll提供了N多函数用来构建一个CLR环境 ,最后当运行时环境构建完毕(一些函数执行完毕)后,调用_CorDllMain或_CorExeMain来查找并执行托管程序的入口方法(如控制台就是Main方法)。

如果你足够熟悉CLR,那么你完全可以在一个非托管程序中通过调用运行库函数来定制CLR并执行托管代码。
像SqlServer就集成了CLR,可以使用任何 .NET Framework 语言编写存储过程、触发器、用户定义类型、用户定义函数(标量函数和表值函数)以及用户定义的聚合函数。

有关CLR大纲介绍: https://msdn.microsoft.com/zh-cn/library/9x0wh2z3(v=vs.85).aspx 
CLR集成: https://docs.microsoft.com/zh-cn/previous-versions/sql/sql-server-2008/ms131052(v%3dsql.100) 
构造CLR的接口:https://msdn.microsoft.com/zh-cn/library/ms231039(v=vs.85).aspx 
适用于 .NET Framework 2.0 的宿主接口:https://msdn.microsoft.com/zh-cn/library/ms164336(v=vs.85).aspx
选择CLR版本: https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/startup/supportedruntime-element

所以C#编写的程序如果想运行就必须要依靠.NET提供的CLR环境来支持。 而CLR是.NET技术框架中的一部分,故只要在Windows系统中安装.NET Framework即可。

Windows系统自带.NET Framework

Windows系统默认安装的有.NET Framework,并且可以安装多个.NET Framework版本,你也不需要因此卸载,因为你使用的应用程序可能依赖于特定版本,如果你移除该版本,则应用程序可能会中断。

Microsoft .NET Framework百度百科下有windows系统默认安装的.NET版本

.NET、NET Framewor以及.NET Core的关系(二)

如何同时调用两个两个相同命名空间和类型的程序集?

除了程序集版本不同外,还有一种情况就是,我一个项目同时引用了程序集A和程序集B,但程序集A和程序集B中的命名空间和类型名称完全一模一样,这个时候我调用任意一个类型都无法区分它是来自于哪个程序集的,那么这种情况我们可以使用extern alias外部别名。
我们需要在所有代码前定义别名,extern alias a;extern alias b;,然后在VS中对引用的程序集右键属性-别名,分别将其更改为a和b(或在csc中通过/r:{别名}={程序集}.dll)。
在代码中通过 {别名}::{命名空间}.{类型}的方式来使用。
extern-alias介绍: https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/extern-alias

共享程序集GAC

我上面说了这么多有关CLR加载程序集的细节和规则,事实上,类似于mscorlib.dll、System.dll这样的FCL类库被引用的如此频繁,它已经是我们.NET编程中必不可少的一部分,几尽每个项目都会引用,为了不再每次使用的时候都复制一份,所以计算机上有一个位置专门存储这些我们都会用到的程序集,叫做全局程序集缓存(Global Assembly Cache,GAC),这个位置一般位于C:\Windows\Microsoft.NET\assembly和3.5之前版本的C:\Windows\assembly。
既然是共享存放的位置,那不可避免的会遇到文件名重复的情况,那么为了杜绝该类情况,规定在GAC中只能存在强名称程序集,每当CLR要加载强名称程序集时,会先通过标识去GAC中查找,而考虑到程序集文件名称一致但版本文化等复杂的情况,所以GAC有自己的一套目录结构。我们如果想将自己的程序集放入GAC中,那么就必须先签名,然后通过如gacutil.exe工具(其存在于命令行工具中 https://docs.microsoft.com/zh-cn/dotnet/framework/tools/developer-command-prompt-for-vs中)来注册至GAC中,值得一提的是在将强名称程序集安装在GAC中,会效验签名。

GAC工具: https://docs.microsoft.com/en-us/dotnet/framework/tools/gacutil-exe-gac-tool

延伸

CLR是按需加载程序集的,没有执行代码也就没有调用相应的指令,没有相应的指令,CLR也不会对其进行相应的操作。 当我们执行Environment.CurrentDirectory这段代码的时候,CLR首先要获取Environment类型信息,通过自身元数据得知其存在mscorlib.dll程序集中,所以CLR要加载该程序集,而mscorlib.dll又由于其地位特殊,早在CLR初始化的时候就已经被类型加载器自动加载至内存中,所以这行代码可以直接在内存中读取到类型的方法信息。
在这个章节,我虽然描述了CLR搜索程序集的规则,但事实上,加载程序集读取类型信息远远没有这么简单,这涉及到了属于.NET Framework独有的”应用程序域”概念和内存信息的查找。

简单延伸两个问题,mscorlib.dll被加载在哪里?内存堆中又是什么样的一个情况?

应用程序域

传统非托管程序是直接承载在Windows进程中,托管程序是承载在.NET虚拟机CLR上的,而在CLR中管控的这部分资源中,被分成了一个个逻辑上的分区,这个逻辑分区被称为应用程序域,是.NET Framework中定义的一个概念。
因为堆内存的构建和删除都通过GC去托管,降低了人为出错的几率,在此特性基础上.NET强调在一个进程中通过CLR强大的管理建立起对资源逻辑上的隔离区域,每个区域的应用程序互不影响,从而让托管代码程序的安全性和健壮性得到了提升。

熟悉程序集加载规则和AppDomain是在.NET技术下进行插件编程的前提。AppDomain这部分概念并不复杂。
当启动一个托管程序时,最先启动的是CLR,在这过程中会通过代码初始化三个逻辑区域,最先是SystemDomain系统程序域,然后是SharedDoamin共享域,最后是{程序集名称}Domain默认域。

系统程序域里维持着一些系统构建项,我们可以通过这些项来监控并管理其它应用程序域等。共享域存放着其它域都会访问到的一些信息,当共享域初始化完毕后,会自动加载mscorlib.dll程序集至该共享域。而默认域则用储存自身程序集的信息,我们的主程序集就会被加载至这个默认域中,执行程序入口方法,在没有特殊动作外所产生的一切耗费都发生在该域。

我们可以在代码中创建和卸载应用程序域,域与域之间有隔离性,挂掉A域不会影响到B域,并且对于每一个加载的程序集都要指定域的,没有在代码中指定域的话,默认都是加载至默认域中。
AppDomain可以想象成组的概念,AppDomain包含了我们加载的一组程序集。我们通过代码卸载AppDomain,即同时卸载了该AppDomain中所加载的所有程序集在内存中的相关区域。

AppDomain的初衷是边缘隔离,它可以让程序不重新启动而长时间运行,围绕着该概念建立的体系从而让我们能够使用.NET技术进行插件编程。

当我们想让程序在不关闭不重新部署的情况下添加一个新的功能或者改变某一块功能,我们可以这样做:将程序的主模块仍默认加载至默认域,再创建一个新的应用程序域,然后将需要更改或替换的模块的程序集加载至该域,每当更改和替换的时候直接卸载该域即可。 而因为域的隔离性,我在A域和B域加载同一个程序集,那么A域和B域就会各存在内存地址不同但数据相同的程序集数据。

跨边界访问

事实上,在开发中我们还应该注意跨域访问对象的操作(即在A域中的程序集代码直接调用B域中的对象)是与平常编程中有所不同的,一个域中的应用程序不能直接访问另一个域中的代码和数据,对于这样的在进程内跨域访问操作分两类。

一是按引用封送,需要继承System.MarshalByRefObject,传递的是该对象的代理引用,与源域有相同的生命周期。
二是按值封送,需要被[Serializable]标记,是通过序列化传递的副本,副本与源域的对象无关。
无论哪种方式都涉及到两个域直接的封送、解封,所以跨域访问调用不适用于过高频率。
(比如,原来你是这样调用对象: var user=new User(); 现在你要这样:var user=(User){应用程序域对象实例}.CreateInstanceFromAndUnwrap(“Model.dll”,”Model.User”); )

值得注意的是,应用程序域是对程序集的组的划分,它与进程中的线程是两个一横一竖,方向不一样的概念,不应该将这2个概念放在一起比较。我们可以通过Thread.GetDomain来查看执行线程所在的域。
应用程序域在类库中是System.AppDomain类,部分重要的成员有:

        获取当前 System.Threading.Thread 的当前应用程序域        public static AppDomain CurrentDomain { get; }
       使用指定的名称新建应用程序域        public static AppDomain CreateDomain(string friendlyName);
       卸载指定的应用程序域。        public static void Unload(AppDomain domain);
       指示是否对当前进程启用应用程序域的 CPU 和内存监视,开启后可以根据相关属性进行监控        public static bool MonitoringIsEnabled { get; set; }
       当前域托管代码抛出异常时最先发生的一个事件,框架设计中可以用到        public event EventHandler<FirstChanceExceptionEventArgs> FirstChanceException;
       当某个异常未被捕获时调用该事件,如代码里只catch了a异常,实际产生的是 b异常,那么b异常就没有捕捉到。        public event UnhandledExceptionEventHandler UnhandledException;
       为指定的应用程序域属性分配指定值。该应用程序域的局部存储值,该存储不划分上下文和线程,均可通过GetData获取。        public void SetData(string name, object data);
       如果想使用托管代码来覆盖CLR的默认行为https://msdn.microsoft.com/zh-cn/library/system.appdomainmanager(v=vs.85).aspx
public AppDomainManager DomainManager { get; }
       返回域的配置信息,如在config中配置的节点信息        public AppDomainSetup SetupInformation { get; }

应用程序域: https://docs.microsoft.com/zh-cn/dotnet/framework/app-domains/application-domains

AppDomain和AppPool

注意:此处的AppDomain应用程序域 和 IIS中的AppPool应用程序池 是2个概念,AppPool是IIS独有的概念,它也相当于一个组的概念,对网站进行划组,然后对组进行一些如进程模型、CPU、内存、请求队列的高级配置。

内存

应用程序域把资源给隔离开,这个资源,主要指内存。那么什么是内存呢?

要知道,程序运行的过程就是电脑不断通过CPU进行计算的过程,这个过程需要读取并产生运算的数据,为此我们需要一个拥有足够容量能够快速与CPU交互的存储容器,这就是内存了。对于内存大小,32位处理器,寻址空间最大为2的32次方byte,也就是4G内存,除去操作系统所占用的公有部分,进程大概能占用2G内存,而如果是64位处理器,则是8T。

而在.NET中,内存区域分为堆栈和托管堆。

堆栈和堆的区别

堆和堆栈就内存而言只不过是地址范围的区别。不过堆栈的数据结构和其存储定义让其在时间和空间上都紧密的存储,这样能带来更高的内存密度,能在CPU缓存和分页系统表现的更好。故而访问堆栈的速度总体来说比访问堆要快点。

线程堆栈

操作系统会为每条线程分配一定的空间,Windwos为1M,这称之为线程堆栈。在CLR中的栈主要用来执行线程方法时,保存临时的局部变量和函数所需的参数及返回的值等,在栈上的成员不受GC管理器的控制,它们由操作系统负责分配,当线程走出方法后,该栈上成员采用后进先出的顺序由操作系统负责释放,执行效率高。
而托管堆则没有固定容量限制,它取决于操作系统允许进程分配的内存大小和程序本身对内存的使用情况,托管堆主要用来存放对象实例,不需要我们人工去分配和释放,其由GC管理器托管。

为什么值类型存储在栈上

不同的类型拥有不同的编译时规则和运行时内存分配行为,我们应知道,C# 是一种强类型语言,每个变量和常量都有一个类型,在.NET中,每种类型又被定义为值类型或引用类型。

使用 struct、enum 关键字直接派生于System.ValueType定义的类型是值类型,使用 class、interface、delagate 关键字派生于System.Object定义的类型是引用类型。
对于在一个方法中产生的值类型成员,将其值分配在栈中。这样做的原因是因为值类型的值其占用固定内存的大小。

C#中int关键字对应BCL中的Int32,short对应Int16。Int32为2的32位,如果把32个二进制数排列开来,我们要求既能表达正数也能表达负数,所以得需要其中1位来表达正负,首位是0则为+,首位是1则为-,那么我们能表示数据的数就只有31位了,而0是介于-1和1之间的整数,所以对应的Int32能表现的就是2的31次方到2的31次方-1,即2147483647和-2147483648这个整数段。

1个字节=8位,32位就是4个字节,像这种以Int32为代表的值类型,本身就是固定的内存占用大小,所以将值类型放在内存连续分配的栈中。

托管堆模型

而引用类型相比值类型就有点特殊,newobj创建一个引用类型,因其类型内的引用对象可以指向任何类型,故而无法准确得知其固定大小,所以像对于引用类型这种无法预知的容易产生内存碎片的动态内存,我们把它放到托管堆中存储。

托管堆由GC托管,其分配的核心在于堆中维护着一个nextObjPtr指针,我们每次实例(new)一个对象的时候,CLR将对象存入堆中,并在栈中存放该对象的起始地址,然后该指针都会根据该对象的大小来计算下一个对象的起始地址。不同于值类型直接在栈中存放值,引用类型则还需要在栈中存放一个代表(指向)堆中对象的值(地址)。

而托管堆又可以因存储规则的不同将其分类,托管堆可以被分为3类:

  • 1.用于托管对象实例化的垃圾回收堆,又以存储对象大小分为小对象(<85000byte)的GC堆(SOH,Small Object Heap)和用于存储大对象实例的(>=85000byte)大对象堆(LOG,Larage Object Heap)。

  • 2.用于存储CLR组件和类型系统的加载(Loader)堆,其中又以使用频率分为经常访问的高频堆(里面包含有MethodTables方法表, MeghodDescs方法描述, FieldDescs方法描述和InterfaceMaps接口图),和较低的低频堆,和Stub堆(辅助代码,如JIT编译后修改机器代码指令地址环节)。

  • 3.用于存储JIT代码的堆及其它杂项的堆。

加载程序集就是将程序集中的信息给映射在加载堆,对产生的实例对象存放至垃圾回收堆。前文说过应用程序域是指通过CLR管理而建立起的逻辑上的内存边界,那么每个域都有其自己的加载堆,只有卸载应用程序域的时候,才会回收该域对应的加载堆。

而加载堆中的高频堆包含的有一个非常重要的数据结构表—方法表,每个类型都仅有一份方法表(MethodTables),它是对象的第一个实例创建前的类加载活动的结果,它主要包含了我们所关注的3部分信息:

  • 1包含指向EEClass的一个指针。EEClass是一个非常重要的数据结构,当类加载器加载到该类型时会从元数据中创建出EEClass,EEClass里主要存放着与类型相关的表达信息。

  • 2包含指向各自方法的方法描述器(MethodDesc)的指针逻辑组成的线性表信息:继承的虚函数, 新虚函数, 实例方法, 静态方法。

  • 3包含指向静态字段的指针。

那么,实例一个对象,CLR是如何将该对象所对应的类型行为及信息的内存位置(加载堆)关联起来的呢?

原来,在托管堆上的每个对象都有2个额外的供于CLR使用的成员,我们是访问不到的,其中一个就是类型对象指针,它指向位于加载堆中的方法表从而让类型的状态和行为关联了起来, 类型指针的这部分概念我们可以想象成obj.GetType()方法获得的运行时对象类型的实例。而另一个成员就是同步块索引,其主要用于2点:1.关联内置SyncBlock数组的项从而完成互斥锁等目的。 2.是对象Hash值计算的输入参数之一。

.NET、NET Framewor以及.NET Core的关系(二)

如图,我当前登录账号名称为DemoXiaoZeng,然后通过Thread.CurrentPrincipal设置当前主体,执行aa方法,顺利打印111。如果检测到PrincipalPermission类中的Name属性值不是当前登录账号,那么就报错:对主体权限请求失败。

.NET、NET Framewor以及.NET Core的关系(二)

.NET、NET Framewor以及.NET Core的关系(二)

当然,VS还有其它强大的功能,我建议大家依次点完 菜单项中的 调试、体系结构、分析这三个大菜单里面的所有项,你会发现VS真是一个强大的IDE。比较实用且方便的功能举几个例子:

比如 从代码生成的序列图,该功能在vs2015之前的版本可以找到(https://msdn.microsoft.com/en-us/library/dd409377.aspx 、https://www.zhihu.com/question/36413876)

.NET、NET Framewor以及.NET Core的关系(二)

比如 模块关系的代码图,可以看到各模块间的关系

.NET、NET Framewor以及.NET Core的关系(二)

比如 对解决方案的代码度量分析结果

比如 调试状态下 .NET、NET Framewor以及.NET Core的关系(二)

函数调用的 代码图,我们可以看到MVC框架的函数管道模型

.NET、NET Framewor以及.NET Core的关系(二)

以及并行堆栈情况、加载的模块、线程的实际情况

.NET、NET Framewor以及.NET Core的关系(二)

.NET、NET Framewor以及.NET Core的关系(二)

还有如进程、内存、反汇编、寄存器等的功能,这里不再一一展示

链接

有关解决方案:https://msdn.microsoft.com/zh-cn/library/b142f8e7(v=vs.110).aspx
有关项目模板: https://msdn.microsoft.com/zh-cn/library/ms247121(v=vs.110).aspx 
有关项目元素的说明介绍:https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2010/16satcwx(v%3dvs.100) 
有关调试更多内容:https://docs.microsoft.com/zh-cn/visualstudio/debugger/
有关代码设计建议:https://docs.microsoft.com/zh-cn/visualstudio/code-quality/code-analysis-for-managed-code-warnings 
有关IntelliTrace介绍:https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2010/dd264915(v%3dvs.100)

建议

我热爱编程。

我知道大多数人对技术的积累都是来自于平常工作中,工作中用到的就去学,用不到就不学,学一年的知识,然后用个五六年。 
我也能理解人的理想和追求不同,有的人可能就想平淡点生活。有的人可能是过了拼劲,习惯了安逸。有的人已经认命了。 
而我现在也每天饱满工作没多少时间,但在下班之余我仍然坚持每天都看一看书。 
想学没时间学,想拼不知道往哪拼。有埋汰自己脑袋笨的,有说自己不感兴趣的。有明明踌躇满志,但总三天捕鱼两天晒网的。我身边的朋友大多都这样。

我想说,尽管我们每个人的境遇、思想、规划不同,但我肯定大家大部分是出于生计而工作。 
而出于生计,那就是为了自己。而既然是为了自己,那就别每天浑浑噩噩过,即使你因各种原因而没有斗志。

编程来不得虚的,如果你没走上管理,那么你的技术好就是好,不好就是不好,混不得,一分技术一分钱。自己不扎实,你运气就不可能太好。
技术是相通的,操作系统、通信、数据结构、协议标准、技术规范、设计模式,语言只是门工具。要知其然也要知其所以然,只知道1个梨+1个梨=2个梨,不知道1个苹果+1个苹果等于啥就悲剧了。

那怎样提升自己?肯定不能像之前那样被动的去学习了。
光靠工作中的积累带来的提升是没有多少。你不能靠1年的技术重复3年的劳动,自己不想提升就不能怨天尤人。
上班大家都一样,我认为成功与否取决于你的业余时间。你每天下班无论再苦都要花一个小时来学习,学什么都行,肯定能改变你的人生轨迹。
比如你每天下班后都用一小时来学一个概念或技术点,那么300天就是300个概念或者技术点,这是何等的恐怖。

当然,这里的学要有点小方法小技巧的。不能太一条道摸到黑的那种,虽然这样最终也能成功,并且印象还深刻,但是总归效率是有点低的。
比如你从网上下载个项目源码,你项目结构不知道,该项目运用技术栈也不太了解,就一点一点的开始解读。这是个提升的好方法,但这样很累,可以成功,但是很慢。见的多懂的少,往往会因为一个概念上的缺失而在一个细小的问题上浪费很长时间。或者说一直漫无目的的看博客来了解技术,那样获取的知识也不系统。

我的建议是读书,书分两类,一类是 讲底层概念的 一类是 讲上层技术实现的。
可以先从上层技术实现的书读起(如何连接数据库、如何写网页、如何写窗体这些)。在有一定编程经验后就从底层概念的书开始读,操作系统的、通信的、数据库的、.NET相关组成的这些…
读完之后再回过头读这些上层技术的书就会看的更明白更透彻,最后再琢磨git下来的项目就显得轻松了。

就.NET CLR组成这一块中文书籍比较少,由浅到深推荐的书有 你必须知道的.NET(挺通俗),CLR C#(挺通俗,进阶必看),如果你想进一步了解CLR,可以看看园子里 包建强http://www.cnblogs.com/Jax/archive/2009/05/25/1488835.html 和中道学友http://www.cnblogs.com/awpatp/archive/2009/11/11/1601397.html翻译的书籍及文章,当然如果你英语合格的话也可以直接阅读他们翻译的来源书籍,我这里有Expert .NET 2.0 IL Assembler的机器翻译版,同时我也建议从调试的方面入手,如 NET高级调试(好多.NET文件调试、反编译的文章都是参考这本书和Apress.Expert.dot.NET.2.0.IL.Assembler(这本书我有机器翻译版)的内容)或者看看Java的JVM的文章。
欢迎加群和我交流(书籍我都放在群文件里了)

现在技术发展很快,我建议大家有基础的可以直接看官方文档,(详细链接我已经在各小节给出)以下是部分常用总链接:

asp.net指南:https://docs.microsoft.com/zh-cn/aspnet/#pivot=core 
Visual Studio IDE 指南:https://docs.microsoft.com/zh-cn/visualstudio/ide/ 
C# 指南: https://docs.microsoft.com/zh-cn/dotnet/csharp/
.NET指南:https://docs.microsoft.com/zh-cn/dotnet/standard/ 
微软开发文档:https://docs.microsoft.com/zh-cn/

最后送给大家我经常做的两句话:
1.先问是不是,再问怎样做,最后我一定会问 为什么
2.没人比谁差多少,相信自己,坚持不断努力,你也能成功

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:9,487
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,903
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,736
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,486
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:8,126
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:5,287