爲什麼要動態鏈接,動態鏈接庫和靜態連接的區別與優勢

本文摘抄於程序員的自我修養-鏈接裝載與庫7.1節,這段寫的很好,直接拿過來來收藏
http://www.wq3028.top/technology/compile/20180727124/

靜態鏈接使得不同的程序開發者和部門能夠相對獨立地開發和測試自己的程序模塊,從某種意義上來講大大促進了程序開發的效率,原先限制程序的規模也隨之擴大。但是慢慢地靜態鏈接的諸多缺點也逐步暴露出來,比如浪費內存和磁盤空間、模塊更新困難等問題,使得人們不得不尋找一種更好的方式來組織程序的模塊。

內存和磁盤空間
靜態鏈接這種方法的確很簡單,原理上很容易理解,實踐上很難實現,在操作系統和硬件不發達的早期,絕大部分系統採用這種方案。隨着計算機軟件的發展,這種方法的缺點很快就暴露出來了,那就是靜態連接的方式對於計算機內存和磁盤的空間浪費非常嚴重。特別是多進程操作系統情況下,靜態鏈接極大地浪費了內存空間,想象一下每個程序內部除了都保留着printf()函數、scanf()函數、strlen()等這樣的公用庫函數,還有數量相當可觀的其他庫函數及它們所需要的輔助數據結構。在現在的Linux系統中,一個普通程序會使用到的C語言靜態庫至少在1 MB以上,那麼,如果我們的機器中運行着100個這樣的程序,就要浪費近100 MB的內存;如果磁盤中有2 000個這樣的程序,就要浪費近2 GB的磁盤空間,很多Linux的機器中,/usr/bin下就有數千個可執行文件。

比如圖7-1所示的Program1和Program2分別包含Program1.o和Program2.o兩個模塊,並且它們還共用Lib.o這兩模塊。在靜態連接的情況下,因爲Program1和Program2都用到了Lib.o這個模塊,所以它們同時在鏈接輸出的可執行文件Program1和Program2有兩個副本。當我們同時運行Program1和Program2時,Lib.o在磁盤中和內存中都有兩份副本。當系統中存在大量的類似於Lib.o的被多個程序共享的目標文件時,其中很大一部分空間就被浪費了。在靜態鏈接中,C語言靜態庫是很典型的浪費空間的例子,還有其他數以千計的庫如果都需要靜態鏈接,那麼空間浪費無法想象。

這裏寫圖片描述
圖7-1 靜態鏈接時文件在內存中的副本

程序開發和發佈
空間浪費是靜態鏈接的一個問題,另一個問題是靜態鏈接對程序的更新、部署和發佈也會帶來很多麻煩。比如程序Program1所使用的Lib.o是由一個第三方廠商提供的,當該廠商更新了Lib.o的時候(比如修正了lib.o裏面包含的一個Bug),那麼Program1的廠商就需要拿到最新版的Lib.o,然後將其與Program1.o鏈接後,將新的Program1整個發佈給用戶。這樣做的缺點很明顯,即一旦程序中有任何模塊更新,整個程序就要重新鏈接、發佈給用戶。比如一個程序有20個模塊,每個模塊1 MB,那麼每次更新任何一個模塊,用戶就得重新獲取這個20 MB的程序。如果程序都使用靜態鏈接,那麼通過網絡來更新程序將會非常不便,因爲一旦程序任何位置的一個小改動,都會導致整個程序重新下載。

動態鏈接
要解決空間浪費和更新困難這兩個問題最簡單的辦法就是把程序的模塊相互分割開來,形成獨立的文件,而不再將它們靜態地鏈接在一起。簡單地講,就是不對那些組成程序的目標文件進行鏈接,等到程序要運行時才進行鏈接。也就是說,把鏈接這個過程推遲到了運行時再進行,這就是動態鏈接(Dynamic Linking)的基本思想。

還是以Program1和Program2爲例,假設我們保留Program1.o、Program2.o和Lib.o三個目標文件。當我們要運行Program1這個程序時,系統首先加載Program1.o,當系統發現Program1.o中用到了Lib.o,即Program1.o依賴於Lib.o,那麼系統接着加載Lib.o,如果Program1.o或Lib.o還依賴於其他目標文件,系統會按照這種方法將它們全部加載至內存。所有需要的目標文件加載完畢之後,如果依賴關係滿足,即所有依賴的目標文件都存在於磁盤,系統開始進行鏈接工作。這個鏈接工作的原理與靜態鏈接非常相似,包括符號解析、地址重定位等,我們在前面已經很詳細地介紹過了。完成這些步驟之後,系統開始把控制權交給Program1.o的程序入口處,程序開始運行。這時如果我們需要運行Program2,那麼系統只需要加載Program2.o,而不需要重新加載Lib.o,因爲內存中已經存在了一份Lib.o的副本(見圖7-2),系統要做的只是將Program2.o和Lib.o鏈接起來。很明顯,上面的這種做法解決了共享的目標文件多個副本浪費磁盤和內存空間的問題,可以看到,磁盤和內存中只存在一份Lib.o,而不是兩份。另外在內存中共享一個目標文件

這裏寫圖片描述

圖7-2 動態鏈接時文件在內存中的副本

模塊的好處不僅僅是節省內存,它還可以減少物理頁面的換入換出,也可以增加CPU緩存的命中率,因爲不同進程間的數據和指令訪問都集中在了同一個共享模塊上。上面的動態鏈接方案也可以使程序的升級變得更加容易,當我們要升級程序庫或程序共享的某個模塊時,理論上只要簡單地將舊的目標文件覆蓋掉,而無須將所有的程序再重新鏈接一遍。當程序下一次運行的時候,新版本的目標文件會被自動裝載到內存並且鏈接起來,程序就完成了升級的目標。當一個程序產品的規模很大的時候,往往會分割成多個子系統及多個模塊,每個模塊都由獨立的小組開發,甚至會使用不同的編程語言。動態鏈接的方式使得開發過程中各個模塊更加獨立,耦合度更小,便於不同的開發者和開發組織之間獨立進行開發和測試。

程序可擴展性和兼容性
動態鏈接還有一個特點就是程序在運行時可以動態地選擇加載各種程序模塊,這個優點就是後來被人們用來製作程序的插件(Plug-in)。比如某個公司開發完成了某個產品,它按照一定的規則制定好程序的接口,其他公司或開發者可以按照這種接口來編寫符合要求的動態鏈接文件。該產品程序可以動態地載入各種由第三方開發的模塊,在程序運行時動態地鏈接,實現程序功能的擴展。動態鏈接還可以加強程序的兼容性。一個程序在不同的平臺運行時可以動態地鏈接到由操作系統提供的動態鏈接庫,這些動態鏈接庫相當於在程序和操作系統之間增加了一箇中間層,從而消除了程序對不同平臺之間依賴的差異性。比如操作系統A和操作系統B對於printf()的實現制不同,如果我們的程序是靜態鏈接的,那麼程序需要分別鏈接成能夠在A運行和在B運行的兩個版本並且分開發布;但是如果是動態鏈接,只要操作系統A和操作系統B都能提供一個動態鏈接庫包含printf(),並且這個printf()使用相同的接口,那麼程序只需要有一個版本,就可以在兩個操作系統上運行,動態地選擇相應的printf()的實現版本。當然這只是理論上的可能性,實際上還存在不少問題,我們會在後面繼續探討關於動態鏈接模塊之間兼容性的問題。

從上面的描述來看,動態鏈接是不是一種“萬能膏藥”,包治百病呢?很遺憾,動態鏈接也有諸多的問題及令人煩惱和費解的地方。很常見的一個問題是,當程序所依賴的某個模塊更新後,由於新的模塊與舊的模塊之間接口不兼容,導致了原有的程序無法運行。這個問題在早期的Windows版本中尤爲嚴重,因爲它們缺少一種有效的共享庫版本管理機制,使得用戶經常出現新程序安裝完之後,其他某個程序無法正常工作的現象,這個問題也經常被稱爲“DLLHell”。

動態鏈接的基本實現
動態鏈接的基本思想是把程序按照模塊拆分成各個相對獨立部分,在程序運行時纔將它們鏈接在一起形成一個完整的程序,而不是像靜態鏈接一樣把所有的程序模塊都鏈接成一個個單獨的可執行文件。那麼我們能不能按照前面例子中所描述的那樣,直接使用目標文件進行動態鏈接呢?這個問題的答案是:理論上是可行的,但實際上動態鏈接的實現方案與直接使用目標文件稍有差別。我們將在後面分析目標文件和動態鏈接文件的區別。動態鏈接涉及運行時的鏈接及多個文件的裝載,必需要有操作系統的支持,因爲動態鏈接的情況下,進程的虛擬地址空間的分佈會比靜態鏈接情況下更爲複雜,還有一些存儲管理、內存共享、進程線程等機制在動態鏈接下也會有一些微妙的變化。目前主流的操作系統幾乎都支持動態鏈接這種方式,在Linux系統中,ELF動態鏈接文件被稱爲動態共享對象(DSO,Dynamic Shared Objects),簡稱共享對象,它們一般都是以“.so”爲擴展名的一些文件;而在Windows系統中,動態鏈接文件被稱爲動態鏈接庫(Dynamical Linking Library),它們通常就是我們平時很常見的以“.dll”爲擴展名的文件。

從本質上講,普通可執行程序和動態鏈接庫中都包含指令和數據,這一點沒有區別。在使用動態鏈接庫的情況下,程序本身被分爲了程序主要模塊(Program1)和動態鏈接庫(Lib.so),但實際上它們都可以看作是整個程序的一個模塊,所以當我們提到程序模塊時可以指程序主模塊也可以指動態鏈接庫。在Linux中,常用的C語言庫的運行庫glibc,它的動態鏈接形式的版本保存在“/lib”目錄下,文件名叫做“libc.so”。整個系統只保留一份C語言庫的動態鏈接文件“libc.so”,而所有的C語言編寫的、動態鏈接的程序都可以在運行時使用它。當程序被裝載的時候,系統的動態鏈接器會將程序所需要的所有動態鏈接庫(最基本的就是libc.so)裝載到進程的地址空間,並且將程序中所有未決議的符號綁定到相應的動態鏈接庫中,並進行重定位工作。程序與libc.so之間真正的鏈接工作是由動態鏈接器完成的,而不是由我們前面看到過的靜態鏈接器ld完成的。也就是說,動態鏈接是把鏈接這個過程從本來的程序裝載前被推遲到了裝載的時候。可能有人會問,這樣的做法的確很靈活,但是程序每次被裝載時都要進行重新進行鏈接,是不是很慢?的確,動態鏈接會導致程序在性能的一些損失,但是對動態鏈接的鏈接過程可以進行優化,比如我們後面要介紹的延遲綁定(Lazy Binding)等方法,可以使得動態鏈接的性能損失儘可能地減小。據估算,動態鏈接與靜態鏈接相比,性能損失大約在5%以下。當然經過實踐的證明,這點性能損失用來換取程序在空間上的節省和程序構建和升級時的靈活性,是相當值得的。

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