Shared Source CLI Essentials翻譯(一):1.1 CLI虛擬執行環境

第一章:CLI組件模型簡介

21世紀的程序員需要爲許多事情操心

首先,現在的軟件系統越來越複雜。以往的命令提示符終端已經過時,用戶要求具有良好體驗的圖形界面。將數據存放在本地文件系統中的方式也已經很少見了;由於關係數據庫的普及,用戶已經習慣使用電腦進行聯機查詢或者打印報表,還有數據的生命週期也被延長了。過去程序部署在本地計算機,人們使用拷貝文件的方式來共享數據;現在網絡遍佈整個行星,軟件要適應各種複雜的網絡環境。總而言之,寫程序已經從程序員獨自編寫代碼的變成了大型團隊使用先進的活動,並前需要更先進的基礎架構的支持。。

使用匯編或者C等底層技術從頭開發項目的時代已經成爲過去。就算只是開發一些例如HTTP協議或者XML解析器的簡單工作,人們都覺得沒有時間和耐心,何況花時間去提高其性能和質量了。現在人們更加關注可複用的代碼和組件。不管你喜不喜歡,程序員們現在不得不基於他人的代碼進行軟件開發了。

組件式軟件:一種將各自獨立的代碼塊合併在一起開發應用程序的技術,已經是時下的潮流。因爲大量使用了現有組件提供的功能,開發效率被大幅提高了。不過,這種技術對開發工具和過程提出了新的要求:需要有在運行時對不可信的或未知的開發者的組件進行嚴格控制和驗證的機制。這是因爲,目前我們處在一個網絡無處不在的時代,基於複雜組件的軟件通常可以自動更新,而有時候程序組件也可能在不知不覺的情況下被惡意的替換掉。根據被病毒受害者,或者因爲安裝和卸載程序導致系統不穩定的人的反饋,很多問題都是組件式軟件引起的。

多年以來,企業使用組件程序開發獲得的效益被安全方面的代價抵消了。不過在過去的10年中,我們還是看到了使用託管組件機制的商業虛擬執行環境的成功。託管組件作爲軟件的一部分可以被獨立的開發和部署,並且能和應用程序軟件安全共存。因爲需要一個虛擬執行環境來提供運行時和相關服務,所以它們是“託管”的。這些執行環境都專注於提供安全的操作和協作機制,而不是把精力放在對CPU資源管理等本屬於操作系統的職責上,恰好滿足組件技術的需求。

虛擬執行環境和託管組件,就像圖1-1中所描繪,主要有三種優點:對應用程序開發人員而言,使用託管組件會更容易集成和有更高的開發效率。對於工具的開發者,例如編譯器的開發者而言,一個清晰且詳細定義的基礎架構,可以使其有更多時間來進行工具開發而不用太多操心基礎架構和互操作方面的事情。最後,終端用戶從使用統一的基礎架構和打包方式獲益,因爲他們都是和特性處理器或操作系統無關的。

1-1 託管在虛擬執行環境下的組件能夠安全的協作


1.1 CLI虛擬執行環境

ECMA通用語言基礎(CLI)是一個虛擬執行環境的標準。它描述了一種數據驅動的架構,其中語言無關的數據使得軟件系統可以自組裝並且保證類型安全。這些數據被稱作元數據,用以描述程序的在內存中佈局的行爲。CLI執行引擎使用這些元數據來加載和管理組件。儘管CLI組件受到了嚴格控制,它們依然擁有直接訪問彼此的共享資源的能力,CLI模型在控制和靈活性方面取得了很好的平衡。

ECMA,歐洲計算機製造商協會,是一個有着多年曆史的標準化組織。除了自己的標準外,ECMAISO(國際化標準組織)也有很大的關係,基於此,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 BasicInteger有什麼關係?和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所描述的執行模型意味着高級編譯器所生成的類型應該獨立於特定處理器指令和內存結構。這種分離爲執行模型帶來很多高級特性,比如處理器和操作系統適配的能力,還有版本控制的能力。當然也有新的挑戰。例如,因爲所有類型都是用CILCTS描述的,所以必須在真正執行代碼之前將其翻譯爲本地代碼和內存結構;本質上,整個應用程序在運行前都必須重新編譯,其代價非常昂貴。

爲了降低將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添加自定義元數據信息。

不僅僅是可以找到類型信息。反射還可以操作類型實例,如獲取和修改數據,或動態調用方法。我們將在第三章接觸到元數據驅動程序設計及其實現,並在第八章展開詳細討論。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章