您的位置:首页 > 运维架构 > Linux

ARM的嵌入式Linux移植体验之设备驱动

2008-02-21 14:06 423 查看

第一章:CLI组件模型简介

21世纪的程序员需要为许多事情操心
首先,现在的软件系统越来越复杂。以往的命令提示符终端已经过时,用户要求具有良好体验的图形界面。将数据存放在本地文件系统中的方式也已经很少见了;由于关系数据库的普及,用户已经习惯使用电脑进行联机查询或者打印报表,还有数据的生命周期也被延长了。过去程序部署在本地计算机,人们使用拷贝文件的方式来共享数据;现在网络遍布整个行星,软件要适应各种复杂的网络环境。总而言之,写程序已经从程序员独自编写代码的变成了大型团队使用先进的活动,并前需要更先进的基础架构的支持。。

使用汇编或者C等底层技术从头开发项目的时代已经成为过去。就算只是开发一些例如HTTP协议或者XML解析器的简单工作,人们都觉得没有时间和耐心,何况花时间去提高其性能和质量了。现在人们更加关注可复用的代码和组件。不管你喜不喜欢,程序员们现在不得不基于他人的代码进行软件开发了。

组件式软件:一种将各自独立的代码块合并在一起开发应用程序的技术,已经是时下的潮流。因为大量使用了现有组件提供的功能,开发效率被大幅提高了。不过,这种技术对开发工具和过程提出了新的要求:需要有在运行时对不可信的或未知的开发者的组件进行严格控制和验证的机制。这是因为,目前我们处在一个网络无处不在的时代,基于复杂组件的软件通常可以自动更新,而有时候程序组件也可能在不知不觉的情况下被恶意的替换掉。根据被病毒受害者,或者因为安装和卸载程序导致系统不稳定的人的反馈,很多问题都是组件式软件引起的。

多年以来,企业使用组件程序开发获得的效益被安全方面的代价抵消了。不过在过去的10年中,我们还是看到了使用托管组件机制的商业虚拟执行环境的成功。托管组件作为软件的一部分可以被独立的开发和部署,并且能和应用程序软件安全共存。因为需要一个虚拟执行环境来提供运行时和相关服务,所以它们是“托管”的。这些执行环境都专注于提供安全的操作和协作机制,而不是把精力放在对CPU资源管理等本属于操作系统的职责上,恰好满足组件技术的需求。

虚拟执行环境和托管组件,就像图1-1中所描绘,主要有三种优点:对应用程序开发人员而言,使用托管组件会更容易集成和有更高的开发效率。对于工具的开发者,例如编译器的开发者而言,一个清晰且详细定义的基础架构,可以使其有更多时间来进行工具开发而不用太多操心基础架构和互操作方面的事情。最后,终端用户从使用统一的基础架构和打包方式获益,因为他们都是和特性处理器或操作系统无关的。
图 1-1 托管在虚拟执行环境下的组件能够安全的协作





1.1 CLI虚拟执行环境

ECMA通用语言基础(CLI)是一个虚拟执行环境的标准。它描述了一种数据驱动的架构,其中语言无关的数据使得软件系统可以自组装并且保证类型安全。这些数据被称作元数据,用以描述程序的在内存中布局的行为。CLI执行引擎使用这些元数据来加载和管理组件。尽管CLI组件受到了严格控制,它们依然拥有直接访问彼此的共享资源的能力,CLI模型在控制和灵活性方面取得了很好的平衡。

ECMA,欧洲计算机制造商协会,是一个有着多年历史的标准化组织。除了自己的标准外,ECMA和ISO(国际化标准组织)也有很大的关系,基于此,CLI规范是被批准为ISO/IEC 23271:2006,并且附带一个 指定的ISO:IEC 23272:2006技术报告。C#标准被批准为ISO/IEC 23270:2003
ECMA在互联网上发布了CLI规范,本书的CD中也包含了这些文件,总共有五个大的分卷。在进行CLI标准化的时,一种叫做C#的语言也被纳入了ECMA标准。C#实现了CLI的大部分功能,并且易于学习,本书中也包含了一些C#的示例程序。理论上,C#和CLI是彼此独立的,实际上,它们之间的关系密切,因此许多人甚至认为C#是开发CLI组件的标准语言。
CLI执行引擎加载组件、解析元数据,并编译成可执行代码然后代码在执行引擎的控制下运行。使用这种方式运行的代码称为托管代码,一般使用CLI兼容的语言编写而成。有一套完善的事件链用以从被称为程序集的打包单元中加载元数据,并编译成合适的机器指令。图 1-2是一个简化版事件链,它们是本书的主题。CLI规范的第一部分对此也进行了非常详细的描述。(第8小节描述了通用类型系统,第11小节描述了虚拟执行系统,都是非常好的背景资料)
图 1-2 CLI加载过程的每一个步骤都基于上一步骤中对元数据的分析




在某些方面CLI执行引擎和操作系统很相似,因为它也拥有某些特权并提供相应的服务(例如加载、隔离和调度)和托管资源(如内存和IO)的管理,还有控制托管代码的执行。此外无论操作系统还是CLI引擎,服务既可以被程序直接调用也可以是作为执行模型环境的一部分。(外围服务是运行时计算环境非常重要的一部分,所以其总是处于运行状态)
其他方面,和传统编译器类似,CLI也有编译-连接-加载的过程。CLI规范不仅需要不厌其烦的解释托管程序如何运行,非托管代码也要安全的和托管代码和平共处、共享资源。这些强大的技术都是构建组件程序所需的。

1.1.1 CLI规范的基本概念

隐藏在CLI规范和执行模型后是一组核心概念。CLI的设计中无论是抽象还是具体的技术都贯彻了这些思想,用以使开发人员更好的组织代码。具体而言就是一组设计规则:
· 使用统一的类型进行编程
· 类型被打包成自描述且易部署的单元
· 类型在运行时被独立加载,但是可以共享资源
· 使用基于版本、语言文化(例如时间格式、字符编码)等的灵活的运行时绑定机制(版本、文化)解决intertype依赖问题,
· 在某种程度上可验证的类型是类型安全的,但是不要求所有类型都是类型安全的
· 针对特定CPU的性能优化工作,比如内存布局和编译优化,某种程度上可以在最后进行,但不惩罚在早期处理的工具
· 提供运行时的安全策略
· 基于可扩展的元数据驱动来设计运行时服务,以便进行扩展和添加新功能
这里我们已经接触了这些最重要的概念,本书将带领我们将了解它们的细节。
1.1.1.1 类型
CLI将世界万物按类型分类,程序员使用类型来组织他们代码的结构和行为。组件模型对类型的定义相当简单:类型使用字段和属性来承载数据,使用方法和事件来描述行为(第三章将会详细讨论)。状态和行为既可以存在于实例级别,类型的所有实例只共享相同的数据结构;也可以存在于类型级别,此时类型的所有实例使用同一份数据和方法的dispatch信息拷贝。最后,组件模型支持标准的面向对象结构,比如接口、单继承和构造函数。

类型的结构是以元数据的形式提供给执行引擎、开发人员及其它类型的。元数据非常重要,因为它使得不同的人创建、来源不同、平台不同的类型和平共处并彼此保持独立。默认情况下,CLI只在需要的时候才加载和编译类型。类型和类型之间的引用是基于符号的,也就是说使用某种可以在运行时被解析的名称进行引用而不是预先计算好的地址或者偏移量。这种基于符号的引用为版本控制提供了强大的支持,执行引擎的绑定机制可以找到一个类型的各个不同版本。
类型可以使用单继承的方式从另一个类型继承结构和行为。继承类包含了基类的所有字段和方法,继承类的实例可以当做基类的实例使用。类型最多只能有一个基类,不过类型却可以实现任意个数的接口。所有类型都有一个共同的基类:System.Object,要么是直接从其继承,要么从其他类型继承而来
CLI扩展了字段和方法的概念,为程序员提供了一种更高级的结构:属性和事件。属性允许类型使用任意代码来对外公开数据,而不是直接的内存访问。就某方面而言,属性确实是一种语法糖,其内在表现形式其实是方法;但是从语义上而言,属性是类型元数据中的一等元素,它带来了更一致的API
类型使用事件来通知外部观察者类型内部的活动(例如,告知某个数据可用了或者内部状态发生了改变)。为了提供事件注册的机制,CLI使用委托封装了所需的信息以便执行回调。当注册一个事件时,开发人员需要创建委托。有两种类型的委托:静态委托封装一个指向类型静态方法的指针,实例委托则会关联一个到回调方法的对象的引用。委托通常被当做参数传递给事件注册方法;当类型触发事件时,只需要简单的通过注册的委托执行一下回调方法。
简而言之,类型是一种用字段承载数据,用方法表示行为的组织程序模块的层级结构。不过这个结论简洁却不够完整,模型、属性、事件以及其它的构造等搭建出来的共享程序库和运行是服务才是CLI的魅力所在。
1.1.1.2 共享类型系统和中间语言
CLI底层类型由字段和方法构成,但是字段和方法又是如何定义的呢?CLI规范定义了一种CPU无关的中间语言来描述程序,除此之外还有一套通用类型系统为中间语言提供基元类型。总之,这二者组成了一个抽象的计算模型。规范详细描述了将中间语言和类型系统转化为本地指令流与内存引用的规则,其目的是为了能够精确表达不同的编程语言所表示的语义。中间语言,类型系统及转换规则使CLI能够以语言无关的方式来表示程序。

CLI规范定义的中间语言成为通用中间语言(CIL)。它拥有大量的操作码,基于一个简单的抽象堆栈机,而不依赖于现有的任何硬件架构。同样,通用类型系统(CTS)定义的一系列基元类型充分考虑了标准化和跨语言互操作性。要充分认识这种语言无关特性带来的好处:高级编译器需要能够生成CIL指令和相应的数据类型;例如,C#的int是多大,和Visual Basic的Integer有什么关系?和C++的long是一样的么?通过匹配指令集和类型,做出选择相当的容易,只不过是该使用哪个指令和类型的事情。当然,这由编译器的实现者来控制。但是一个成熟的规范将大大简化选择的过程。这样,不同语言就可以互操作了,带来了更有效地代码重用。第三章将会详细讨论的CLI类型系统,第五章将会讨论如何将CIL转换成本地指令。
1.1.1.3 简单的类型打包模型:程序集
通过类型系统和计算模型,CLI能够支持组件式软件,不同的团队在不同时间编写的代码被验证、加载在一起来构建应用程序。CLI中单个组件被打包成一种被称为程序集的单元。执行引擎按需从本地硬盘、网络加载程序集,也甚至还可以在运行时动态创建。
程序集是CLI组件模型的基本单元。类型不能在程序集之外存在;程序集是CLI能加载的唯一组件。程序集可以包含多个模块,并使用被称作程序集清单的元数据来描述其内容。虽然程序集可以有多个模块组成,不过通常都只包含一个模块。
为了确保程序集不被篡改,可以使用一个哈希密钥对对程序集进行签名,这个签名被存放在清单中。执行引擎加载程序集前会对其进行哈希运算,如果和清单中的哈希签名不一致,执行引擎将会拒绝加载该程序集,并抛出一个异常。
在许多方面,程序集和CLI的关系就像共享库或DLL和操作系统的关系:一种识别代码边界的手段。由于CLI完全使用元数据和符号绑定,组件的加载、验证和执行都是独立的,无论它们之间是否有依赖关系。这是至关重要的,因为平台、应用程序、库还有硬件不断变化。使用CLI构建的组件应该能够适应这些变化。第三章和第四章中将会讨论程序集。
1.1.1.4 组件隔离:应用程序域和Remoting
和把代码组织成组件同样重要的是加载这些组件,使它们能够在一起工作,并提供相应的代码安全保护机制。操作系统通过使用独立的地址空间并提供相应的通信机制来实现这一目的。地址空间提供了保护边界,通信机制提供了相互协作的通道。CLI也有类似的机制来隔离代码的执行,其中就包括应用程序域和Remoting。
程序集总是会被加载到应用程序域中,其结果就是所有类型都在应用程序域的范围内运行。例如,程序集中定义的静态变量都分配在应用程序域中。如果同一程序集被多个不同的域加载了,那么类型相应的数据就会有多份不同的拷贝。本质上应用程序域是“轻量级地址空间”,CLI提供类似操作系统的机制在应用程序域间传递数据。类型如果希望跨应用程序域边界通信必须使用通信信道,并遵守相应的规则。
这种叫做Remoting的技术能够使运行在不同电脑上的应用程序域彼此通信(包括不同操作系统或只是不同进程)。不过通常Remoting都被用在同一进程中的不同应用程序域间进行通信。只有具有序列化的能力或者派生自System.MarshalByRefObject的类型才能被Remoting在应用程序域间传递,后者使用代理对象的方式进行通信。应用程序域、Remoting还有程序集加载的详细信息将在第四章进行讨论。
1.1.1.5 带有版本控制的程序集命名
所有的代码和类型都存放在程序集中,因此需要一套良好的机制来使执行引擎从程序集中发现类型。标准的程序集的名称由文件名、版本号、语言文化还有哈希签名组成。使用这种组合命名的方式能够很好的解决多版本的问题。当进行编译时每一个程序集都会携带其所引用的其他程序集组合名称。最后,只有指定版本的程序集才会被加载。可以通过配置来设置绑定策略,但是它们永远不会被忽略。
程序集可以在两个地方被找到:全局程序集缓存(GAC)或通过基于URL的搜索路径。GAC是一个单机的程序集数据库,其中的数据以程序集全名进行标识。GAC不一定要是文件夹,但是必须能够保存并跟踪一个程序集的多个版本。搜索路径本质上是一组URL的集合(通常是文件夹),可以在这些路径下找到需要加载的程序集。第四章将讨论加载过程及实现方式
1.1.1.6 JIT编译和类型安全
CLI所描述的执行模型意味着高级编译器所生成的类型应该独立于特定处理器指令和内存结构。这种分离为执行模型带来很多高级特性,比如处理器和操作系统适配的能力,还有版本控制的能力。当然也有新的挑战。例如,因为所有类型都是用CIL和CTS描述的,所以必须在真正执行代码之前将其翻译为本地代码和内存结构;本质上,整个应用程序在运行前都必须重新编译,其代价非常昂贵。
为了降低将CIL翻译成本地代码的开销——无论是时间上还是内存占用方面的开销,类型只在需要时才进行加载,而且即使类型被加载进内存了,方法还要等待真正要执行时才被编译。这种推迟代码内存布局和生成的过程被称为JIT编译。CLI并不强制要求一定要等到最后才进行JIT编译,但应用程序生命周期中是隐含了类型延迟加载和JIT编译的。例如,可以想象一下应用程序安装工具可能会对应用程序执行编译。本书将在第五章讨论基于CLI规范的JIT编译
CLI使用JIT编译的最重要的原因其实并不显而易见。把抽象组件翻译为本地运行时代码过程中,在加载器和编译器的控制下执行引擎可以在运行时有控制代码的执行及效率,甚至包括控制托管代码和非托管C++组件的互调。传统的编译过程:链接和加载,在CLI中依然存在。但是如我们所见,每个环节都必须进行大量地优化(例如使用缓存),因为延迟加载会导致运行时的高性能开销。不过这个开销是可以接受的,因为所有相关的行为都会被推后。
由于CLI采用了按需加载类型的机制,而所有类型又用了平台无关的IL语言,所以可以很方便地为执行引擎增加新的编译及运行时行为。CLI被设计成具有类型安全检查的功能,IL代码是在执行引擎的控制下编译成本地指令的,所以可以在代码真正执行之前对其进行类型安全检查。这个时候也可以做一些安全验证的活动,也就是说安全策略是被直接注入到代码中的。总之,通过使用延迟加载、验证还有运行时编译等机制,CLI实现了真正的托管执行
1.1.1.7 托管执行
类型加载是CLI的触发器。在加载的过程中,CLI会进行编译、汇编、链接、对可执行体元数据的验证、类型安全验证等活动,甚至包括托管资源如内存、处理器时钟,一切都在执行引擎的控制下进行。所有这些使得CLI必须拥有名称绑定、内存分配、编译、优化、隔离、同步以及符号解析等基础设施。由于所着这些步骤通常都会被尽可能的推迟,执行引擎可以在加载过程中实施高度控制,包括内存分配、代码生成以及和底层硬件及操作系统平台的交互方式。
延迟的编译、链接、加载提供了更好的包括跨平台和跨版本的可移植性。在最后时刻计算内存地址偏移、选择编译器指令、调用约定、当然还有平台本身的服务,这些提供了更好的向前兼容性。这一切都是基于完善的元数据定义,非常强大。
元数据和延迟加载提高了安全性和稳定性。每一个程序集都可以有一组权限来规定其行为,当方法试图执行一个敏感操作(比如试图读写文件或者访问网络),CLI能够锁住调用堆栈,并且返回以确认当前代码是否具有相关权限。如果没有,那么操作会被拒绝,并且抛出一个异常。(异常是另一种用来简化组件和组件交互的机制,CLI执行引擎不仅为异常提供了良好的支持,还和底层异常及信号处理有很好集成)第六章和第七章讨论托管异常。
1.1.1.8 数据驱动的可扩展元数据
CLI组件是自描述的。一个CLI组件包含了对其所有成员的定义,这些运行时可用的信息给执行引擎的高适配能力提供了帮助。每种类型、方法、字段以及每个方法的每个参数都需要有完整的描述,并存放于程序集中。由于CLI是在最后时刻进行链接,这带来了可通过操作元数据来操作或创建组件的极大灵活性。CLI的这种能力也能被CLI的上层语言使用,这对工具和服务而言都是一笔意外之财。

CLI程序员可以使用反射来获取类型信息。反射提供了在运行时获取类型信息的能力。例如,对于一个给定的托管组件,开发人员可以发现类型的结构,包括构造函数、字段、方法、属性、事件、接口以及继承关系。而且更重要的是,还可以使用Attribute添加自定义元数据信息。
不仅仅是可以找到类型信息。反射还可以操作类型实例,如获取和修改数据,或动态调用方法。我们将在第三章接触到元数据驱动程序设计及其实现,并在第八章展开详细讨论。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: